feat: complete cmpp gateway delivery recovery workflows
This commit is contained in:
+211
-5
@@ -24,6 +24,23 @@ async function request<T>(path: string, options: RequestOptions = {}): Promise<T
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function requestBlob(path: string, options: RequestOptions = {}): Promise<Blob> {
|
||||
const headers = new Headers(options.headers);
|
||||
const session = readSession();
|
||||
if (session?.accessToken) {
|
||||
headers.set('Authorization', `${session.tokenType} ${session.accessToken}`);
|
||||
}
|
||||
const tenantId = options.tenantId ?? (path.startsWith('/client') ? getSessionTenantId() : undefined);
|
||||
if (tenantId) {
|
||||
headers.set('x-tenant-id', tenantId);
|
||||
}
|
||||
const response = await fetch(`/api${path}`, { ...options, headers });
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
export type AdminChannel = {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -38,7 +55,7 @@ export type AdminChannel = {
|
||||
rateLimitPerSecond: number;
|
||||
unitPrice: number;
|
||||
status: string;
|
||||
config?: unknown;
|
||||
config?: { desiredConnections?: number; windowSize?: number; [key: string]: unknown } | null;
|
||||
connectionStates?: CmppConnectionState[];
|
||||
};
|
||||
|
||||
@@ -151,6 +168,14 @@ export type DashboardResponse = {
|
||||
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null; smsUnits?: number | null } };
|
||||
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
|
||||
pendingAuditCount: number;
|
||||
downstreamDeliverySummary?: {
|
||||
pending: number;
|
||||
failed: number;
|
||||
delivered: number;
|
||||
stalledPending: number;
|
||||
recentFailed: number;
|
||||
alertCount: number;
|
||||
};
|
||||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; smsUnits: number; creditCents: number; status: string; tenant?: TenantOption }>;
|
||||
recentTasks: Array<Record<string, unknown>>;
|
||||
recentRecharges: Array<RechargeOrder>;
|
||||
@@ -298,19 +323,71 @@ export type SmsMessageRecord = {
|
||||
receiptRecords?: Array<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export type SmsMessageSegmentAudit = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
batchTaskId?: string | null;
|
||||
messageRecordId: string;
|
||||
submitRecordId?: string | null;
|
||||
channelId?: string | null;
|
||||
submitId: string;
|
||||
attempt: number;
|
||||
segmentTotal: number;
|
||||
segmentIndex: number;
|
||||
sequenceId?: number | null;
|
||||
gatewayMessageId?: string | null;
|
||||
submitStatus: string;
|
||||
receiptStatus?: string | null;
|
||||
rawStatus?: string | null;
|
||||
compensationType?: string | null;
|
||||
errorCode?: string | null;
|
||||
errorMessage?: string | null;
|
||||
submittedAt?: string | null;
|
||||
deliveredAt?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
channel?: AdminChannel | null;
|
||||
};
|
||||
|
||||
export type SmsUplinkMessage = {
|
||||
id: string;
|
||||
tenantId?: string | null;
|
||||
channelId: string;
|
||||
applicationId?: string | null;
|
||||
messageRecordId?: string | null;
|
||||
messageId?: string | null;
|
||||
sequenceId?: number | null;
|
||||
phoneNumber: string;
|
||||
destId: string;
|
||||
content: string;
|
||||
matchStatus?: string;
|
||||
matchReason?: string | null;
|
||||
receivedAt: string;
|
||||
createdAt: string;
|
||||
tenant?: TenantOption | null;
|
||||
application?: { id: string; name: string } | null;
|
||||
messageRecord?: SmsMessageRecord | null;
|
||||
channel?: AdminChannel | null;
|
||||
matchCandidates?: SmsUplinkMatchCandidate[];
|
||||
};
|
||||
|
||||
export type SmsUplinkMatchCandidate = {
|
||||
id: string;
|
||||
uplinkMessageId: string;
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
messageRecordId?: string | null;
|
||||
matchSource: string;
|
||||
confidence: number;
|
||||
reason?: string | null;
|
||||
status: string;
|
||||
claimedAt?: string | null;
|
||||
claimedById?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tenant?: TenantOption | null;
|
||||
application?: { id: string; name: string } | null;
|
||||
messageRecord?: SmsMessageRecord | null;
|
||||
};
|
||||
|
||||
export type DictionaryItem = Record<string, unknown> & {
|
||||
@@ -448,6 +525,13 @@ export type OperationLogResponse = {
|
||||
modules: string[];
|
||||
};
|
||||
|
||||
export type PagedResponse<T> = {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
export type EnterpriseApplication = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
@@ -459,6 +543,9 @@ export type EnterpriseApplication = {
|
||||
queuePriority?: 'normal' | 'priority' | string | null;
|
||||
maxPhonesPerTask?: number | null;
|
||||
templateMismatchMode?: string | null;
|
||||
cmppAccount?: string | null;
|
||||
cmppMaxConnections?: number | null;
|
||||
cmppWindowSize?: number | null;
|
||||
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
|
||||
tenant?: TenantOption;
|
||||
sentToday?: number;
|
||||
@@ -508,6 +595,107 @@ export type ApplicationCmppParams = {
|
||||
protocolVersion: string;
|
||||
};
|
||||
|
||||
export type DownstreamDeliveryRecord = {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
applicationId: string;
|
||||
messageRecordId?: string | null;
|
||||
messageId?: string | null;
|
||||
deliveryType: string;
|
||||
status: string;
|
||||
payload: Record<string, unknown>;
|
||||
retryCount: number;
|
||||
nextRetryAt?: string | null;
|
||||
deliveredAt?: string | null;
|
||||
lastError?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tenant?: TenantOption | null;
|
||||
application?: EnterpriseApplication | null;
|
||||
messageRecord?: SmsMessageRecord | null;
|
||||
};
|
||||
|
||||
export type BatchRequeueResponse = {
|
||||
total: number;
|
||||
successCount: number;
|
||||
failedCount: number;
|
||||
results: Array<{ id: string; status: 'success' | 'failed'; errorMessage?: string }>;
|
||||
};
|
||||
|
||||
export type DownstreamDeliveryDashboard = {
|
||||
summary: {
|
||||
total: number;
|
||||
pending: number;
|
||||
delivered: number;
|
||||
failed: number;
|
||||
stalledPending: number;
|
||||
recentFailed: number;
|
||||
alertCount: number;
|
||||
};
|
||||
typeBreakdown: Array<{
|
||||
deliveryType: string;
|
||||
total: number;
|
||||
pending: number;
|
||||
delivered: number;
|
||||
failed: number;
|
||||
}>;
|
||||
retryBuckets: Array<{
|
||||
label: string;
|
||||
count: number;
|
||||
}>;
|
||||
topApplications: Array<{
|
||||
applicationId: string;
|
||||
name: string;
|
||||
pending: number;
|
||||
failed: number;
|
||||
delivered: number;
|
||||
alertCount: number;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type GatewayDownstreamRecoveryStatus = {
|
||||
id: string;
|
||||
account: string;
|
||||
tenantId?: string | null;
|
||||
applicationId?: string | null;
|
||||
gatewayInstanceId?: string | null;
|
||||
state: string;
|
||||
lockOwner?: string | null;
|
||||
lockExpiresAt?: string | null;
|
||||
lastAttemptAt?: string | null;
|
||||
lastSuccessAt?: string | null;
|
||||
lastFailureAt?: string | null;
|
||||
nextRetryAt?: string | null;
|
||||
attemptCount: number;
|
||||
failureCategory?: string | null;
|
||||
lastError?: string | null;
|
||||
lastSkipReason?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tenant?: TenantOption | null;
|
||||
application?: EnterpriseApplication | null;
|
||||
};
|
||||
|
||||
export type DownstreamRecoveryStatusResponse = PagedResponse<GatewayDownstreamRecoveryStatus> & {
|
||||
summary: {
|
||||
total: number;
|
||||
running: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
waitingConnection: number;
|
||||
backoff: number;
|
||||
failureCategories: Array<{ category: string; count: number }>;
|
||||
};
|
||||
};
|
||||
|
||||
export type DownstreamRecoveryStatusExportQuery = {
|
||||
tenantId?: string;
|
||||
applicationId?: string;
|
||||
state?: string;
|
||||
failureCategory?: string;
|
||||
keyword?: string;
|
||||
};
|
||||
|
||||
function withQuery(path: string, query: Record<string, string | number | undefined>) {
|
||||
const params = new URLSearchParams();
|
||||
Object.entries(query).forEach(([key, value]) => {
|
||||
@@ -552,9 +740,9 @@ export const adminApi = {
|
||||
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
|
||||
getEnterpriseApplication: (id: string) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`),
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
|
||||
request<EnterpriseApplication>('/admin/enterprise-applications', { method: 'POST', body: JSON.stringify(body) }),
|
||||
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; ipAllowlist?: string[] }) =>
|
||||
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; maxPhonesPerTask?: number; templateMismatchMode?: string; cmppAccount?: string; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
changeApplicationStatus: (id: string, status: string, reason?: string) =>
|
||||
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}/status`, {
|
||||
@@ -571,9 +759,9 @@ export const adminApi = {
|
||||
getApplicationCmppParams: (applicationId: string) =>
|
||||
request<ApplicationCmppParams>(`/admin/enterprise-applications/${applicationId}/cmpp-params`),
|
||||
listChannels: () => request<AdminChannel[]>('/admin/channels'),
|
||||
createChannel: (body: Partial<AdminChannel> & { passwordCipher?: string }) =>
|
||||
createChannel: (body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number }) =>
|
||||
request<AdminChannel>('/admin/channels', { method: 'POST', body: JSON.stringify(body) }),
|
||||
updateChannel: (id: string, body: Partial<AdminChannel> & { passwordCipher?: string }) =>
|
||||
updateChannel: (id: string, body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number }) =>
|
||||
request<AdminChannel>(`/admin/channels/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
copyChannel: (id: string, body: { operatorId?: string } = {}) => request<AdminChannel>(`/admin/channels/${id}/copy`, {
|
||||
method: 'POST',
|
||||
@@ -664,12 +852,30 @@ export const adminApi = {
|
||||
request<SmsBatchTask>(`/admin/send/batch-tasks/${id}/terminate`, { method: 'POST', body: JSON.stringify({}) }),
|
||||
listAdminMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; taskId?: string; phoneNumber?: string; status?: string } = {}) =>
|
||||
request<SmsMessageRecord[]>(withQuery('/admin/send/messages', query)),
|
||||
listMessageSegmentAudits: (query: { messageId?: string; messageRecordId?: string }) =>
|
||||
request<SmsMessageSegmentAudit[]>(withQuery('/admin/operations/message-segment-audits', query)),
|
||||
listOperationMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; taskId?: string; messageId?: string; phoneNumber?: string; status?: string } = {}) =>
|
||||
request<SmsMessageRecord[]>(withQuery('/admin/operations/messages', query)),
|
||||
listAdminUplinkMessages: (query: { tenantId?: string; channelId?: string } = {}) =>
|
||||
request<SmsUplinkMessage[]>(withQuery('/admin/operations/uplink-messages', query)),
|
||||
claimUplinkMatchCandidate: (uplinkMessageId: string, body: { candidateId: string; operatorId?: string }) =>
|
||||
request<SmsUplinkMessage>(`/admin/operations/uplink-messages/${uplinkMessageId}/claim`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
listMonitor: (query: { tenantId?: string; channelId?: string } = {}) => request<Record<string, unknown>>(withQuery('/admin/operations/monitor', query)),
|
||||
listStatistics: (query: { tenantId?: string; groupBy?: string } = {}) => request<Array<Record<string, unknown>>>(withQuery('/admin/operations/statistics', query)),
|
||||
getDownstreamDeliveryDashboard: (query: { tenantId?: string; applicationId?: string; deliveryType?: string } = {}) =>
|
||||
request<DownstreamDeliveryDashboard>(withQuery('/admin/operations/downstream-deliveries/dashboard', query)),
|
||||
listDownstreamRecoveryStatuses: (query: { tenantId?: string; applicationId?: string; state?: string; failureCategory?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||||
request<DownstreamRecoveryStatusResponse>(withQuery('/admin/operations/downstream-recovery-statuses', query)),
|
||||
getDownstreamRecoveryStatus: (id: string) =>
|
||||
request<GatewayDownstreamRecoveryStatus>(`/admin/operations/downstream-recovery-statuses/${id}`),
|
||||
exportDownstreamRecoveryStatuses: (query: DownstreamRecoveryStatusExportQuery = {}) =>
|
||||
requestBlob(withQuery('/admin/operations/downstream-recovery-statuses/export', query)),
|
||||
listDownstreamDeliveries: (query: { tenantId?: string; applicationId?: string; deliveryType?: string; status?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||||
request<PagedResponse<DownstreamDeliveryRecord>>(withQuery('/admin/operations/downstream-deliveries', query)),
|
||||
requeueDownstreamDelivery: (id: string) =>
|
||||
request<DownstreamDeliveryRecord>(`/admin/operations/downstream-deliveries/${id}/requeue`, { method: 'POST', body: JSON.stringify({}) }),
|
||||
batchRequeueDownstreamDeliveries: (ids: string[]) =>
|
||||
request<BatchRequeueResponse>('/admin/operations/downstream-deliveries/requeue', { method: 'POST', body: JSON.stringify({ ids }) }),
|
||||
listRiskReviewTasks: (query: { tenantId?: string; status?: string } = {}) => request<RiskReviewTask[]>(withQuery('/admin/risk-review/tasks', query)),
|
||||
approveRiskReviewTask: (id: string, reason?: string) =>
|
||||
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/approve`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||||
|
||||
Reference in New Issue
Block a user