592 lines
17 KiB
TypeScript
592 lines
17 KiB
TypeScript
async function adminFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const response = await fetch(path, {
|
|
...init,
|
|
credentials: 'include',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
...(init?.body ? { 'Content-Type': 'application/json' } : {}),
|
|
...(init?.headers ?? {}),
|
|
},
|
|
});
|
|
const payload = (await response.json().catch(() => ({}))) as {
|
|
data?: T;
|
|
message?: string;
|
|
error?: { code: string; message: string };
|
|
};
|
|
if (!response.ok) {
|
|
throw new Error(payload?.error?.message ?? payload?.message ?? `请求失败 (${response.status})`);
|
|
}
|
|
return (payload.data !== undefined ? payload.data : payload) as T;
|
|
}
|
|
|
|
export type AdminUser = {
|
|
id: string;
|
|
username: string;
|
|
slug: string;
|
|
email: string;
|
|
displayName: string;
|
|
role: string;
|
|
status: string;
|
|
balanceCents: number;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type AdminSummary = {
|
|
totalUsers: number;
|
|
activeUsers: number;
|
|
totalBalanceCents: number;
|
|
llm?: {
|
|
keyCount: number;
|
|
selectedKeyName: string | null;
|
|
globalModel: string | null;
|
|
};
|
|
};
|
|
|
|
export type LedgerEntry = {
|
|
id: string;
|
|
userId: string;
|
|
username?: string;
|
|
type: string;
|
|
amountCents: number;
|
|
note: string;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type UsageRecord = {
|
|
id: string;
|
|
userId: string;
|
|
username?: string;
|
|
provider: string;
|
|
model: string;
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
costCents: number;
|
|
createdAt: string;
|
|
};
|
|
|
|
export type WechatAdminSummary = {
|
|
config: {
|
|
mpEnabled: boolean;
|
|
scheduleEnabled: boolean;
|
|
reminderWorkerEnabled: boolean;
|
|
appId: string | null;
|
|
publicBaseUrl: string | null;
|
|
bindPath: string | null;
|
|
tokenEndpointConfigured: boolean;
|
|
customerServiceEndpointConfigured: boolean;
|
|
};
|
|
counts: {
|
|
boundUsers: number;
|
|
routes: { total: number; active: number };
|
|
recentMessages: Record<string, number>;
|
|
digests: Record<string, number>;
|
|
recentDeliveries: Record<string, number>;
|
|
};
|
|
};
|
|
|
|
export type WechatBinding = {
|
|
userId: string;
|
|
username: string;
|
|
displayName: string;
|
|
status: string;
|
|
appId: string | null;
|
|
openidMasked: string;
|
|
nickname: string | null;
|
|
avatarUrl: string | null;
|
|
lastLoginAt: number;
|
|
boundAt: number;
|
|
routeId: string | null;
|
|
routeStatus: string | null;
|
|
agentSessionId: string | null;
|
|
routeUpdatedAt: number | null;
|
|
};
|
|
|
|
export type WechatMessage = {
|
|
appId: string | null;
|
|
openidMasked: string;
|
|
msgId: string;
|
|
status: string;
|
|
agentSessionId: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
userId: string | null;
|
|
username: string | null;
|
|
displayName: string | null;
|
|
};
|
|
|
|
export type WechatDigestSubscription = {
|
|
id: string;
|
|
userId: string;
|
|
username: string;
|
|
displayName: string;
|
|
digestType: string;
|
|
hour: number;
|
|
minute: number;
|
|
timezone: string;
|
|
channel: string;
|
|
status: string;
|
|
nextRunAt: number;
|
|
lastRunAt: number | null;
|
|
attempts: number;
|
|
lastError: string | null;
|
|
sourceText: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
};
|
|
|
|
export type WechatDeliveryLog = {
|
|
id: string;
|
|
reminderId: string | null;
|
|
subscriptionId: string | null;
|
|
userId: string;
|
|
username: string;
|
|
displayName: string;
|
|
channel: string;
|
|
status: string;
|
|
providerMessageId: string | null;
|
|
errorCode: string | null;
|
|
errorMessage: string | null;
|
|
createdAt: number;
|
|
digestType: string | null;
|
|
};
|
|
|
|
export type WechatWebNotification = {
|
|
id: string;
|
|
userId: string;
|
|
username: string;
|
|
displayName: string;
|
|
channel: string;
|
|
notificationType: string;
|
|
title: string;
|
|
body: string;
|
|
status: string;
|
|
readAt: number | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
};
|
|
|
|
export type CreateWechatNotificationPayload = {
|
|
userId?: string;
|
|
userIds?: string[];
|
|
audience?: 'all';
|
|
allUsers?: boolean;
|
|
title: string;
|
|
body: string;
|
|
notificationType?: string;
|
|
channel?: 'web' | 'wechat';
|
|
channels?: Array<'web' | 'wechat'>;
|
|
};
|
|
|
|
export type SystemDisclosureMode = 'off' | 'shadow' | 'enforce';
|
|
|
|
export type SystemDisclosurePolicyConfig = {
|
|
enabled: boolean;
|
|
mode: SystemDisclosureMode;
|
|
refusalText: string;
|
|
productNames: string[];
|
|
selfReferences: string[];
|
|
categories: Record<string, boolean>;
|
|
};
|
|
|
|
export type SystemDisclosurePolicyState = {
|
|
config: SystemDisclosurePolicyConfig;
|
|
policyVersion: number;
|
|
updatedBy: string | null;
|
|
updatedAt: number | null;
|
|
source: string;
|
|
refreshedAt?: number | null;
|
|
lastRefreshError?: string | null;
|
|
};
|
|
|
|
export type OrchestratorMode = 'off' | 'shadow' | 'canary' | 'active';
|
|
|
|
export type OrchestratorConfig = {
|
|
mode: OrchestratorMode;
|
|
primaryEngine: string;
|
|
fallbackEngine: string;
|
|
serviceUrl: string;
|
|
requestTimeoutMs: number;
|
|
rolloutPercent: number;
|
|
userAllowlist: string[];
|
|
workflowAllowlist: string[];
|
|
fallbackToNative: boolean;
|
|
requireHealthy: boolean;
|
|
};
|
|
|
|
export type OrchestratorRuntime = {
|
|
killSwitch: boolean;
|
|
configured: boolean;
|
|
effective: boolean;
|
|
reason: string | null;
|
|
executesLangGraph: boolean;
|
|
shadowsLangGraph: boolean;
|
|
};
|
|
|
|
export type OrchestratorEngineDescriptor = {
|
|
id: string;
|
|
label: string;
|
|
kind: string;
|
|
configured: boolean;
|
|
capabilities: string[];
|
|
};
|
|
|
|
export type OrchestratorConfigState = {
|
|
config: OrchestratorConfig;
|
|
configVersion: number;
|
|
updatedBy: string | null;
|
|
updatedAt: number | null;
|
|
source: string;
|
|
runtime: OrchestratorRuntime;
|
|
engines: OrchestratorEngineDescriptor[];
|
|
};
|
|
|
|
export type OrchestratorServiceHealth = {
|
|
checkedAt: number;
|
|
ok: boolean;
|
|
status: 'healthy' | 'unhealthy' | 'unconfigured' | 'timeout' | 'unreachable' | 'fetch_unavailable';
|
|
latencyMs: number;
|
|
httpStatus: number | null;
|
|
details: {
|
|
service: string | null;
|
|
checkpoint: { kind?: string; durable?: boolean } | null;
|
|
execution: string | null;
|
|
} | null;
|
|
};
|
|
|
|
export type OrchestratorRuntimeState = OrchestratorConfigState & {
|
|
serviceHealth: OrchestratorServiceHealth;
|
|
};
|
|
|
|
export type OrchestratorShadowMetrics = {
|
|
observations: number;
|
|
successes: number;
|
|
failures: number;
|
|
successRate: number | null;
|
|
failureRate: number | null;
|
|
latencyP50Ms: number | null;
|
|
latencyP95Ms: number | null;
|
|
nativeSucceeded: number;
|
|
nativeFailed: number;
|
|
lastObservedAt: number | null;
|
|
sampled: boolean;
|
|
};
|
|
|
|
export type OrchestratorShadowRun = {
|
|
eventId: string;
|
|
runId: string;
|
|
requestId: string;
|
|
userId: string;
|
|
sessionId: string | null;
|
|
nativeStatus: string;
|
|
nativeAttempts: number;
|
|
shadowStatus: 'succeeded' | 'failed';
|
|
engine: string;
|
|
configVersion: number | null;
|
|
phase: string | null;
|
|
taskType: string | null;
|
|
synthetic: boolean;
|
|
executorAdapter: string | null;
|
|
latencyMs: number | null;
|
|
error: { code: string; message: string } | null;
|
|
observedAt: number;
|
|
nativeCompletedAt: number | null;
|
|
};
|
|
|
|
export type OrchestratorShadowRunList = {
|
|
generatedAt: number;
|
|
window: { hours: number; from: number };
|
|
metrics: OrchestratorShadowMetrics;
|
|
runs: OrchestratorShadowRun[];
|
|
};
|
|
|
|
export type OrchestratorCanaryReadinessCheck = {
|
|
id: string;
|
|
passed: boolean;
|
|
actual: string | number | boolean | null;
|
|
target: string | number | boolean | null;
|
|
};
|
|
|
|
export type OrchestratorCanaryReadiness = {
|
|
generatedAt: number;
|
|
ready: boolean;
|
|
recommendation: 'keep_shadow' | 'manual_canary_review';
|
|
window: { hours: number; from: number };
|
|
thresholds: {
|
|
hours: number;
|
|
minObservations: number;
|
|
minSuccessRate: number;
|
|
maxP95LatencyMs: number;
|
|
minLatencyCoverageRate: number;
|
|
minNativeSettledRate: number;
|
|
minDistinctSessions: number;
|
|
maxHoursSinceLastObservation: number;
|
|
};
|
|
samples: {
|
|
totalObservations: number;
|
|
eligibleObservations: number;
|
|
excludedSynthetic: number;
|
|
successes: number;
|
|
failures: number;
|
|
successRate: number | null;
|
|
latencyCoverageRate: number | null;
|
|
latencyP95Ms: number | null;
|
|
nativeSettledRate: number | null;
|
|
distinctSessions: number;
|
|
lastObservedAt: number | null;
|
|
hoursSinceLastObservation: number | null;
|
|
sampled: boolean;
|
|
};
|
|
service: {
|
|
status: string | null;
|
|
latencyMs: number | null;
|
|
checkpointKind: string | null;
|
|
checkpointDurable: boolean | null;
|
|
execution: string | null;
|
|
};
|
|
checks: OrchestratorCanaryReadinessCheck[];
|
|
blockers: string[];
|
|
failureCodes: Array<{ code: string; count: number }>;
|
|
};
|
|
|
|
export type OrchestratorShadowRunDetail = {
|
|
native: {
|
|
runId: string;
|
|
requestId: string;
|
|
userId: string;
|
|
sessionId: string | null;
|
|
status: string;
|
|
attempts: number;
|
|
error: string | null;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
startedAt: number | null;
|
|
completedAt: number | null;
|
|
};
|
|
localEvents: Array<{
|
|
eventId: string;
|
|
type: string;
|
|
data: Record<string, unknown> | null;
|
|
createdAt: number;
|
|
}>;
|
|
remote: {
|
|
available: boolean;
|
|
state: {
|
|
status?: string;
|
|
phase?: string;
|
|
plan?: Record<string, unknown>;
|
|
result?: Record<string, unknown>;
|
|
} | null;
|
|
events: Array<{
|
|
sequence?: number;
|
|
type?: string;
|
|
timestamp?: number;
|
|
data?: Record<string, unknown> | null;
|
|
}>;
|
|
error: { code: string; message: string } | null;
|
|
};
|
|
};
|
|
|
|
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchAdminSummary() {
|
|
return adminFetch<{ summary: AdminSummary }>('/admin-api/summary');
|
|
}
|
|
|
|
// ─── Trust & Policy ───────────────────────────────────────────────────────────
|
|
|
|
export async function fetchSystemDisclosurePolicy() {
|
|
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config');
|
|
}
|
|
|
|
export async function updateSystemDisclosurePolicy(config: SystemDisclosurePolicyConfig) {
|
|
return adminFetch<SystemDisclosurePolicyState>('/admin-api/system-disclosure-policy/config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ config }),
|
|
});
|
|
}
|
|
|
|
// ─── Workflow Orchestrator ───────────────────────────────────────────────────
|
|
|
|
export async function fetchOrchestratorConfig() {
|
|
return adminFetch<OrchestratorConfigState>('/admin-api/orchestrator/config');
|
|
}
|
|
|
|
export async function updateOrchestratorConfig(config: OrchestratorConfig) {
|
|
return adminFetch<OrchestratorConfigState>('/admin-api/orchestrator/config', {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ config }),
|
|
});
|
|
}
|
|
|
|
export async function fetchOrchestratorRuntime() {
|
|
return adminFetch<OrchestratorRuntimeState>('/admin-api/orchestrator/runtime');
|
|
}
|
|
|
|
export async function fetchOrchestratorShadowRuns(params: {
|
|
hours?: number;
|
|
limit?: number;
|
|
status?: 'all' | 'succeeded' | 'failed';
|
|
} = {}) {
|
|
const query = new URLSearchParams();
|
|
if (params.hours) query.set('hours', String(params.hours));
|
|
if (params.limit) query.set('limit', String(params.limit));
|
|
if (params.status) query.set('status', params.status);
|
|
return adminFetch<OrchestratorShadowRunList>(
|
|
`/admin-api/orchestrator/shadow-runs?${query}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchOrchestratorCanaryReadiness() {
|
|
return adminFetch<OrchestratorCanaryReadiness>(
|
|
'/admin-api/orchestrator/canary-readiness',
|
|
);
|
|
}
|
|
|
|
export async function fetchOrchestratorShadowRun(runId: string) {
|
|
return adminFetch<OrchestratorShadowRunDetail>(
|
|
`/admin-api/orchestrator/shadow-runs/${encodeURIComponent(runId)}`,
|
|
);
|
|
}
|
|
|
|
// ─── Users ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchAdminUsers(params: {
|
|
page?: number;
|
|
pageSize?: number;
|
|
search?: string;
|
|
role?: string;
|
|
status?: string;
|
|
}) {
|
|
const q = new URLSearchParams();
|
|
if (params.page) q.set('page', String(params.page));
|
|
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
|
if (params.search) q.set('search', params.search);
|
|
if (params.role) q.set('role', params.role);
|
|
if (params.status) q.set('status', params.status);
|
|
return adminFetch<{ users: AdminUser[]; total: number; page: number; pageSize: number; totalPages: number }>(
|
|
`/admin-api/users?${q}`,
|
|
);
|
|
}
|
|
|
|
export async function createAdminUser(body: {
|
|
username: string;
|
|
password: string;
|
|
displayName?: string;
|
|
role?: string;
|
|
}) {
|
|
return adminFetch<{ user: AdminUser }>('/admin-api/users', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
export async function patchAdminUser(
|
|
userId: string,
|
|
patch: { role?: string; status?: string; displayName?: string; password?: string },
|
|
) {
|
|
return adminFetch<{ user: AdminUser }>(`/admin-api/users/${userId}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(patch),
|
|
});
|
|
}
|
|
|
|
export async function rechargeUser(userId: string, amountCents: number, note = '') {
|
|
return adminFetch<{ user: AdminUser }>(`/admin-api/users/${userId}/recharge`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ amountCents, note }),
|
|
});
|
|
}
|
|
|
|
// ─── Billing ──────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchAdminLedger(params: { page?: number; pageSize?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.page) q.set('page', String(params.page));
|
|
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
|
return adminFetch<{ entries: LedgerEntry[]; total: number; page: number; totalPages: number }>(
|
|
`/admin-api/ledger?${q}`,
|
|
);
|
|
}
|
|
|
|
export async function fetchAdminUsage(params: { page?: number; pageSize?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.page) q.set('page', String(params.page));
|
|
if (params.pageSize) q.set('pageSize', String(params.pageSize));
|
|
return adminFetch<{ records: UsageRecord[]; total: number; page: number; totalPages: number }>(
|
|
`/admin-api/usage?${q}`,
|
|
);
|
|
}
|
|
|
|
// ─── WeChat MP ────────────────────────────────────────────────────────────────
|
|
|
|
export async function fetchWechatSummary() {
|
|
return adminFetch<WechatAdminSummary>('/admin-api/wechat/summary');
|
|
}
|
|
|
|
export async function fetchWechatBindings(params: { search?: string; limit?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.search) q.set('search', params.search);
|
|
if (params.limit) q.set('limit', String(params.limit));
|
|
return adminFetch<{ bindings: WechatBinding[] }>(`/admin-api/wechat/bindings?${q}`);
|
|
}
|
|
|
|
export async function fetchWechatMessages(params: { status?: string; limit?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.status) q.set('status', params.status);
|
|
if (params.limit) q.set('limit', String(params.limit));
|
|
return adminFetch<{ messages: WechatMessage[] }>(`/admin-api/wechat/messages?${q}`);
|
|
}
|
|
|
|
export async function fetchWechatDigests(params: { status?: string; limit?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.status) q.set('status', params.status);
|
|
if (params.limit) q.set('limit', String(params.limit));
|
|
return adminFetch<{ digests: WechatDigestSubscription[] }>(`/admin-api/wechat/digests?${q}`);
|
|
}
|
|
|
|
export async function fetchWechatDeliveries(params: { status?: string; limit?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.status) q.set('status', params.status);
|
|
if (params.limit) q.set('limit', String(params.limit));
|
|
return adminFetch<{ deliveries: WechatDeliveryLog[] }>(`/admin-api/wechat/deliveries?${q}`);
|
|
}
|
|
|
|
export async function fetchWechatWebNotifications(params: { status?: string; limit?: number } = {}) {
|
|
const q = new URLSearchParams();
|
|
if (params.status) q.set('status', params.status);
|
|
if (params.limit) q.set('limit', String(params.limit));
|
|
return adminFetch<{ notifications: WechatWebNotification[] }>(
|
|
`/admin-api/wechat/web-notifications?${q}`,
|
|
);
|
|
}
|
|
|
|
export async function createWechatWebNotification(body: CreateWechatNotificationPayload) {
|
|
return adminFetch<{
|
|
ok: boolean;
|
|
created: number;
|
|
wechatSent: number;
|
|
wechatFailures: Array<{ userId: string; message: string }>;
|
|
targets: number;
|
|
}>('/admin-api/wechat/web-notifications', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
export async function clearWechatRoute(userId: string) {
|
|
return adminFetch<{ ok: boolean; deleted: number; openidMasked: string }>(
|
|
`/admin-api/wechat/users/${userId}/route/clear`,
|
|
{ method: 'POST' },
|
|
);
|
|
}
|
|
|
|
export async function cancelWechatDigest(id: string) {
|
|
return adminFetch<{ ok: boolean }>(`/admin-api/wechat/digests/${id}/cancel`, { method: 'POST' });
|
|
}
|
|
|
|
export async function resumeWechatDigest(id: string) {
|
|
return adminFetch<{ ok: boolean; nextRunAt: number }>(`/admin-api/wechat/digests/${id}/resume`, {
|
|
method: 'POST',
|
|
});
|
|
}
|