feat: complete cmpp gateway delivery recovery workflows

This commit is contained in:
hectorzhao
2026-07-08 16:30:06 +08:00
parent cc628d0214
commit 8144f08652
60 changed files with 8901 additions and 94 deletions
+211 -5
View File
@@ -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 }) }),