import { useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from 'react'; import { Popover as AntPopover } from 'antd'; import { ChevronLeft, ChevronRight, Copy, CreditCard, Eye, KeyRound, ListChecks, Plus, ReceiptText, RotateCcw, Search, ShieldCheck, SlidersHorizontal, Trash2, UserRound } from 'lucide-react'; import type { GatewayAccessRuleBatchRequest, GatewayApiKey, GatewayTask, GatewayTaskParamPreprocessingLog, GatewayWalletAccount, GatewayWalletTransaction, IntegrationPlatform, PlatformModel } from '@easyai-ai-gateway/contracts'; import type { ConsoleData } from '../app-state'; import { EntityTable } from '../components/EntityTable'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, DateTimePicker, DateTimeRangePicker, FormDialog, Input, Label, Select, Table, TableCell, TableFooter, TableHead, TablePageActions, TableRow, TableToolbar, TableViewportLayout, Tabs } from '../components/ui'; import { AccessPermissionEditor, countAccessPermissionRules } from './admin/AccessPermissionEditor'; import type { ApiKeyForm, LoadState, WorkspaceSection, WorkspaceTaskQuery, WorkspaceTransactionQuery } from '../types'; import { listTaskParamPreprocessing } from '../api'; const tabs = [ { value: 'overview', label: '个人总览', icon: }, { value: 'billing', label: '余额充值', icon: }, { value: 'apiKeys', label: 'API Key', icon: }, { value: 'tasks', label: '任务记录', icon: }, { value: 'transactions', label: '消费记录', icon: }, ] satisfies Array<{ value: WorkspaceSection; label: string; icon: ReactNode }>; const taskPageSizeOptions = [10, 20, 50]; export function WorkspacePage(props: { apiKeyForm: ApiKeyForm; apiKeySecret: string; apiKeySecretsById: Record; apiKeyPolicyModels: PlatformModel[]; data: ConsoleData; message: string; section: WorkspaceSection; state: LoadState; token: string; taskQuery: WorkspaceTaskQuery; taskTotal: number; transactionQuery: WorkspaceTransactionQuery; transactionTotal: number; onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise; onDeleteApiKey: (apiKeyId: string) => Promise; onApiKeyFormChange: (value: ApiKeyForm) => void; onSectionChange: (value: WorkspaceSection) => void; onSubmitApiKey: (event: FormEvent) => void | Promise; onTaskQueryChange: (value: WorkspaceTaskQuery) => void; onTransactionQueryChange: (value: WorkspaceTransactionQuery) => void; onUseApiKeyForPlayground: (apiKeyId?: string) => void; }) { return (
{props.section === 'overview' && } {props.section === 'billing' && } {props.section === 'apiKeys' && } {props.section === 'tasks' && } {props.section === 'transactions' && }
); } function WorkspaceOverview(props: { data: ConsoleData }) { const owner = props.data.users[0]; return (
个人中心总览 用户组策略 [item.groupKey, item.priority, item.status, item.source])} />
); } function BillingPanel(props: { walletAccounts: GatewayWalletAccount[] }) { const primaryWallet = primaryWalletAccount(props.walletAccounts); const availableBalance = primaryWallet ? primaryWallet.balance - primaryWallet.frozenBalance : 0; return (
余额 {primaryWallet?.currency ?? 'resource'} {formatMoney(primaryWallet?.balance ?? 0)} {primaryWallet?.status ?? 'active'}
充值
); } function ConsumptionPanel(props: { data: ConsoleData; query: WorkspaceTransactionQuery; total: number; onQueryChange: (value: WorkspaceTransactionQuery) => void; }) { const transactions = props.data.walletTransactions; const transactionQuery = props.query; const [pageJump, setPageJump] = useState(String(transactionQuery.page)); const pageSizeOptions = useMemo(() => { return Array.from(new Set([...taskPageSizeOptions, transactionQuery.pageSize])).sort((a, b) => a - b); }, [transactionQuery.pageSize]); const totalPages = Math.max(1, Math.ceil(props.total / transactionQuery.pageSize)); const currentPage = Math.min(transactionQuery.page, totalPages); const pageStart = props.total ? Math.min((currentPage - 1) * transactionQuery.pageSize + 1, props.total) : 0; const pageEnd = Math.min(currentPage * transactionQuery.pageSize, props.total); const hasActiveFilters = Boolean(transactionQuery.query || transactionQuery.createdFrom || transactionQuery.createdTo); useEffect(() => { if (transactionQuery.page > totalPages) { props.onQueryChange({ ...transactionQuery, page: totalPages }); } }, [props.onQueryChange, transactionQuery, totalPages]); useEffect(() => { setPageJump(String(currentPage)); }, [currentPage]); function updateQuery(value: string) { props.onQueryChange({ ...transactionQuery, query: value, page: 1 }); } function updateCreatedRange(value: { from: string; to: string }) { props.onQueryChange({ ...transactionQuery, createdFrom: value.from, createdTo: value.to, page: 1 }); } function updatePageSize(value: string) { const nextPageSize = Number(value); props.onQueryChange({ ...transactionQuery, page: 1, pageSize: Number.isFinite(nextPageSize) ? nextPageSize : 10 }); } function updatePage(page: number) { props.onQueryChange({ ...transactionQuery, page }); } function submitPageJump() { const parsed = Number(pageJump); if (!Number.isFinite(parsed) || parsed <= 0) { setPageJump(String(currentPage)); return; } updatePage(Math.min(totalPages, Math.max(1, Math.floor(parsed)))); } function resetFilters() { props.onQueryChange({ query: '', createdFrom: '', createdTo: '', page: 1, pageSize: transactionQuery.pageSize, }); } return ( {transactions.length ? ( 消费时间 模型 平台 类型 API Key Token 消耗 扣费 余额变化 任务 {transactions.map((transaction) => ( ))}
) : (
暂无消费记录 完成可计费任务后会生成消费流水。
)}
共 {props.total} 条 · {pageStart}-{pageEnd}
{ event.preventDefault(); submitPageJump(); }} > 第 {currentPage} / {totalPages} 页 跳至 setPageJump(event.target.value)} />
); } function WalletTransactionRecord(props: { transaction: GatewayWalletTransaction }) { const transaction = props.transaction; const metadata = transaction.metadata; const referenceId = transaction.referenceId || metadataString(transaction.metadata, 'taskId'); const requestId = metadataString(metadata, 'requestId'); const model = metadataString(metadata, 'resolvedModel') || metadataString(metadata, 'platformModelAlias') || metadataString(metadata, 'providerModel') || metadataString(metadata, 'model'); const requestedModel = metadataString(metadata, 'requestedModel'); const platform = metadataString(metadata, 'platformName') || metadataString(metadata, 'platformKey') || metadataString(metadata, 'provider') || metadataString(metadata, 'clientId'); const provider = metadataString(metadata, 'provider'); const taskType = metadataString(metadata, 'modelType') || metadataString(metadata, 'kind'); const apiKey = metadataString(metadata, 'apiKeyName') || metadataString(metadata, 'apiKeyPrefix') || metadataString(metadata, 'apiKeyId'); const chargeAmount = transactionChargeAmount(transaction); const chargeCurrency = transactionChargeCurrency(transaction); return ( {formatDateTime(transaction.createdAt)} {model || '-'} {requestedModel && requestedModel !== model && {requestedModel}} {platform || '-'} {provider && provider !== platform && {provider}} {taskType || '-'} {transactionTypeLabel(transaction.transactionType)} {apiKey || '-'} {formatTokenUsage(metadataObject(metadata, 'usage'))} {transaction.direction === 'debit' ? '-' : '+'}{formatMoney(chargeAmount)} {chargeCurrency} {formatMoney(transaction.balanceBefore)} {formatMoney(transaction.balanceAfter)} {referenceId ? {referenceId} : '-'} {requestId && RequestID {requestId}} ); } function ApiKeyPanel(props: { apiKeyForm: ApiKeyForm; apiKeySecret: string; apiKeySecretsById: Record; apiKeyPolicyModels: PlatformModel[]; data: ConsoleData; message: string; state: LoadState; onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise; onDeleteApiKey: (apiKeyId: string) => Promise; onApiKeyFormChange: (value: ApiKeyForm) => void; onSubmitApiKey: (event: FormEvent) => void | Promise; onUseApiKeyForPlayground: (apiKeyId?: string) => void; }) { const [createOpen, setCreateOpen] = useState(false); const [policyApiKeyId, setPolicyApiKeyId] = useState(''); const [pendingDelete, setPendingDelete] = useState(null); const [localMessage, setLocalMessage] = useState(''); const selectedPolicyKey = useMemo( () => props.data.apiKeys.find((item) => item.id === policyApiKeyId), [policyApiKeyId, props.data.apiKeys], ); const permissionPlatforms = useMemo(() => platformsForPermissionTree(props.apiKeyPolicyModels), [props.apiKeyPolicyModels]); async function copyApiKey(item: GatewayApiKey) { const secret = apiKeySecretFor(item, props.apiKeySecretsById); if (!secret) return; await navigator.clipboard.writeText(secret); setLocalMessage(`已复制 ${item.name || item.keyPrefix}`); } async function submitCreate(event: FormEvent) { try { await props.onSubmitApiKey(event); setCreateOpen(false); } catch { return; } } async function confirmDeleteApiKey() { if (!pendingDelete) return; await props.onDeleteApiKey(pendingDelete.id); setPendingDelete(null); } return ( <>
API Key

按 Key 维护调用凭证、最近使用时间和平台/模型权限策略。

{(localMessage || props.message) &&

{localMessage || props.message}

} 名称 API Key 权限策略 最近使用 有效期 状态 创建时间 操作 {props.data.apiKeys.length ? props.data.apiKeys.map((item) => { const secret = apiKeySecretFor(item, props.apiKeySecretsById); const summary = countAccessPermissionRules(props.data.accessRules, 'api_key', item.id); return ( {item.name} {item.keyPrefix} {secret ? maskApiKey(secret) : item.keyPrefix} {formatDateTime(item.lastUsedAt)} {formatDateTime(item.expiresAt)} {item.status} {formatDateTime(item.createdAt)} ); }) : (
暂无 API Key 点击右上角创建调用凭证。
)}
)} open={createOpen} title="创建 API Key" onClose={() => setCreateOpen(false)} onSubmit={(event) => void submitCreate(event)} > setPolicyApiKeyId('')}>关闭} open={Boolean(selectedPolicyKey)} title={selectedPolicyKey ? `权限策略:${selectedPolicyKey.name}` : '权限策略'} onClose={() => setPolicyApiKeyId('')} onSubmit={(event) => event.preventDefault()} > setPendingDelete(null)} onConfirm={confirmDeleteApiKey} /> ); } function TaskPanel(props: { data: ConsoleData; query: WorkspaceTaskQuery; token: string; total: number; onQueryChange: (value: WorkspaceTaskQuery) => void; }) { const [localMessage, setLocalMessage] = useState(''); const [jsonTask, setJsonTask] = useState(null); const [pageJump, setPageJump] = useState(String(props.query.page)); const taskQuery = props.query; const tasks = props.data.tasks; const taskTypes = useMemo(() => { const knownTypes = ['text_generate', 'image_generate', 'image_edit', 'video_generate', 'image_to_video']; const values = [...knownTypes, taskQuery.modelType, ...tasks.map((task) => task.modelType)]; return Array.from(new Set(values.filter((value): value is string => Boolean(value)))).sort((a, b) => a.localeCompare(b)); }, [taskQuery.modelType, tasks]); const pageSizeOptions = useMemo(() => { return Array.from(new Set([...taskPageSizeOptions, taskQuery.pageSize])).sort((a, b) => a - b); }, [taskQuery.pageSize]); const totalPages = Math.max(1, Math.ceil(props.total / taskQuery.pageSize)); const currentPage = Math.min(taskQuery.page, totalPages); const pageStart = props.total ? Math.min((currentPage - 1) * taskQuery.pageSize + 1, props.total) : 0; const pageEnd = Math.min(currentPage * taskQuery.pageSize, props.total); const hasActiveFilters = Boolean(taskQuery.query || taskQuery.createdFrom || taskQuery.createdTo || taskQuery.modelType); useEffect(() => { if (taskQuery.page > totalPages) { props.onQueryChange({ ...taskQuery, page: totalPages }); } }, [props.onQueryChange, taskQuery, totalPages]); useEffect(() => { setPageJump(String(currentPage)); }, [currentPage]); async function copyTaskRequestId(task: GatewayTask) { if (!task.requestId) return; await navigator.clipboard.writeText(task.requestId); setLocalMessage(`已复制 RequestID:${task.requestId}`); } function resetFilters() { props.onQueryChange({ query: '', modelType: '', createdFrom: '', createdTo: '', page: 1, pageSize: taskQuery.pageSize, }); } function updateQuery(value: string) { props.onQueryChange({ ...taskQuery, query: value, page: 1 }); } function updateTypeFilter(value: string) { props.onQueryChange({ ...taskQuery, modelType: value === 'all' ? '' : value, page: 1 }); } function updateCreatedRange(value: { from: string; to: string }) { props.onQueryChange({ ...taskQuery, createdFrom: value.from, createdTo: value.to, page: 1 }); } function updatePageSize(value: string) { const nextPageSize = Number(value); props.onQueryChange({ ...taskQuery, page: 1, pageSize: Number.isFinite(nextPageSize) ? nextPageSize : 10 }); } function updatePage(page: number) { props.onQueryChange({ ...taskQuery, page }); } function submitPageJump() { const parsed = Number(pageJump); if (!Number.isFinite(parsed) || parsed <= 0) { setPageJump(String(currentPage)); return; } updatePage(Math.min(totalPages, Math.max(1, Math.floor(parsed)))); } return ( <> {localMessage &&

{localMessage}

} {tasks.length ? ( 任务 RequestID 状态 模型 尝试链路 参数转换 类型 API Key Token 扣费 耗时 创建时间 原始 JSON {tasks.map((task) => ( ))}
) : (
{hasActiveFilters ? '没有匹配的任务' : '暂无任务'} {hasActiveFilters && 调整关键词、类型或创建时间后再试。}
)}
共 {props.total} 条 · {pageStart}-{pageEnd}
{ event.preventDefault(); submitPageJump(); }} > 第 {currentPage} / {totalPages} 页 跳至 setPageJump(event.target.value)} />
setJsonTask(null)}>关闭} open={Boolean(jsonTask)} title="任务原始 JSON" onClose={() => setJsonTask(null)} onSubmit={(event) => event.preventDefault()} >
{JSON.stringify(jsonTask, null, 2)}
); } function TaskRecord(props: { task: GatewayTask; token: string; onCopyRequestId: (task: GatewayTask) => Promise; onOpenJson: (task: GatewayTask) => void }) { const usage = props.task.usage ?? {}; const tokenUsage = formatTokenUsage(usage); const chargeText = props.task.finalChargeAmount !== undefined ? formatCellValue(props.task.finalChargeAmount) : '-'; const resolvedModel = props.task.resolvedModel || props.task.model; const badgeVariant = props.task.status === 'succeeded' ? 'success' : props.task.status === 'failed' ? 'destructive' : 'secondary'; return ( {props.task.kind} ID {props.task.id} {props.task.requestId || '-'} {props.task.status} {resolvedModel} {props.task.requestedModel && props.task.requestedModel !== resolvedModel && {props.task.requestedModel}} {props.task.modelType || '-'} {props.task.apiKeyName || props.task.apiKeyPrefix || props.task.apiKeyId || '-'} {tokenUsage} {chargeText} {formatDuration(props.task.responseDurationMs)} {formatDateTime(props.task.createdAt)} ); } type TaskParamConversionSummary = { changed: boolean; changeCount: number; actions: string[]; paths: string[]; capabilityPaths: string[]; }; function TaskParamConversionCell(props: { task: GatewayTask; token: string }) { const summary = taskParamConversionSummary(props.task); const [open, setOpen] = useState(false); const [logs, setLogs] = useState(null); const [loadState, setLoadState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle'); const [error, setError] = useState(''); const logsRef = useRef(null); const loadingRef = useRef(false); useEffect(() => { logsRef.current = null; loadingRef.current = false; setLogs(null); setLoadState('idle'); setError(''); }, [props.task.id]); useEffect(() => { if (!open || !summary.changed || logsRef.current || loadingRef.current || !props.token) return; let cancelled = false; loadingRef.current = true; setLoadState('loading'); setError(''); listTaskParamPreprocessing(props.token, props.task.id) .then((response) => { if (cancelled) return; const items = response.items ?? []; logsRef.current = items; setLogs(items); setLoadState('ready'); }) .catch((err) => { if (cancelled) return; setError(err instanceof Error ? err.message : '参数转换明细加载失败'); setLoadState('error'); }) .finally(() => { if (!cancelled) { loadingRef.current = false; } }); return () => { cancelled = true; loadingRef.current = false; }; }, [open, props.task.id, props.token, summary.changed]); if (!summary.changed) { return 无转换; } return ( } overlayClassName="taskParamConversionAntPopover" placement="bottomLeft" trigger={['hover', 'focus']} onOpenChange={setOpen} > ); } function TaskParamConversionPopover(props: { error: string; loadState: 'idle' | 'loading' | 'ready' | 'error'; logs: GatewayTaskParamPreprocessingLog[] | null; summary: TaskParamConversionSummary; }) { const logs = props.logs ?? []; return ( 参数转换汇总 {taskParamSummaryText(props.summary)} {props.loadState === 'loading' && 正在加载转换明细...} {props.loadState === 'error' && {props.error || '参数转换明细加载失败'}} {props.loadState === 'ready' && logs.length === 0 && 暂无转换明细。} {logs.map((log) => ( {taskParamLogTitle(log)} {taskParamLogSubtitle(log)} {log.changeCount} 项 {(log.changes ?? []).slice(0, 8).map((change, index) => ( {taskParamActionLabel(objectString(change, 'action'))} {objectString(change, 'path') || '-'} {objectString(change, 'reason') || '按模型能力配置调整参数。'} {objectString(change, 'capabilityPath') && 能力配置:{objectString(change, 'capabilityPath')}} {taskParamChangePreview(change)} ))} {(log.changes?.length ?? 0) > 8 && 还有 {(log.changes?.length ?? 0) - 8} 项转换未展开。} ))} {props.loadState !== 'ready' && props.summary.capabilityPaths.length > 0 && ( {props.summary.capabilityPaths.slice(0, 4).map((path) => {path})} )} ); } function TaskAttemptChain(props: { task: GatewayTask }) { const attempts = props.task.attempts ?? []; if (!attempts.length) return -; return ( } overlayClassName="taskRecordAttemptAntPopover" placement="bottomLeft" trigger={['hover', 'focus']} > ); } function TaskAttemptPopoverContent(props: { task: GatewayTask }) { const attempts = props.task.attempts ?? []; return ( {attempts.map((attempt) => { const trace = taskAttemptTrace(attempt); const rateLimitText = taskAttemptRateLimitText(attempt); return ( #{attempt.attemptNo} {taskAttemptTarget(attempt)} {taskAttemptStatusText(attempt.status)} {taskAttemptMeta(attempt)} {attempt.status === 'failed' && {taskAttemptFailureReason(attempt)}} {(rateLimitText || trace.length > 0) && ( {rateLimitText && {rateLimitText}} {trace.map((entry, index) => ( {taskAttemptTraceText(entry)} ))} )} ); })} ); } function taskAttemptTitle(attempt: NonNullable[number]) { const parts = [ `#${attempt.attemptNo}`, attempt.platformName || attempt.provider || attempt.clientId || '', attempt.status, attempt.errorMessage || attempt.errorCode || metadataString(attempt.metrics, 'error') || '', ].filter(Boolean); return parts.join(' · '); } function taskAttemptTarget(attempt: NonNullable[number]) { return attempt.platformName || attempt.provider || attempt.clientId || `尝试 ${attempt.attemptNo}`; } function taskAttemptStatusText(status: string) { if (status === 'succeeded') return '成功'; if (status === 'failed') return '失败'; if (status === 'running') return '运行中'; return status || '-'; } function taskAttemptMeta(attempt: NonNullable[number]) { const statusCode = taskAttemptStatusCode(attempt); const values = [ attempt.providerModelName || attempt.modelName || attempt.modelAlias, attempt.requestId ? `RequestID ${attempt.requestId}` : '', statusCode ? `状态码 ${statusCode}` : '', attempt.responseDurationMs ? formatDuration(attempt.responseDurationMs) : '', ].filter(Boolean); return values.join(' · ') || attempt.clientId || '-'; } function taskAttemptFailureReason(attempt: NonNullable[number]) { const detail = firstText( attempt.errorMessage, metadataString(attempt.metrics, 'error'), metadataString(attempt.metrics, 'message'), ); const code = firstText(attempt.errorCode, metadataString(attempt.metrics, 'errorCode')); const statusCode = taskAttemptStatusCode(attempt); const statusText = statusCode ? `状态码 ${statusCode}` : ''; const category = taskAttemptFailureCategory(attempt); const categoryText = category ? `错误分类 ${category}` : ''; if (detail && code && detail !== code) return [detail, code, statusText, categoryText].filter(Boolean).join(' · '); return [detail || code || '失败', statusText, categoryText].filter(Boolean).join(' · '); } function taskAttemptStatusCode(attempt: NonNullable[number]) { const value = attempt.statusCode ?? metadataNumber(attempt.metrics, 'statusCode'); return value && Number.isFinite(value) ? Math.trunc(value) : null; } function taskAttemptTrace(attempt: NonNullable[number]) { const raw = attempt.metrics?.trace; if (!Array.isArray(raw)) return []; return raw.filter((item): item is Record => Boolean(item) && typeof item === 'object' && !Array.isArray(item)); } function taskAttemptRateLimitText(attempt: NonNullable[number]) { const detail = metadataObject(attempt.metrics, 'rateLimit'); if (!Object.keys(detail).length) return ''; const scopeName = objectString(detail, 'scopeName') || objectString(detail, 'scopeKey') || '限流对象'; const metric = objectString(detail, 'metric') || 'rate_limit'; const current = metadataNumber(detail, 'current'); const amount = metadataNumber(detail, 'amount'); const projected = metadataNumber(detail, 'projected'); const limit = metadataNumber(detail, 'limit'); const windowSeconds = metadataNumber(detail, 'windowSeconds'); const retryAfterMs = metadataNumber(detail, 'retryAfterMs'); const values = [ `限流 ${scopeName} · ${metric}`, current !== null ? `当前 ${formatCellValue(current)}` : '', amount !== null ? `本次 ${formatCellValue(amount)}` : '', projected !== null ? `预计 ${formatCellValue(projected)}` : '', limit !== null ? `限制 ${formatCellValue(limit)}` : '', windowSeconds !== null ? `窗口 ${Math.trunc(windowSeconds)} 秒` : '', retryAfterMs !== null ? `约 ${formatDuration(Math.trunc(retryAfterMs))} 后可重试` : '', ].filter(Boolean); return values.join(' · '); } function taskAttemptTraceText(entry: Record) { const event = objectString(entry, 'event'); const action = objectString(entry, 'action'); const reason = objectString(entry, 'reason'); const statusCode = metadataNumber(entry, 'statusCode'); const errorCode = objectString(entry, 'errorCode'); const category = objectString(entry, 'category'); const policy = taskAttemptTracePolicy(entry); const values = [ taskAttemptTraceEventLabel(event, action), statusCode ? `状态码 ${Math.trunc(statusCode)}` : '', errorCode ? `错误 ${errorCode}` : '', category ? `错误分类 ${category}` : '', policy, reason ? `原因 ${taskAttemptTraceReasonLabel(reason)}` : '', ].filter(Boolean); return values.join(' · '); } function taskAttemptFailureCategory(attempt: NonNullable[number]) { const category = firstText( metadataString(attempt.metrics, 'errorCategory'), metadataString(attempt.metrics, 'category'), ); if (category) return category; for (const entry of taskAttemptTrace(attempt)) { const traceCategory = objectString(entry, 'category'); if (traceCategory) return traceCategory; } return ''; } function taskAttemptTracePolicy(entry: Record) { const source = objectString(entry, 'policySource'); const policy = objectString(entry, 'policy'); const rule = objectString(entry, 'policyRule'); const matchedValue = objectString(entry, 'matchedValue'); const sourceLabel = source || policy; const policyPath = [sourceLabel, rule].filter(Boolean).join('.'); if (!policyPath) return ''; return matchedValue ? `策略 ${policyPath}=${matchedValue}` : `策略 ${policyPath}`; } function taskAttemptTraceEventLabel(event: string, action: string) { if (event === 'failure') return '失败'; if (event === 'same_client_retry') return action === 'retry' ? '本平台重试' : '本平台停止重试'; if (event === 'failover_next') return '切换下个平台'; if (event === 'failover_stop') return '停止切换平台'; if (event === 'priority_demoted') return '优先级降级'; return event || action || '链路事件'; } function taskAttemptTraceReasonLabel(reason: string) { const labels: Record = { client_call_failed: '客户端调用失败', retry_disabled: '模型重试关闭', retry_deny_policy: '命中模型拒绝重试规则', retry_allow_policy: '命中模型允许重试规则', client_retryable: '客户端标记可重试', client_non_retryable: '客户端标记不可重试', same_client_max_attempts: '达到本平台最大尝试次数', request_validation_failed: '请求校验失败', candidate_selection_failed: '候选模型选择失败', parameter_preprocessing_failed: '参数预处理失败', wallet_balance_check_failed: '余额校验失败', local_rate_limit_blocked: '本地限流拦截', pre_provider_failed: '调用上游前失败', local_rate_limit_wait_queue: '本地限流排队等待', failover_time_budget_exceeded: '超过全局切换时间预算', runner_policy_disabled: '全局调度策略停用', hard_stop_policy: '命中硬拒绝规则', failover_disabled: '平台切换关闭', failover_deny_policy: '命中拒绝切换规则', failover_allow_policy: '命中允许切换规则', max_platforms_reached: '达到最大平台数', no_next_platform: '没有更多候选平台', priority_demote_policy: '命中优先级降级规则', }; return labels[reason] ?? reason; } function taskParamConversionSummary(task: GatewayTask): TaskParamConversionSummary { const summary: TaskParamConversionSummary = { changed: false, changeCount: 0, actions: [], paths: [], capabilityPaths: [], }; let mergedAttemptSummary = false; for (const attempt of task.attempts ?? []) { const attemptSummary = metadataObject(attempt.metrics, 'parameterPreprocessingSummary'); if (Object.keys(attemptSummary).length) { mergedAttemptSummary = true; mergeTaskParamSummary(summary, attemptSummary); } } if (!mergedAttemptSummary) { mergeTaskParamSummary(summary, metadataObject(task.metrics, 'parameterPreprocessingSummary')); } return summary; } function mergeTaskParamSummary(target: TaskParamConversionSummary, raw: Record) { if (!Object.keys(raw).length) return; target.changed = target.changed || raw.changed === true; const changeCount = metadataNumber(raw, 'changeCount'); if (changeCount) target.changeCount += Math.max(0, Math.trunc(changeCount)); for (const action of metadataStringList(raw, 'actions')) appendUniqueText(target.actions, action); for (const path of metadataStringList(raw, 'paths')) appendUniqueText(target.paths, path); for (const path of metadataStringList(raw, 'capabilityPaths')) appendUniqueText(target.capabilityPaths, path); } function taskParamSummaryText(summary: TaskParamConversionSummary) { const actionText = summary.actions.map(taskParamActionLabel).join('、'); const parts = [ `${summary.changeCount || summary.paths.length || 1} 项转换`, actionText ? `动作 ${actionText}` : '', summary.capabilityPaths.length ? `涉及 ${summary.capabilityPaths.length} 项能力配置` : '', ].filter(Boolean); return parts.join(' · '); } function taskParamLogTitle(log: GatewayTaskParamPreprocessingLog) { const parts = [ log.attemptNo ? `#${log.attemptNo}` : '', log.modelType || '', ].filter(Boolean); return parts.join(' · ') || '预处理记录'; } function taskParamLogSubtitle(log: GatewayTaskParamPreprocessingLog) { const model = log.model ?? {}; const modelLabel = firstNonEmptyText( objectString(model, 'modelAlias'), objectString(model, 'providerModelName'), objectString(model, 'modelName'), ); const provider = objectString(model, 'provider'); const parts = [ provider, modelLabel, ].filter(Boolean); if (parts.length) return parts.join(' · '); return log.clientId || '候选模型'; } function firstNonEmptyText(...values: string[]) { return values.find((value) => value.trim()) ?? ''; } function taskParamActionLabel(action: string) { if (action === 'remove') return '移除'; if (action === 'adjust') return '调整'; if (action === 'set') return '补齐'; return action || '转换'; } function taskParamChangePreview(change: Record) { const before = previewCompactValue(change.before); const after = previewCompactValue(change.after); const action = objectString(change, 'action'); if (action === 'remove') return `原值 ${before}`; if (action === 'set') return `新值 ${after}`; return `原值 ${before} -> 新值 ${after}`; } function previewCompactValue(value: unknown) { if (value === undefined || value === null || value === '') return '-'; const text = typeof value === 'string' ? value : JSON.stringify(value); if (!text) return '-'; return text.length > 150 ? `${text.slice(0, 150)}...` : text; } function formatCellValue(value: unknown) { if (value === undefined || value === null || value === '') return '-'; return String(value); } function primaryWalletAccount(accounts: GatewayWalletAccount[]) { return accounts.find((account) => account.currency === 'resource') ?? accounts[0]; } function formatMoney(value: unknown) { const numericValue = typeof value === 'number' ? value : Number(value); if (!Number.isFinite(numericValue)) return '0.00'; return new Intl.NumberFormat(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2, }).format(numericValue); } function transactionTypeLabel(value: string) { if (value === 'task_billing') return '任务扣费'; if (value === 'admin_adjust') return '余额调整'; if (value === 'recharge') return '充值'; if (value === 'refund') return '退款'; if (value === 'reserve') return '冻结'; if (value === 'release') return '释放'; return value || '-'; } function firstText(...values: Array) { for (const value of values) { if (typeof value === 'string' && value.trim()) return value.trim(); } return ''; } function metadataString(metadata: Record | undefined, key: string) { const value = metadata?.[key]; return typeof value === 'string' && value.trim() ? value.trim() : ''; } function metadataStringList(metadata: Record | undefined, key: string) { const value = metadata?.[key]; if (!Array.isArray(value)) return []; return value.filter((item): item is string => typeof item === 'string' && item.trim() !== '').map((item) => item.trim()); } function metadataNumber(metadata: Record | undefined, key: string) { const value = metadata?.[key]; if (value === undefined || value === null || value === '') return null; const numericValue = typeof value === 'number' ? value : Number(value); return Number.isFinite(numericValue) ? numericValue : null; } function metadataObject(metadata: Record | undefined, key: string): Record { const value = metadata?.[key]; if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; return value as Record; } function objectString(value: Record, key: string) { const next = value[key]; return typeof next === 'string' && next.trim() ? next.trim() : ''; } function appendUniqueText(values: string[], value: string) { if (!value || values.includes(value)) return; values.push(value); } function transactionChargeAmount(transaction: GatewayWalletTransaction) { return metadataNumber(transaction.metadata, 'finalChargeAmount') ?? transaction.amount; } function transactionChargeCurrency(transaction: GatewayWalletTransaction) { const summary = metadataObject(transaction.metadata, 'billingSummary'); const finalCharge = metadataObject(summary, 'finalCharge'); return objectString(finalCharge, 'currency') || objectString(summary, 'currency') || transaction.currency || 'resource'; } function formatTokenUsage(usage: Record) { const input = tokenValue(usage.inputTokens ?? usage.promptTokens ?? usage.input_tokens ?? usage.prompt_tokens); const output = tokenValue(usage.outputTokens ?? usage.completionTokens ?? usage.output_tokens ?? usage.completion_tokens); const total = tokenValue(usage.totalTokens ?? usage.total_tokens ?? (input !== null && output !== null ? input + output : null)); if (input === null && output === null && total === null) return '-'; return ( 输入:{formatCellValue(input)}/输出:{formatCellValue(output)} 总计:{formatCellValue(total)} ); } function tokenValue(value: unknown) { if (value === undefined || value === null || value === '') return null; const numericValue = typeof value === 'number' ? value : Number(value); return Number.isFinite(numericValue) ? numericValue : null; } function formatDuration(value?: number) { if (value === undefined || value === null) return '-'; const milliseconds = Math.max(0, Math.round(value)); if (milliseconds === 0) return '0秒'; if (milliseconds < 1000) return `${milliseconds}毫秒`; const totalSeconds = Math.round(milliseconds / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (hours > 0) return `${hours}小时${minutes}分${seconds}秒`; if (minutes > 0) return `${minutes}分${seconds}秒`; return `${seconds}秒`; } function taskErrorText(task: GatewayTask) { return task.errorCode || task.errorMessage || task.error || ''; } function InfoItem(props: { label: string; value: string }) { return (
{props.label} {props.value}
); } function apiKeySecretFor(item: GatewayApiKey, secretsById: Record) { return secretsById[item.id] || item.secret || ''; } function maskApiKey(secret: string) { if (secret.length <= 18) return secret; return `${secret.slice(0, 12)}...${secret.slice(-4)}`; } function permissionSummaryText(summary: ReturnType) { const allow = summary.allow.platforms + summary.allow.models; const deny = summary.deny.platforms + summary.deny.models; if (allow === 0 && deny === 0) return '未配置'; return `允许 ${allow} / 拒绝 ${deny}`; } function formatDateTime(value?: string) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '-'; return date.toLocaleString(); } function platformsForPermissionTree(models: PlatformModel[]): IntegrationPlatform[] { const byPlatform = new Map(); for (const model of models) { if (byPlatform.has(model.platformId)) continue; const name = model.platformName || model.provider || model.platformId; byPlatform.set(model.platformId, { id: model.platformId, provider: model.provider || '', platformKey: model.platformId, name, authType: '', status: 'enabled', priority: 100, defaultPricingMode: 'inherit', defaultDiscountFactor: 1, createdAt: '', updatedAt: '', }); } return Array.from(byPlatform.values()).sort((a, b) => a.name.localeCompare(b.name)); }