1618 lines
81 KiB
TypeScript
1618 lines
81 KiB
TypeScript
import { clearSession, dispatchSessionEvent, getSessionTenantId, hasRecentUserActivity, readSession, requestReauthentication, type LoginSession } from './session';
|
||
import { assertUploadFileSize } from '@/utils/fileUpload';
|
||
|
||
type RequestOptions = RequestInit & {
|
||
tenantId?: string;
|
||
reauthenticationAttempted?: boolean;
|
||
};
|
||
|
||
type ApiErrorBody = { message?: string | string[]; error?: string; code?: string };
|
||
|
||
async function readErrorBody(response: Response): Promise<ApiErrorBody> {
|
||
const text = await response.text();
|
||
if (!text) return {};
|
||
try {
|
||
return JSON.parse(text) as ApiErrorBody;
|
||
} catch {
|
||
return { message: text };
|
||
}
|
||
}
|
||
|
||
export const DEFAULT_CLIENT_TENANT_ID = 'tenant-a';
|
||
|
||
async function readErrorMessage(response: Response) {
|
||
const fallback = `请求失败(${response.status})`;
|
||
const text = await response.text();
|
||
if (!text) return fallback;
|
||
|
||
try {
|
||
const parsed = JSON.parse(text) as { message?: string | string[]; error?: string };
|
||
if (Array.isArray(parsed.message)) return parsed.message.join(';');
|
||
if (parsed.message) return parsed.message;
|
||
if (parsed.error) return parsed.error;
|
||
} catch {
|
||
return text;
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||
const headers = new Headers(options.headers);
|
||
headers.set('Content-Type', 'application/json');
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
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, credentials: 'same-origin' });
|
||
const isLoginAttempt = path === '/admin/auth/login' || path === '/client/auth/login';
|
||
if (response.status === 401 && session && !isLoginAttempt) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: body.message });
|
||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||
}
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
throw new Error('登录会话已失效,请重新登录');
|
||
}
|
||
if (response.status === 403 && session && !options.reauthenticationAttempted) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||
await requestReauthentication();
|
||
return request<T>(path, { ...options, reauthenticationAttempted: true });
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await readErrorMessage(response));
|
||
}
|
||
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 && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
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, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: body.message });
|
||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||
}
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
throw new Error('登录会话已失效,请重新登录');
|
||
}
|
||
if (response.status === 403 && session && !options.reauthenticationAttempted) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||
await requestReauthentication();
|
||
return requestBlob(path, { ...options, reauthenticationAttempted: true });
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await readErrorMessage(response));
|
||
}
|
||
return response.blob();
|
||
}
|
||
|
||
async function requestForm<T>(path: string, form: FormData, reauthenticationAttempted = false): Promise<T> {
|
||
const headers = new Headers();
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
const response = await fetch(`/api${path}`, { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: body.message });
|
||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||
}
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
throw new Error('登录会话已失效,请重新登录');
|
||
}
|
||
if (response.status === 403 && session && !reauthenticationAttempted) {
|
||
const body = await readErrorBody(response.clone());
|
||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||
await requestReauthentication();
|
||
return requestForm<T>(path, form, true);
|
||
}
|
||
}
|
||
if (!response.ok) throw new Error(await readErrorMessage(response));
|
||
return response.json() as Promise<T>;
|
||
}
|
||
|
||
export type AdminChannel = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
carrier?: string | null;
|
||
sendRegion?: string | null;
|
||
gatewayHost: string;
|
||
gatewayPort: number;
|
||
enterpriseCode?: string | null;
|
||
account: string;
|
||
srcId: string;
|
||
cmppVersion?: '2.0' | '3.0' | string | null;
|
||
rateLimitPerSecond: number;
|
||
unitPrice: number;
|
||
status: string;
|
||
config?: { desiredConnections?: number; windowSize?: number; extensionDigits?: number; [key: string]: unknown } | null;
|
||
connectionStates?: CmppConnectionState[];
|
||
};
|
||
|
||
export type ChannelConnectionLogResponse = {
|
||
channelId: string;
|
||
connectionStates: CmppConnectionState[];
|
||
logs: Array<{
|
||
id: string;
|
||
time: string;
|
||
event: string;
|
||
action: string;
|
||
resourceId?: string;
|
||
detail?: unknown;
|
||
}>;
|
||
};
|
||
|
||
export type ChannelTestResponse = {
|
||
channelId: string;
|
||
status: string;
|
||
testNo: string;
|
||
submitted: number;
|
||
messages: Array<{
|
||
phoneNumber: string;
|
||
messageRecordId: string;
|
||
submitId: string;
|
||
streamMessageId?: string;
|
||
}>;
|
||
queuedAt: string;
|
||
};
|
||
|
||
export type EnterpriseCertification = {
|
||
id: string;
|
||
tenantId: string;
|
||
companyName: string;
|
||
licenseNo?: string | null;
|
||
contactName?: string | null;
|
||
contactPhone?: string | null;
|
||
materials?: Record<string, unknown> | null;
|
||
status: string;
|
||
rejectReason?: string | null;
|
||
submittedAt: string;
|
||
reviewedAt?: string | null;
|
||
tenant?: { id: string; name: string; code: string };
|
||
};
|
||
|
||
export type SmsTemplateAudit = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
name: string;
|
||
content: string;
|
||
category?: string | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
application?: { name: string };
|
||
tenant?: { name: string };
|
||
};
|
||
|
||
export type TenantOption = {
|
||
id: string;
|
||
name: string;
|
||
code: string;
|
||
status: string;
|
||
enterpriseProfile?: {
|
||
creditCode?: string;
|
||
province?: string;
|
||
city?: string;
|
||
address?: string;
|
||
contactName?: string;
|
||
contactIdCard?: string;
|
||
contactPhone?: string;
|
||
contactEmail?: string;
|
||
photoFileObjectId?: string;
|
||
} | null;
|
||
};
|
||
|
||
export type TenantManagementRow = TenantOption & {
|
||
account?: TenantAccount | null;
|
||
todaySpendCents: number;
|
||
todayRefundCents: number;
|
||
};
|
||
|
||
export type CaptchaResponse = {
|
||
captchaId: string;
|
||
challenge: string;
|
||
expiresInSeconds: number;
|
||
};
|
||
|
||
export type ManagedUser = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
username: string;
|
||
email?: string | null;
|
||
phone?: string | null;
|
||
displayName: string;
|
||
status: string;
|
||
failedLoginCount: number;
|
||
lockedUntil?: string | null;
|
||
lastLoginAt?: string | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption | null;
|
||
roles: Array<{ role: { code: string; name: string; scope: string } }>;
|
||
};
|
||
|
||
export type UserPayload = {
|
||
tenantId?: string | null;
|
||
username?: string;
|
||
email?: string | null;
|
||
phone?: string | null;
|
||
displayName: string;
|
||
password?: string;
|
||
status?: string;
|
||
roleCode: 'platform_admin' | 'enterprise_admin';
|
||
operatorId?: string;
|
||
};
|
||
|
||
export type DashboardResponse = {
|
||
taskCount: number;
|
||
messageStatus: Array<{ status: string; _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } }>;
|
||
today: { sent: number; delivered: number; failed: number; unknown: number; successRate: number; spendCents: number; returnedCents: number; billingUnits: number };
|
||
uplinkCount: number;
|
||
billing: { _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } };
|
||
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null } };
|
||
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
|
||
pendingAuditCount: number;
|
||
pendingAudits: { enterpriseCertifications: number; smsAudits: number; templates: number; signatures: number; drainageInfos: number; total: number };
|
||
downstreamDeliverySummary?: {
|
||
pending: number;
|
||
failed: number;
|
||
delivered: number;
|
||
stalledPending: number;
|
||
stalledAck: number;
|
||
recentFailed: number;
|
||
alertCount: number;
|
||
};
|
||
accounts: Array<{ id: string; tenantId: string; balanceCents: number; creditCents: number; status: string; tenant?: TenantOption }>;
|
||
recentTasks: Array<Record<string, unknown>>;
|
||
recentRecharges: Array<RechargeOrder>;
|
||
};
|
||
|
||
export type RechargeOrder = {
|
||
id: string;
|
||
tenantId: string;
|
||
orderNo: string;
|
||
amountCents: number;
|
||
status: string;
|
||
payMethod?: string | null;
|
||
paidAt?: string | null;
|
||
operatorId?: string | null;
|
||
remark?: string | null;
|
||
balanceAfterCents?: number | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type ClientSmsApplication = {
|
||
id: string;
|
||
tenantId: string;
|
||
name: string;
|
||
scene?: string | null;
|
||
customerUnitPrice?: number | null;
|
||
queuePriority?: 'normal' | 'priority' | string | null;
|
||
status: string;
|
||
dailyLimit?: number | null;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
sentToday?: number;
|
||
deliveryRate?: number;
|
||
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||
interfaceEnabled?: boolean | null;
|
||
cmppConnections?: CmppDownstreamConnection[];
|
||
httpConfig?: HttpApiConfig | null;
|
||
};
|
||
|
||
export type ClientSmsSignature = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId?: string | null;
|
||
name: string;
|
||
purpose?: string | null;
|
||
drainageInfo?: Record<string, unknown> | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
materials?: Array<Record<string, unknown>>;
|
||
tenant?: TenantOption;
|
||
application?: ClientSmsApplication | null;
|
||
reportStatus?: string;
|
||
reportTasks?: Array<ReportTask & { channel?: AdminChannel }>;
|
||
reportTargets?: Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>;
|
||
carrierReportSummary?: Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>;
|
||
drainageReportTargets?: Record<string, Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>>;
|
||
drainageCarrierReportSummary?: Record<string, Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>>;
|
||
};
|
||
|
||
export type ClientSmsSignatureView = Pick<ClientSmsSignature,
|
||
'id' | 'tenantId' | 'applicationId' | 'name' | 'purpose' | 'auditStatus' | 'reportStatus' | 'rejectReason' | 'createdAt' | 'updatedAt' | 'materials'
|
||
> & {
|
||
pendingReport?: boolean;
|
||
reportChangedAt?: string;
|
||
application?: Pick<ClientSmsApplication, 'id' | 'name' | 'status'> | null;
|
||
submittedMaterialCount: number;
|
||
reportValues: Record<string, unknown>;
|
||
drainageInfo: { links: Array<{
|
||
id: string;
|
||
siteName: string;
|
||
url: string;
|
||
remark?: string | null;
|
||
reportValues: Record<string, unknown>;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
submittedAt: string;
|
||
reviewedAt?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
}> };
|
||
};
|
||
|
||
export type ClientSignatureWorkspace = {
|
||
items: ClientSmsSignatureView[];
|
||
summary: { total: number; pending: number; approved: number; rejected: number; draft: number };
|
||
};
|
||
|
||
export type SmsDrainageInfo = {
|
||
id: string;
|
||
tenantId: string;
|
||
signatureId: string;
|
||
applicationId?: string | null;
|
||
siteName: string;
|
||
url: string;
|
||
remark?: string | null;
|
||
reportValues?: Record<string, unknown> | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
submittedAt: string;
|
||
reviewedAt?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: TenantOption;
|
||
signature?: ClientSmsSignature;
|
||
application?: ClientSmsApplication | null;
|
||
reportTasks?: ReportTask[];
|
||
};
|
||
|
||
export type ClientSmsTemplate = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
signatureId?: string | null;
|
||
name: string;
|
||
content: string;
|
||
category?: string | null;
|
||
auditStatus: string;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
variables?: Array<{ name: string; example?: string | null; required?: boolean }>;
|
||
application?: { id: string; name: string };
|
||
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type SmsBatchTask = {
|
||
id: string;
|
||
tenantId: string;
|
||
taskNo: string;
|
||
sourceType?: string;
|
||
contentHash?: string | null;
|
||
windowStartedAt?: string | null;
|
||
windowEndsAt?: string | null;
|
||
applicationId?: string | null;
|
||
templateId?: string | null;
|
||
content: string;
|
||
category?: string | null;
|
||
phoneTotal: number;
|
||
status: string;
|
||
auditStatus?: string | null;
|
||
reviewReason?: string | null;
|
||
rejectReason?: string | null;
|
||
progressTotal: number;
|
||
progressSent?: number;
|
||
progressDelivered?: number;
|
||
progressFailed?: number;
|
||
submittedTotal?: number;
|
||
successTotal?: number;
|
||
failedTotal?: number;
|
||
unknownTotal?: number;
|
||
timeoutTotal?: number;
|
||
scheduledAt?: string | null;
|
||
canceledAt?: string | null;
|
||
createdAt: string;
|
||
tenant?: TenantOption;
|
||
application?: { id: string; name: string };
|
||
template?: { id: string; name: string; content: string; billingUnits?: number };
|
||
messages?: SmsMessageRecord[];
|
||
messageStats?: Array<{
|
||
batchTaskId?: string | null;
|
||
carrier?: string | null;
|
||
province?: string | null;
|
||
status: string;
|
||
_count: { _all: number };
|
||
_sum: { billingUnits?: number | null };
|
||
}>;
|
||
};
|
||
|
||
export type ImportPreviewResponse = {
|
||
fileName?: string;
|
||
encoding: string;
|
||
totalRows: number;
|
||
validCount: number;
|
||
errorCount: number;
|
||
phones: string[];
|
||
errors: Array<{ rowNumber: number; phoneNumber?: string; reason: string }>;
|
||
};
|
||
|
||
export type SmsMessageRecord = {
|
||
id: string;
|
||
tenantId: string;
|
||
batchTaskId?: string | null;
|
||
applicationId?: string | null;
|
||
channelId?: string | null;
|
||
messageId: string;
|
||
phoneNumber: string;
|
||
carrier?: string | null;
|
||
province?: string | null;
|
||
content: string;
|
||
clientSrcId?: string | null;
|
||
applicationExtension?: string | null;
|
||
billingUnits: number;
|
||
amountCents: number;
|
||
status: string;
|
||
errorMessage?: string | null;
|
||
errorCode?: string | null;
|
||
queuedAt: string;
|
||
submittedAt?: string | null;
|
||
deliveredAt?: string | null;
|
||
receiptStatus?: string | null;
|
||
submitStatus?: string | null;
|
||
channel?: AdminChannel | null;
|
||
tenant?: TenantOption | null;
|
||
application?: { id: string; name: string };
|
||
submitRecords?: SmsSubmitRecord[];
|
||
receiptRecords?: SmsReceiptRecord[];
|
||
};
|
||
|
||
export type SmsSubmitRecord = {
|
||
id: string;
|
||
channelId: string;
|
||
submitId: string;
|
||
sequenceId?: number | null;
|
||
gatewayMessageId?: string | null;
|
||
submitStatus: string;
|
||
errorCode?: string | null;
|
||
errorMessage?: string | null;
|
||
submittedAt?: string | null;
|
||
createdAt: string;
|
||
channel?: AdminChannel | null;
|
||
};
|
||
|
||
export type SmsReceiptRecord = {
|
||
id: string;
|
||
channelId?: string | null;
|
||
messageId: string;
|
||
gatewayMessageId: string;
|
||
sequenceId?: number | null;
|
||
receiptStatus: string;
|
||
rawStatus: string;
|
||
errorCode?: string | null;
|
||
deliveredAt: string;
|
||
createdAt: string;
|
||
channel?: AdminChannel | null;
|
||
};
|
||
|
||
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 HttpApiConfig = {
|
||
enabled: boolean;
|
||
sendEnabled: boolean;
|
||
messageQueryEnabled: boolean;
|
||
receiptWebhookEnabled: boolean;
|
||
uplinkWebhookEnabled: boolean;
|
||
uplinkQueryEnabled: boolean;
|
||
credentialSelfServiceEnabled: boolean;
|
||
qpsLimit: number;
|
||
timestampToleranceSeconds: number;
|
||
maxCredentialCount: number;
|
||
uplinkRetentionDays: number;
|
||
maxQueryRangeDays: number;
|
||
maxPageSize: number;
|
||
receiptDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
|
||
uplinkDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
|
||
webhookRetryEnabled: boolean;
|
||
webhookMaxAttempts: number;
|
||
webhookTimeoutSeconds: number;
|
||
requireHttps: boolean;
|
||
allowClientManualRetry: boolean;
|
||
allowClientTest: boolean;
|
||
};
|
||
|
||
export type HttpApiConfigResponse = { applicationId: string; applicationName?: string; config: HttpApiConfig | null; ipAllowlist: string[] };
|
||
export type HttpApiCredential = { id: string; name: string; accessKey: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; expiresAt?: string | null; lastUsedAt?: string | null; lastUsedIp?: string | null; createdAt: string };
|
||
export type HttpWebhookEndpoint = { id: string; eventType: 'receipt' | 'uplink'; url: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; lastTestAt?: string | null; lastTestStatus?: string | null; updatedAt: string };
|
||
export type HttpApiRequestLog = { id: string; requestId: string; clientMessageId?: string | null; sourceIp?: string | null; httpStatus?: number | null; businessCode?: string | null; status: string; durationMs?: number | null; createdAt: string; completedAt?: string | null };
|
||
export type HttpWebhookDelivery = { id: string; status: string; attemptCount: number; lastHttpStatus?: number | null; lastError?: string | null; createdAt: string; event: { eventId: string; eventType: string; messageId?: string | null }; endpoint: { eventType: string; url: string } };
|
||
|
||
export type DictionaryItem = Record<string, unknown> & {
|
||
id: string;
|
||
status?: string;
|
||
createdAt?: string;
|
||
updatedAt?: string;
|
||
};
|
||
|
||
export type ChannelGroup = DictionaryItem & {
|
||
code: string;
|
||
name: string;
|
||
carrier: 'mobile' | 'unicom' | 'telecom';
|
||
description?: string | null;
|
||
retryEnabled?: boolean;
|
||
retryTimeLimitHours?: number;
|
||
retryTimeLimitMinutes?: number;
|
||
items?: ChannelGroupItem[];
|
||
};
|
||
|
||
export type ChannelGroupItem = DictionaryItem & {
|
||
groupId: string;
|
||
channelId: string;
|
||
carrier?: 'mobile' | 'unicom' | 'telecom' | null;
|
||
province?: string | null;
|
||
priority: number;
|
||
weight?: number;
|
||
isBackup?: boolean;
|
||
channel?: AdminChannel;
|
||
};
|
||
|
||
export type ChannelReportField = DictionaryItem & {
|
||
channelId: string;
|
||
drainageFieldId?: string | null;
|
||
reportType?: 'signature' | 'drainage' | 'both';
|
||
code: string;
|
||
name: string;
|
||
fieldType: string;
|
||
required: boolean;
|
||
description?: string | null;
|
||
sortOrder?: number;
|
||
exportName?: string | null;
|
||
columnWidth?: number;
|
||
imageWidth?: number;
|
||
imageHeight?: number;
|
||
defaultValue?: string | null;
|
||
transform?: string | null;
|
||
drainageField?: DictionaryItem | null;
|
||
};
|
||
|
||
export type ReportMaterialPendingItem = {
|
||
id: string;
|
||
reportType: 'signature' | 'drainage';
|
||
signatureId: string;
|
||
drainageItemId?: string | null;
|
||
materialVersion: number;
|
||
changedAt: string;
|
||
name: string;
|
||
detail?: string | null;
|
||
signatureName?: string;
|
||
tenant?: TenantOption;
|
||
application?: ClientSmsApplication | null;
|
||
};
|
||
|
||
export type ReportImportMapping = {
|
||
sourceHeader: string;
|
||
sourceHeaderPath?: string;
|
||
sourceColumnIndex: number;
|
||
targetFieldCode: string;
|
||
targetKind: 'signatureName' | 'purpose' | 'siteName' | 'url' | 'remark' | 'dynamic';
|
||
fieldType: 'string' | 'image' | 'file';
|
||
required?: boolean;
|
||
transform?: string;
|
||
sortOrder?: number;
|
||
};
|
||
|
||
export type ReportImportProfile = {
|
||
id: string;
|
||
name: string;
|
||
reportType: 'signature' | 'drainage';
|
||
tenantId?: string | null;
|
||
applicationId?: string | null;
|
||
sheetName?: string | null;
|
||
headerRowCount: number;
|
||
dataStartRow: number;
|
||
columns: ReportImportMapping[];
|
||
};
|
||
|
||
export type ApplicationReportField = {
|
||
id: string;
|
||
code: string;
|
||
name: string;
|
||
fieldType: string;
|
||
required: boolean;
|
||
description?: string | null;
|
||
reportTypes: string[];
|
||
commonReportTypes?: Array<'signature' | 'drainage'>;
|
||
channels: Array<{ id: string; code: string; name: string; groupId: string; groupName: string; required: boolean; reportType: 'signature' | 'drainage' | 'both'; source?: 'common' | 'channel' | 'both' }>;
|
||
};
|
||
|
||
export type ClientApplicationReportField = Omit<ApplicationReportField, 'channels' | 'commonReportTypes'>;
|
||
|
||
export type CommonReportField = DictionaryItem & {
|
||
drainageFieldId: string;
|
||
reportType: 'signature' | 'drainage';
|
||
required: boolean;
|
||
sortOrder: number;
|
||
drainageField: DictionaryItem & { code?: string; name?: string; fieldType?: string; description?: string | null };
|
||
};
|
||
|
||
export type ReportTask = DictionaryItem & {
|
||
tenantId: string;
|
||
signatureId: string;
|
||
channelId: string;
|
||
reportType?: 'signature' | 'drainage';
|
||
drainageItemId?: string | null;
|
||
status: string;
|
||
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
|
||
drainageInfo?: SmsDrainageInfo | null;
|
||
channel?: { id: string; name: string; code: string };
|
||
};
|
||
|
||
export type ReportRecord = DictionaryItem & {
|
||
taskId: string;
|
||
channelId: string;
|
||
action: string;
|
||
statusBefore?: string | null;
|
||
statusAfter?: string | null;
|
||
reason?: string | null;
|
||
sourceEntry?: 'system' | 'legacy' | 'enterprise_signature' | 'report_task' | 'channel_report';
|
||
channel?: AdminChannel;
|
||
task?: ReportTask;
|
||
};
|
||
|
||
export type FileObject = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
bucket: string;
|
||
objectKey: string;
|
||
fileName: string;
|
||
contentType: string;
|
||
sizeBytes: string | number;
|
||
purpose: string;
|
||
createdAt: string;
|
||
};
|
||
|
||
export type FileRef = {
|
||
fileObjectId: string;
|
||
fileName: string;
|
||
contentType?: string;
|
||
};
|
||
|
||
export function fileDownloadUrl(fileObjectId: string, disposition: 'attachment' | 'inline' = 'attachment') {
|
||
return `/api/admin/files/${encodeURIComponent(fileObjectId)}/download?disposition=${disposition}`;
|
||
}
|
||
|
||
export type RiskReviewTask = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId?: string | null;
|
||
templateId?: string | null;
|
||
taskNo: string;
|
||
sourceType?: string;
|
||
contentHash?: string | null;
|
||
windowStartedAt?: string | null;
|
||
windowEndsAt?: string | null;
|
||
content: string;
|
||
category?: string | null;
|
||
phoneTotal: number;
|
||
uniquePhoneTotal: number;
|
||
duplicateRatio: number;
|
||
illegalRatio: number;
|
||
blacklistHitRatio: number;
|
||
variableIssues?: unknown;
|
||
status: string;
|
||
riskDecision: string;
|
||
reviewReason?: string | null;
|
||
rejectReason?: string | null;
|
||
createdAt: string;
|
||
reviewedAt?: string | null;
|
||
reviewedBy?: { id: string; username: string; displayName: string } | null;
|
||
riskHits?: Array<{ id: string; ruleName: string; reason: string }>;
|
||
_count?: { messageRecords: number };
|
||
};
|
||
|
||
export type TenantAccount = {
|
||
id: string;
|
||
tenantId: string;
|
||
balanceCents: number;
|
||
creditCents: number;
|
||
status: string;
|
||
tenant?: TenantOption;
|
||
};
|
||
|
||
export type OperationLogItem = {
|
||
id: string;
|
||
time: string;
|
||
level: 'info' | 'success' | 'warning' | 'error';
|
||
tenant: string;
|
||
module: string;
|
||
operator: string;
|
||
action: string;
|
||
resourceId: string;
|
||
detail: Record<string, unknown>;
|
||
ip: string;
|
||
userAgent: string;
|
||
};
|
||
|
||
export type OperationLogResponse = {
|
||
items: OperationLogItem[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
modules: string[];
|
||
};
|
||
|
||
export type PagedResponse<T> = {
|
||
items: T[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
};
|
||
|
||
export type DailyReconciliationReport = {
|
||
id: string;
|
||
reportDate: string;
|
||
tenantId: string;
|
||
tenantName: string;
|
||
applicationId: string;
|
||
applicationName: string;
|
||
sentUnits: number;
|
||
successUnits: number;
|
||
failedUnits: number;
|
||
generatedAt: string;
|
||
updatedAt: string;
|
||
};
|
||
|
||
export type DailyProfitReport = {
|
||
id: string;
|
||
reportDate: string;
|
||
dimensionType: 'application' | 'channel';
|
||
dimensionId: string;
|
||
dimensionName: string;
|
||
tenantId?: string | null;
|
||
tenantName?: string | null;
|
||
applicationId?: string | null;
|
||
channelId?: string | null;
|
||
sentUnits: number;
|
||
successUnits: number;
|
||
failedUnits: number;
|
||
revenueCents: number;
|
||
refundCents: number;
|
||
costCents: number;
|
||
profitCents: number;
|
||
profitRateBps: number;
|
||
generatedAt: string;
|
||
updatedAt: string;
|
||
};
|
||
|
||
export type DailyQualityReport = {
|
||
id: string;
|
||
reportDate: string;
|
||
dimensionType: 'application' | 'channel' | 'signature' | 'drainage';
|
||
dimensionId: string;
|
||
dimensionName: string;
|
||
tenantId?: string | null;
|
||
tenantName?: string | null;
|
||
applicationId?: string | null;
|
||
channelId?: string | null;
|
||
signatureId?: string | null;
|
||
drainageInfoId?: string | null;
|
||
sentUnits: number;
|
||
successUnits: number;
|
||
failedUnits: number;
|
||
successRateBps: number;
|
||
avgArrivalMs?: number | null;
|
||
generatedAt: string;
|
||
updatedAt: string;
|
||
};
|
||
|
||
export type CursorPage<T> = {
|
||
items: T[];
|
||
pageSize: number;
|
||
hasMore: boolean;
|
||
nextCursor: string | null;
|
||
};
|
||
|
||
export type EnterpriseApplication = {
|
||
id: string;
|
||
tenantId: string;
|
||
name: string;
|
||
scene?: string | null;
|
||
status: string;
|
||
dailyLimit?: number | null;
|
||
customerUnitPrice?: number | null;
|
||
queuePriority?: 'normal' | 'priority' | string | null;
|
||
maxPhonesPerTask?: number | null;
|
||
templateMismatchMode?: string | null;
|
||
downstreamReceiptRetryEnabled?: boolean | null;
|
||
downstreamUplinkRetryEnabled?: boolean | null;
|
||
cmppAccount?: string | null;
|
||
cmppEnterpriseCode?: string | null;
|
||
cmppApplicationExtension?: string | null;
|
||
cmppAccessNumberFillEnabled?: boolean | null;
|
||
cmppAccessNumberFillPrefix?: string | null;
|
||
cmppClientSrcId?: string | null;
|
||
interfaceEnabled?: boolean | null;
|
||
interfaceType?: 'cmpp20' | string | null;
|
||
cmppMaxConnections?: number | null;
|
||
cmppWindowSize?: number | null;
|
||
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
|
||
httpConfig?: HttpApiConfig | null;
|
||
tenant?: TenantOption;
|
||
sentToday?: number;
|
||
deliveryRate?: number;
|
||
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
|
||
cmppConnections?: CmppDownstreamConnection[];
|
||
};
|
||
|
||
export type CmppDownstreamConnection = {
|
||
id: string;
|
||
tenantId: string;
|
||
applicationId: string;
|
||
account: string;
|
||
enterpriseCode: string;
|
||
connectionId: string;
|
||
remoteIp?: string | null;
|
||
protocol?: string | null;
|
||
status: string;
|
||
connectedAt: string;
|
||
lastHeartbeatAt?: string | null;
|
||
lastSubmitAt?: string | null;
|
||
lastDeliverAt?: string | null;
|
||
disconnectedAt?: string | null;
|
||
lastError?: string | null;
|
||
updatedAt: string;
|
||
};
|
||
|
||
export type CmppConnectionState = {
|
||
id: string;
|
||
tenantId?: string | null;
|
||
applicationId?: string | null;
|
||
channelId: string;
|
||
connectionId: string;
|
||
status: string;
|
||
desiredConnections: number;
|
||
currentConnections: number;
|
||
lastConnectedAt?: string | null;
|
||
lastDisconnectedAt?: string | null;
|
||
lastHeartbeatAt?: string | null;
|
||
reconnectCount: number;
|
||
lastError?: string | null;
|
||
updatedAt: string;
|
||
channel?: AdminChannel;
|
||
};
|
||
|
||
export type ApplicationConnectionsResponse = {
|
||
application: EnterpriseApplication;
|
||
connections: CmppDownstreamConnection[];
|
||
summary: { desiredConnections: number; currentConnections: number; status: string };
|
||
};
|
||
|
||
export type ApplicationCmppParams = {
|
||
applicationId: string;
|
||
applicationName: string;
|
||
tenantName: string;
|
||
appCode: string;
|
||
gatewayHost: string;
|
||
gatewayPort: number;
|
||
enterpriseCode: string;
|
||
account: string;
|
||
passwordCipher: string;
|
||
srcId: string;
|
||
applicationExtension?: string | null;
|
||
accessNumberFillEnabled?: boolean;
|
||
accessNumberFillPrefix?: string | null;
|
||
interfaceEnabled?: boolean;
|
||
interfaceType?: string;
|
||
maxConnections: number;
|
||
heartbeatSeconds: number;
|
||
windowSize: number;
|
||
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;
|
||
manualRetryCount: number;
|
||
lastRetriedAt?: string | null;
|
||
retryEnabled: boolean;
|
||
nextRetryAt?: string | null;
|
||
sentAt?: string | null;
|
||
acknowledgedAt?: string | null;
|
||
ackDeadlineAt?: string | null;
|
||
ackResult?: number | null;
|
||
ackSequenceId?: string | null;
|
||
ackMessageId?: string | null;
|
||
connectionId?: 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;
|
||
awaitingAck: number;
|
||
delivered: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: number;
|
||
stalledPending: number;
|
||
stalledAck: number;
|
||
recentFailed: number;
|
||
alertCount: number;
|
||
};
|
||
typeBreakdown: Array<{
|
||
deliveryType: string;
|
||
total: number;
|
||
pending: number;
|
||
awaitingAck: number;
|
||
delivered: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: number;
|
||
}>;
|
||
retryBuckets: Array<{
|
||
label: string;
|
||
count: number;
|
||
}>;
|
||
topApplications: Array<{
|
||
applicationId: string;
|
||
name: string;
|
||
pending: number;
|
||
awaitingAck: number;
|
||
failed: number;
|
||
unconfirmed: number;
|
||
rejected: 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 GatewaySubmitException = {
|
||
id: string;
|
||
streamMessageId: string;
|
||
tenantId?: string | null;
|
||
applicationId?: string | null;
|
||
channelId?: string | null;
|
||
traceId?: string | null;
|
||
messageId?: string | null;
|
||
submitId?: string | null;
|
||
status: 'pending' | 'requeueing' | 'requeued' | 'resolved' | string;
|
||
failureCode: string;
|
||
failureMessage: string;
|
||
attempts: number;
|
||
maxAttempts: number;
|
||
commandPayload?: Record<string, unknown> | null;
|
||
rawPayloadAvailable?: boolean;
|
||
messageState?: {
|
||
status: string;
|
||
submitStatus?: string | null;
|
||
receiptStatus?: string | null;
|
||
phoneNumber: string;
|
||
content: string;
|
||
} | null;
|
||
manualRetryCount: number;
|
||
lastRetryStreamId?: string | null;
|
||
lastRetriedAt?: string | null;
|
||
resolvedAt?: string | null;
|
||
resolvedStatus?: string | null;
|
||
createdAt: string;
|
||
updatedAt: string;
|
||
tenant?: Pick<TenantOption, 'id' | 'name' | 'code' | 'status'> | null;
|
||
application?: Pick<EnterpriseApplication, 'id' | 'tenantId' | 'name' | 'status'> | null;
|
||
channel?: Pick<AdminChannel, 'id' | 'code' | 'name' | 'status' | 'carrier' | 'sendRegion' | 'rateLimitPerSecond'> | null;
|
||
};
|
||
|
||
export type GatewaySubmitExceptionResponse = PagedResponse<GatewaySubmitException> & {
|
||
summary: {
|
||
pending: number;
|
||
requeueing: number;
|
||
requeued: number;
|
||
resolved: number;
|
||
oldestPendingAt?: string | 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]) => {
|
||
if (value !== undefined && value !== '' && value !== 'all') {
|
||
params.set(key, String(value));
|
||
}
|
||
});
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return `${path}${suffix}`;
|
||
}
|
||
|
||
export const adminApi = {
|
||
getCaptcha: () => request<CaptchaResponse>('/admin/auth/captcha'),
|
||
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
|
||
request<LoginSession>('/admin/auth/login', { method: 'POST', body: JSON.stringify(body) }),
|
||
touchSession: () => request<Pick<LoginSession, 'idleTimeoutSeconds' | 'lockRecoverySeconds' | 'absoluteExpiresAt' | 'lastActivityAt' | 'recentAuthenticationExpiresAt'>>('/auth/session/touch', { method: 'POST', body: '{}' }),
|
||
lockSession: () => request<{ locked: boolean }>('/auth/session/lock', { method: 'POST', body: '{}' }),
|
||
unlockSession: (password: string) => request<Pick<LoginSession, 'idleTimeoutSeconds' | 'lockRecoverySeconds' | 'absoluteExpiresAt' | 'lastActivityAt' | 'recentAuthenticationExpiresAt'>>('/auth/session/unlock', { method: 'POST', body: JSON.stringify({ password }) }),
|
||
reauthenticate: (password: string) => request<Pick<LoginSession, 'recentAuthenticationExpiresAt'>>('/auth/reauthenticate', { method: 'POST', body: JSON.stringify({ password }), reauthenticationAttempted: true }),
|
||
logout: () => request<{ success: boolean }>('/auth/logout', { method: 'POST', body: '{}' }),
|
||
changeOwnPassword: (body: { currentPassword: string; password: string }) =>
|
||
request<ManagedUser>('/auth/password', { method: 'POST', body: JSON.stringify(body) }),
|
||
listTenants: () => request<TenantOption[]>('/admin/tenants'),
|
||
listTenantManagementRows: () => request<TenantManagementRow[]>('/admin/tenants/management-list'),
|
||
getTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`),
|
||
createTenant: (body: { name: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
|
||
request<TenantOption>('/admin/tenants', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateTenant: (id: string, body: { name?: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
|
||
request<TenantOption>(`/admin/tenants/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeTenantStatus: (id: string, status: string) =>
|
||
request<TenantOption>(`/admin/tenants/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) }),
|
||
deleteTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`, { method: 'DELETE' }),
|
||
listUsers: (query: { tenantId?: string; roleCode?: string } = {}) => request<ManagedUser[]>(withQuery('/admin/users', query)),
|
||
createUser: (body: UserPayload) => request<ManagedUser>('/admin/users', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateUser: (id: string, body: Omit<UserPayload, 'password'>) => request<ManagedUser>(`/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeUserStatus: (id: string, status: string, operatorId?: string) =>
|
||
request<ManagedUser>(`/admin/users/${id}/status`, { method: 'POST', body: JSON.stringify({ status, operatorId }) }),
|
||
deleteUser: (id: string, operatorId?: string) => request<ManagedUser>(`/admin/users/${id}`, { method: 'DELETE', body: JSON.stringify({ operatorId }) }),
|
||
changeUserPassword: (id: string, password: string, operatorId?: string) =>
|
||
request<ManagedUser>(`/admin/users/${id}/password`, { method: 'POST', body: JSON.stringify({ password, operatorId }) }),
|
||
getDashboard: (tenantId?: string) => request<DashboardResponse>(withQuery('/admin/operations/dashboard/statistics', { tenantId })),
|
||
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }) =>
|
||
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
|
||
listAccounts: () => request<TenantAccount[]>('/admin/billing/accounts'),
|
||
updateCreditLimit: (tenantId: string, body: { creditCents: number; operatorId?: string; remark?: string }) =>
|
||
request<TenantAccount>(`/admin/billing/accounts/${tenantId}/credit-limit`, { method: 'POST', body: JSON.stringify(body) }),
|
||
listManualRecharges: (tenantId?: string) => request<RechargeOrder[]>(withQuery('/admin/billing/manual-recharges', { tenantId })),
|
||
createManualRecharge: (body: { tenantId: string; amountCents: number; operatorId?: string; remark?: string }) =>
|
||
request<RechargeOrder>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
|
||
listEnterpriseApplications: (query: { tenantId?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; status?: string } = {}) =>
|
||
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; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; cmppApplicationExtension?: string; cmppAccessNumberFillEnabled?: boolean; cmppAccessNumberFillPrefix?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; 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; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; cmppApplicationExtension?: string; cmppAccessNumberFillEnabled?: boolean; cmppAccessNumberFillPrefix?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; 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`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ status, reason }),
|
||
}),
|
||
listApplicationConnections: (applicationId: string) =>
|
||
request<ApplicationConnectionsResponse>(`/admin/enterprise-applications/${applicationId}/connections`),
|
||
listApplicationReportFields: (applicationId: string, reportType?: 'signature' | 'drainage') =>
|
||
request<ApplicationReportField[]>(withQuery(`/admin/enterprise-applications/${applicationId}/report-fields`, { reportType })),
|
||
listCommonApplicationReportFields: (reportType: 'signature' | 'drainage') =>
|
||
request<ApplicationReportField[]>(withQuery('/admin/report-fields/common', { reportType })),
|
||
getApplicationCmppParams: (applicationId: string) =>
|
||
request<ApplicationCmppParams>(`/admin/enterprise-applications/${applicationId}/cmpp-params`),
|
||
getApplicationHttpApiConfig: (applicationId: string) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`),
|
||
updateApplicationHttpApiConfig: (applicationId: string, body: Partial<HttpApiConfig> & { ipAllowlist?: string[] }) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
listChannels: () => request<AdminChannel[]>('/admin/channels'),
|
||
listReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<PagedResponse<DailyReconciliationReport>>(withQuery('/admin/reports/reconciliation', query)),
|
||
exportReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string } = {}) =>
|
||
requestBlob(withQuery('/admin/reports/reconciliation/export', query)),
|
||
listProfitReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel'; tenantId?: string; applicationId?: string; channelId?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<PagedResponse<DailyProfitReport> & { dimensionType: 'application' | 'channel' }>(withQuery('/admin/reports/profit', query)),
|
||
exportProfitReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel'; tenantId?: string; applicationId?: string; channelId?: string } = {}) =>
|
||
requestBlob(withQuery('/admin/reports/profit/export', query)),
|
||
listQualityReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel' | 'signature' | 'drainage'; tenantId?: string; applicationId?: string; channelId?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<PagedResponse<DailyQualityReport> & { dimensionType: DailyQualityReport['dimensionType'] }>(withQuery('/admin/reports/quality', query)),
|
||
exportQualityReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel' | 'signature' | 'drainage'; tenantId?: string; applicationId?: string; channelId?: string } = {}) =>
|
||
requestBlob(withQuery('/admin/reports/quality/export', query)),
|
||
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; 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',
|
||
body: JSON.stringify(body),
|
||
}),
|
||
testChannel: (id: string, body: { phoneNumber?: string; phones?: string[] | string; content: string; accessNo?: string }) =>
|
||
request<ChannelTestResponse>(`/admin/channels/${id}/test`, { method: 'POST', body: JSON.stringify(body) }),
|
||
changeChannelStatus: (id: string, status: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}/status`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ status, reason }),
|
||
}),
|
||
deleteChannel: (id: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}`, {
|
||
method: 'DELETE',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listChannelConnectionLogs: (id: string) => request<ChannelConnectionLogResponse>(`/admin/channels/${id}/connection-logs`),
|
||
listTemplateAudits: (query: { keyword?: string; status?: string }) => {
|
||
const params = new URLSearchParams();
|
||
if (query.keyword) params.set('keyword', query.keyword);
|
||
if (query.status && query.status !== 'all') params.set('status', query.status);
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return request<SmsTemplateAudit[]>(`/admin/enterprise-templates${suffix}`);
|
||
},
|
||
approveTemplate: (id: string) => request<SmsTemplateAudit>(`/admin/templates/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectTemplate: (id: string, reason = '运营审核驳回') => request<SmsTemplateAudit>(`/admin/templates/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
approveSignature: (id: string) => request<ClientSmsSignature>(`/admin/signatures/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectSignature: (id: string, reason = '运营审核驳回') => request<ClientSmsSignature>(`/admin/signatures/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listEnterpriseSignatures: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string } = {}) =>
|
||
request<ClientSmsSignature[]>(withQuery('/admin/enterprise-signatures', query)),
|
||
createEnterpriseSignature: (body: { tenantId: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }) =>
|
||
request<ClientSmsSignature>('/admin/enterprise-signatures', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateEnterpriseSignature: (id: string, body: { applicationId?: string | null; name?: string; purpose?: string; auditStatus?: string; drainageInfo?: Record<string, unknown> }) =>
|
||
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeEnterpriseSignatureStatus: (id: string, status: string, reason?: string) =>
|
||
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listDrainageInfos: (query: { tenantId?: string; signatureId?: string; keyword?: string; status?: string } = {}) =>
|
||
request<SmsDrainageInfo[]>(withQuery('/admin/drainage-infos', query)),
|
||
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }) =>
|
||
request<SmsDrainageInfo>(`/admin/enterprise-signatures/${signatureId}/drainage-infos`, { method: 'POST', body: JSON.stringify(body) }),
|
||
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
approveDrainageInfo: (id: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
|
||
rejectDrainageInfo: (id: string, reason: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||
changeDrainageInfoStatus: (id: string, status: string, reason?: string) =>
|
||
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listEnterpriseTemplates: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; nameKeyword?: string; contentKeyword?: string } = {}) =>
|
||
request<ClientSmsTemplate[]>(withQuery('/admin/enterprise-templates', query)),
|
||
createEnterpriseTemplate: (body: { tenantId: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
|
||
request<ClientSmsTemplate>('/admin/enterprise-templates', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateEnterpriseTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
|
||
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
changeEnterpriseTemplateStatus: (id: string, status: string, reason?: string) =>
|
||
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
|
||
listEnterpriseCertifications: (query: { keyword?: string; status?: string }) => {
|
||
const params = new URLSearchParams();
|
||
if (query.keyword) params.set('keyword', query.keyword);
|
||
if (query.status && query.status !== 'all') params.set('status', query.status);
|
||
const suffix = params.toString() ? `?${params}` : '';
|
||
return request<EnterpriseCertification[]>(`/admin/enterprise-certifications${suffix}`);
|
||
},
|
||
getEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}`),
|
||
approveEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/approve`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({}),
|
||
}),
|
||
rejectEnterpriseCertification: (id: string, reason = '运营审核驳回') => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/reject`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ reason }),
|
||
}),
|
||
listChannelGroups: () => request<ChannelGroup[]>('/admin/channel-groups'),
|
||
createChannelGroup: (body: { code: string; name: string; carrier: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number }) =>
|
||
request<ChannelGroup>('/admin/channel-groups', { method: 'POST', body: JSON.stringify(body) }),
|
||
updateChannelGroup: (id: string, body: { code?: string; name?: string; carrier?: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number; items?: Array<Record<string, unknown>> }) =>
|
||
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
deleteChannelGroup: (id: string) =>
|
||
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'DELETE' }),
|
||
addChannelGroupItem: (body: Record<string, unknown>) =>
|
||
request<DictionaryItem>('/admin/channel-groups/items', { method: 'POST', body: JSON.stringify(body) }),
|
||
listChannelRouteRules: () => request<DictionaryItem[]>('/admin/channel-route-rules'),
|
||
createChannelRouteRule: (body: { tenantId?: string; applicationId: string; groupId: string; carrier: string; priority?: number; status?: string }) =>
|
||
request<DictionaryItem>('/admin/channel-route-rules', { method: 'POST', body: JSON.stringify(body) }),
|
||
listChannelConnections: (id: string) => request<CmppConnectionState[]>(`/admin/channels/${id}/connections`),
|
||
replaceApplicationRouteRules: (applicationId: string, body: { routes: Array<{ carrier: 'mobile' | 'unicom' | 'telecom'; groupId: string; priority?: number; status?: string }> }) =>
|
||
request<DictionaryItem[]>(`/admin/enterprise-applications/${applicationId}/route-rules`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
listChannelReportFields: (channelId?: string) => request<ChannelReportField[]>(withQuery('/admin/channel-report-fields', { channelId })),
|
||
createChannelReportField: (body: Record<string, unknown>) =>
|
||
request<ChannelReportField>('/admin/channel-report-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||
replaceChannelReportFields: (channelId: string, reportType: 'signature' | 'drainage', fields: Array<Record<string, unknown>>) =>
|
||
request<ChannelReportField[]>(`/admin/channels/${channelId}/report-fields/${reportType}`, { method: 'PUT', body: JSON.stringify({ fields }) }),
|
||
listPendingReportMaterials: (query: { reportType?: 'signature' | 'drainage'; tenantId?: string; applicationId?: string } = {}) =>
|
||
request<ReportMaterialPendingItem[]>(withQuery('/admin/report-materials/pending', query)),
|
||
listReportImportProfiles: (reportType?: 'signature' | 'drainage') =>
|
||
request<ReportImportProfile[]>(withQuery('/admin/report-materials/import-profiles', { reportType })),
|
||
saveReportImportProfile: (body: Omit<ReportImportProfile, 'id'> & { id?: string }) =>
|
||
request<ReportImportProfile>('/admin/report-materials/import-profiles', { method: 'POST', body: JSON.stringify(body) }),
|
||
analyzeReportMaterialImport: (file: File, body: { tenantId: string; applicationId?: string; reportType: 'signature' | 'drainage'; sheetName?: string; headerRowCount?: number; dataStartRow?: number; profileId?: string }) => {
|
||
assertUploadFileSize(file);
|
||
const form = new FormData();
|
||
form.set('file', file);
|
||
Object.entries(body).forEach(([key, value]) => { if (value !== undefined) form.set(key, String(value)); });
|
||
return requestForm<Record<string, unknown> & { id: string; columns: Array<{ sourceColumnIndex: number; columnLetter: string; sourceHeader: string; sourceHeaderPath: string; imageCount: number }>; rows: Array<Record<string, unknown>>; suggestedMappings: ReportImportMapping[] }>('/admin/report-materials/imports/analyze', form);
|
||
},
|
||
commitReportMaterialImport: (id: string, body: { mappings: ReportImportMapping[]; profile?: Omit<ReportImportProfile, 'id'> & { id?: string } }) =>
|
||
request<Record<string, unknown>>(`/admin/report-materials/imports/${id}/commit`, { method: 'PUT', body: JSON.stringify(body) }),
|
||
listReportMaterialBatches: () => request<Array<Record<string, unknown>>>('/admin/report-materials/batches'),
|
||
createReportMaterialBatch: (body: { createdById?: string; items: Array<{ reportType: 'signature' | 'drainage'; signatureId: string; drainageItemId?: string }> }) =>
|
||
request<Record<string, unknown>>('/admin/report-materials/batches', { method: 'POST', body: JSON.stringify(body) }),
|
||
listReportTasks: (query: { tenantId?: string; status?: string; channelId?: string; reportType?: 'signature' | 'drainage' } = {}) => request<ReportTask[]>(withQuery('/admin/report-tasks', query)),
|
||
createReportTask: (body: { tenantId: string; signatureId: string; channelId: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string; createdById?: string }) =>
|
||
request<ReportTask>('/admin/report-tasks/generate', { method: 'POST', body: JSON.stringify(body) }),
|
||
changeReportTaskStatuses: (body: { items: Array<{ signatureId: string; channelId: string; status: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string }>; reason?: string; operatorId?: string; sourceEntry?: 'enterprise_signature' | 'report_task' | 'channel_report' }) =>
|
||
request<Array<{ signatureId: string; reportStatus: string; carrierReportSummary: Record<string, { status: string; approved: number; total: number }> }>>('/admin/report-tasks/status-change', { method: 'POST', body: JSON.stringify(body) }),
|
||
createReportExport: (id: string, body: { fileObjectId?: string; fileName: string; rowCount?: number }) =>
|
||
request<Record<string, unknown>>(`/admin/report-tasks/${id}/export`, { method: 'POST', body: JSON.stringify(body) }),
|
||
importReportReceipt: (id: string, body: { fileObjectId?: string; fileName: string; fileContent?: string; delimiter?: ',' | '\t'; rowCount?: number; successCount?: number; failedCount?: number; statusAfter?: string; reason?: string; result?: Record<string, unknown> }) =>
|
||
request<Record<string, unknown>>(`/admin/report-tasks/${id}/receipt-import`, { method: 'POST', body: JSON.stringify(body) }),
|
||
listReportRecords: (query: { taskId?: string; channelId?: string } = {}) => request<ReportRecord[]>(withQuery('/admin/report-records', query)),
|
||
listAdminBatchTasks: (query: { tenantId?: string; status?: string } = {}) =>
|
||
request<SmsBatchTask[]>(withQuery('/admin/send/batch-tasks', query)),
|
||
terminateAdminBatchTask: (id: string) =>
|
||
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; channelKeyword?: string; taskId?: string; messageId?: string; phoneNumber?: string; contentKeyword?: string; status?: string; queuedAtFrom?: string; queuedAtTo?: 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)),
|
||
listGatewaySubmitExceptions: (query: { tenantId?: string; applicationId?: string; channelId?: string; status?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<GatewaySubmitExceptionResponse>(withQuery('/admin/operations/gateway-submit-dead-letters', query)),
|
||
requeueGatewaySubmitException: (id: string, body: { confirmedNotSubmitted: boolean; reason: string }) =>
|
||
request<GatewaySubmitException>(`/admin/operations/gateway-submit-dead-letters/${id}/requeue`, { method: 'POST', body: JSON.stringify(body) }),
|
||
listStatistics: (query: { tenantId?: string; groupBy?: string } = {}) => request<Array<Record<string, unknown>>>(withQuery('/admin/operations/statistics', query)),
|
||
getDownstreamDeliveryDashboard: (query: { tenantId?: string; applicationId?: string; deliveryType?: string; createdAtFrom?: string; createdAtTo?: 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; createdAtFrom?: string; createdAtTo?: string } = {}) =>
|
||
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 }) }),
|
||
rejectRiskReviewTask: (id: string, reason?: string) =>
|
||
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
|
||
rejectRiskReviewTasks: (ids: string[], reason: string) =>
|
||
request<RiskReviewTask[]>('/admin/risk-review/tasks/batch/reject', { method: 'POST', body: JSON.stringify({ ids, reason }) }),
|
||
listSensitiveWords: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/sensitive-words', query)),
|
||
createSensitiveWord: (body: { word: string; level?: string; status?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/sensitive-words', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteSensitiveWord: (id: string) => request<DictionaryItem>(`/admin/dictionaries/sensitive-words/${id}`, { method: 'DELETE' }),
|
||
listGlobalBlacklist: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/global', query)),
|
||
createGlobalBlacklist: (body: { phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/blacklists/global', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteGlobalBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/global/${id}`, { method: 'DELETE' }),
|
||
listEnterpriseBlacklist: (query: { tenantId?: string; applicationId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; phoneNumber?: string; reasonKeyword?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/enterprise', query)),
|
||
createEnterpriseBlacklist: (body: { tenantId: string; applicationId: string; phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/blacklists/enterprise', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteEnterpriseBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/enterprise/${id}`, { method: 'DELETE' }),
|
||
listPhoneSegments: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-segments', query)),
|
||
createPhoneSegment: (body: { prefix: string; carrier: string; province?: string; city?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/phone-segments', { method: 'POST', body: JSON.stringify(body) }),
|
||
deletePhoneSegment: (id: string) => request<DictionaryItem>(`/admin/dictionaries/phone-segments/${id}`, { method: 'DELETE' }),
|
||
listPhoneCarrierRules: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-carrier-rules', query)),
|
||
createPhoneCarrierRule: (body: { carrier: string; pattern: string; priority?: number; status?: string; remark?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/phone-carrier-rules', { method: 'POST', body: JSON.stringify(body) }),
|
||
deletePhoneCarrierRule: (id: string) => request<DictionaryItem>(`/admin/dictionaries/phone-carrier-rules/${id}`, { method: 'DELETE' }),
|
||
listDrainageFields: () => request<DictionaryItem[]>('/admin/dictionaries/drainage-fields'),
|
||
createDrainageField: (body: { code: string; name: string; fieldType: 'string' | 'image' | 'file'; required?: boolean; status?: string; description?: string }) =>
|
||
request<DictionaryItem>('/admin/dictionaries/drainage-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteDrainageField: (id: string) => request<DictionaryItem>(`/admin/dictionaries/drainage-fields/${id}`, { method: 'DELETE' }),
|
||
listCommonReportFields: () => request<CommonReportField[]>('/admin/dictionaries/common-report-fields'),
|
||
createCommonReportField: (body: { drainageFieldId: string; reportType: 'signature' | 'drainage'; required: boolean; sortOrder?: number }) =>
|
||
request<CommonReportField>('/admin/dictionaries/common-report-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||
deleteCommonReportField: (id: string) => request<CommonReportField>(`/admin/dictionaries/common-report-fields/${id}`, { method: 'DELETE' }),
|
||
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId?: string) => {
|
||
assertUploadFileSize(file);
|
||
const form = new FormData();
|
||
form.set('file', file);
|
||
form.set('purpose', body.purpose);
|
||
if (body.prefix) {
|
||
form.set('prefix', body.prefix);
|
||
}
|
||
const headers = new Headers();
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch('/api/admin/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const error = await readErrorBody(response.clone());
|
||
if (error.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: error.message });
|
||
} else {
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: error.code, message: error.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await response.text());
|
||
}
|
||
return response.json() as Promise<FileObject>;
|
||
},
|
||
};
|
||
|
||
export const clientApi = {
|
||
getCaptcha: () => request<CaptchaResponse>('/client/auth/captcha'),
|
||
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
|
||
request<LoginSession>('/client/auth/login', { method: 'POST', body: JSON.stringify(body) }),
|
||
listUsers: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser[]>('/client/users', { tenantId }),
|
||
createUser: (body: UserPayload, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>('/client/users', { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
updateUser: (id: string, body: Omit<UserPayload, 'password'>, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
changeUserStatus: (id: string, status: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status, operatorId }) }),
|
||
deleteUser: (id: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}`, { method: 'DELETE', tenantId, body: JSON.stringify({ operatorId }) }),
|
||
changeUserPassword: (id: string, password: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ManagedUser>(`/client/users/${id}/password`, { method: 'POST', tenantId, body: JSON.stringify({ password, operatorId }) }),
|
||
getDashboard: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<DashboardResponse>('/client/operations/dashboard', { tenantId }),
|
||
listEnterpriseCertifications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<EnterpriseCertification[]>('/client/enterprise-certification', { tenantId }),
|
||
submitEnterpriseCertification: (body: { companyName: string; licenseNo?: string; contactName?: string; contactPhone?: string; materials?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<EnterpriseCertification>('/client/enterprise-certification', {
|
||
method: 'POST',
|
||
tenantId,
|
||
body: JSON.stringify({ ...body, tenantId }),
|
||
}),
|
||
listSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<OperationLogResponse>(withQuery('/client/operations/system-logs', query), { tenantId }),
|
||
listOrders: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<RechargeOrder[]>('/client/billing/orders', { tenantId }),
|
||
listApplications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsApplication[]>('/client/applications', { tenantId }),
|
||
getApplicationCmppParams: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ApplicationCmppParams>(`/client/applications/${applicationId}/cmpp-params`, { tenantId }),
|
||
getApplicationHttpApiConfig: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiConfigResponse>(`/client/applications/${applicationId}/http-api`, { tenantId }),
|
||
listHttpApiCredentials: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential[]>(`/client/applications/${applicationId}/http-api/credentials`, { tenantId }),
|
||
createHttpApiCredential: (applicationId: string, body: { name?: string; expiresAt?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential>(`/client/applications/${applicationId}/http-api/credentials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
revokeHttpApiCredential: (applicationId: string, credentialId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/credentials/${credentialId}/revoke`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
listHttpWebhooks: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint[]>(`/client/applications/${applicationId}/http-api/webhooks`, { tenantId }),
|
||
saveHttpWebhook: (applicationId: string, eventType: 'receipt' | 'uplink', body: { url: string; rotateSecret?: boolean; status?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint>(`/client/applications/${applicationId}/http-api/webhooks/${eventType}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
listHttpApiRequests: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiRequestLog[]>(`/client/applications/${applicationId}/http-api/requests`, { tenantId }),
|
||
listHttpWebhookDeliveries: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookDelivery[]>(`/client/applications/${applicationId}/http-api/webhook-deliveries`, { tenantId }),
|
||
retryHttpWebhookDelivery: (applicationId: string, deliveryId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/webhook-deliveries/${deliveryId}/retry`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
listApplicationReportFields: (applicationId: string, reportType: 'signature' | 'drainage' = 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientApplicationReportField[]>(withQuery(`/client/applications/${applicationId}/report-fields`, { reportType }), { tenantId }),
|
||
listCommonApplicationReportFields: (reportType: 'signature' | 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientApplicationReportField[]>(withQuery('/client/report-fields/common', { reportType }), { tenantId }),
|
||
listSignatures: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignatureView[]>('/client/signatures', { tenantId }),
|
||
getSignatureWorkspace: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSignatureWorkspace>('/client/signatures-workspace', { tenantId }),
|
||
createSignature: (body: { tenantId?: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignatureView>('/client/signatures', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
updateSignature: (id: string, body: { applicationId?: string; name?: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignatureView>(`/client/signatures/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
submitSignature: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignatureView>(`/client/signatures/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
changeSignatureStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsSignatureView>(`/client/signatures/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
createSignatureMaterial: (id: string, body: { fileObjectId?: string; materialType: string; title: string; description?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<Record<string, unknown>>(`/client/signatures/${id}/materials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/signatures/${signatureId}/drainage-infos`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
|
||
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/drainage-infos/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
changeDrainageInfoStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsDrainageInfo>(`/client/drainage-infos/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
listTemplates: (query: { status?: string; keyword?: string; includeHistory?: boolean } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate[]>(withQuery('/client/templates', {
|
||
status: query.status,
|
||
keyword: query.keyword,
|
||
includeHistory: query.includeHistory === undefined ? undefined : String(query.includeHistory),
|
||
}), { tenantId }),
|
||
createTemplate: (body: { tenantId?: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>('/client/templates', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
updateTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
|
||
submitTemplate: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
changeTemplateStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ClientSmsTemplate>(`/client/templates/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
|
||
listBatchTasks: (query: { status?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask[]>(withQuery('/client/send/batch-tasks', query), { tenantId }),
|
||
cancelBatchTask: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>(`/client/send/batch-tasks/${id}/cancel`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
|
||
createBatchTask: (body: { applicationId?: string; templateId?: string; content: string; category?: string; phones: string[]; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>('/client/send/batch-tasks', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
previewImport: (body: { applicationId?: string; content: string; fileName?: string; delimiter?: ',' | '\t'; requiredVariables?: string[] }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<ImportPreviewResponse>('/client/send/imports/preview', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
confirmImport: (body: { applicationId?: string; templateId?: string; content: string; category?: string; importContent: string; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; requiredVariables?: string[]; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsBatchTask>('/client/send/imports/confirm', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
|
||
listBatchTaskMessages: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsMessageRecord[]>(`/client/send/batch-tasks/${id}/messages`, { tenantId }),
|
||
listMessages: (query: { applicationId?: string; taskId?: string; messageId?: string; phoneNumber?: string; status?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsMessageRecord[]>(withQuery('/client/operations/messages', query), { tenantId }),
|
||
listUplinkMessages: (query: { channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<SmsUplinkMessage[]>(withQuery('/client/operations/uplink-messages', query), { tenantId }),
|
||
createFileObject: (body: { bucket?: string; objectKey: string; fileName: string; contentType: string; sizeBytes: number; purpose: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
|
||
request<FileObject>('/admin/files', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId, bucket: body.bucket ?? 'cmpp-platform' }) }),
|
||
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => {
|
||
assertUploadFileSize(file);
|
||
const form = new FormData();
|
||
form.set('file', file);
|
||
form.set('purpose', body.purpose);
|
||
if (body.prefix) {
|
||
form.set('prefix', body.prefix);
|
||
}
|
||
const headers = new Headers();
|
||
const session = readSession();
|
||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||
if (tenantId) {
|
||
headers.set('x-tenant-id', tenantId);
|
||
}
|
||
const response = await fetch('/api/admin/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||
if (response.status === 401 && session) {
|
||
const error = await readErrorBody(response.clone());
|
||
if (error.code === 'SESSION_LOCKED') {
|
||
dispatchSessionEvent('locked', { message: error.message });
|
||
} else {
|
||
clearSession();
|
||
dispatchSessionEvent('logout', { code: error.code, message: error.message });
|
||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||
}
|
||
}
|
||
if (!response.ok) {
|
||
throw new Error(await response.text());
|
||
}
|
||
return response.json() as Promise<FileObject>;
|
||
},
|
||
};
|