export type AiRequestInspectionStatus = 'prepared' | 'succeeded' | 'failed'; export interface AiRequestInspectionRecord { id: string; title: string; category: 'preview' | 'generation' | 'review' | 'render'; method: string; path: string; status: AiRequestInspectionStatus; submitted_at: string; finished_at: string | null; request_id: string | null; request_body: unknown; execution_evidence: unknown; error_message: string | null; } type InspectionListener = (records: AiRequestInspectionRecord[]) => void; const STORAGE_KEY = 'ai_content_ai_request_inspections_v1'; const MAX_RECORDS = 30; const listeners = new Set(); let records: AiRequestInspectionRecord[] = readStoredRecords(); const AI_REQUEST_PATTERNS = [ /\/provider-lab\/runs$/, /\/prompt-storyboard(?:\/preview)?$/, /\/original\/(?:idea|outline|chapters|self-check)$/, /\/production\/prompts\/.+\/preview$/, /\/production\/contracts\/.+\/ai-review$/, /\/projects\/.+\/reviews\/text$/, /\/assets\/.+\/review$/, /\/admin\/novel-creation-wizard\/proposals\/jobs$/, /\/novel-generation\/.+\/(?:ip-bible|chapters\/.+\/run|chapters\/batch)\/jobs$/, /\/admin\/hit-analyses(?:$|\/.+\/(?:analyze|patterns)$)/, /(?:^|[\/_-])(?:generate|regenerate|retry|extract|optimize|ai-review|preflight|render|compose|synthesize|test|review-visual|quality-check|cost-estimate|request-preview|plan-request-preview|prepare|auto-bind)(?:[\/?_-]|$)/, ]; export function isInspectableAiRequest(path: string, method: string) { if (method === 'HEAD') return false; if (method === 'GET' && !/\/(?:preflight|cost-estimate)(?:[/?]|$)/.test(path)) return false; return AI_REQUEST_PATTERNS.some((pattern) => pattern.test(path)); } export function beginAiRequestInspection(input: { path: string; method: string; body: unknown; }) { const record: AiRequestInspectionRecord = { id: createInspectionId(), title: requestTitle(input.path), category: requestCategory(input.path), method: input.method, path: input.path, status: 'prepared', submitted_at: new Date().toISOString(), finished_at: null, request_id: null, request_body: normalizeInspectorValue(buildSubmittedParameters(input.path, input.body)), execution_evidence: null, error_message: null }; records = [record, ...records].slice(0, MAX_RECORDS); publish(); return record.id; } export function completeAiRequestInspection( id: string | null, input: { requestId?: string | null; responseData?: unknown } ) { if (!id) return; const record = records.find((item) => item.id === id); const extractedEvidence = extractExecutionEvidence(input.responseData); updateInspection(id, { status: 'succeeded', finished_at: new Date().toISOString(), request_id: input.requestId ?? null, execution_evidence: extractedEvidence ?? ( record?.category === 'preview' ? normalizeInspectorValue(input.responseData) : null ), error_message: null }); } export function failAiRequestInspection(id: string | null, error: unknown) { if (!id) return; updateInspection(id, { status: 'failed', finished_at: new Date().toISOString(), error_message: error instanceof Error ? error.message : String(error ?? '未知错误') }); } export function subscribeAiRequestInspections(listener: InspectionListener) { listeners.add(listener); listener([...records]); return () => listeners.delete(listener); } export function clearAiRequestInspections() { records = []; publish(); } function updateInspection(id: string, patch: Partial) { records = records.map((record) => record.id === id ? { ...record, ...patch } : record); publish(); } function publish() { writeStoredRecords(records); const snapshot = [...records]; for (const listener of listeners) listener(snapshot); } function requestCategory(path: string): AiRequestInspectionRecord['category'] { if (/preview|preflight/.test(path)) return 'preview'; if (/review|optimize/.test(path)) return 'review'; if (/render|compose|synthesize/.test(path)) return 'render'; return 'generation'; } function requestTitle(path: string) { const rules: Array<[RegExp, string]> = [ [/provider-lab\/runs$/, 'API 快测'], [/admin\/providers\/.+\/test$/, '管理端 Provider 实测'], [/novel-creation-wizard\/proposals\/jobs$/, '小说方案生成'], [/novel-generation\/.+\/ip-bible\/jobs$/, '小说 IP 圣经生成'], [/novel-generation\/.+\/chapters\/.+\/jobs$/, '小说章节生成'], [/admin\/hit-analyses(?:$|\/.+\/(?:analyze|patterns)$)/, '爆款案例 AI 分析'], [/projects\/.+\/reviews\/text$|assets\/.+\/review$/, '内容 AI 审核'], [/prompt-storyboard\/preview$/, '提示词分镜预览'], [/prompt-storyboard$/, '提示词生成分镜'], [/production\/prompts\/(.+)\/preview$/, 'S+ 阶段 Prompt 预览'], [/production\/contracts\/.+\/ai-review$/, 'S+ 场景剧本 AI 审核'], [/request-preview|plan-request-preview/i, 'AI 请求预览'], [/anchor-video-test/i, '角色元素视频测试'], [/review-visual/i, '角色图片视觉质检'], [/quality-check/i, '视频质量检查'], [/preflight/i, '视频生成预检'], [/cost-estimate/i, '生成成本估算'], [/auto-bind/i, '角色音色自动绑定'], [/character.*(?:image|anchor).*generate|images.*generate/i, '角色资产生成'], [/keyframe.*generate|generate.*keyframe/i, '关键帧生成'], [/video.*generate|generate.*video/i, '视频生成'], [/audio.*generate|voice.*generate|tts/i, '语音生成'], [/subtitle.*generate/i, '字幕生成'], [/music.*generate/i, '音乐生成'], [/story.*generate/i, '故事圣经生成'], [/episode.*generate/i, '分集计划生成'], [/script.*generate/i, '剧本生成'], [/storyboard.*generate/i, '分镜生成'], [/render|compose/i, '成片合成'], [/review|optimize/i, 'AI 审核优化'] ]; return rules.find(([pattern]) => pattern.test(path))?.[1] ?? 'AI 请求'; } function buildSubmittedParameters(path: string, body: unknown) { const queryIndex = path.indexOf('?'); const query = queryIndex >= 0 ? path.slice(queryIndex + 1) : ''; const queryParams: Record = {}; if (query) { const search = new URLSearchParams(query); for (const key of new Set(search.keys())) { const values = search.getAll(key); queryParams[key] = values.length > 1 ? values : values[0] ?? ''; } } return { path_params: path.slice(0, queryIndex >= 0 ? queryIndex : undefined), query_params: queryParams, body: body ?? null }; } function normalizeInspectorValue(value: unknown, depth = 0, seen = new WeakSet()): unknown { if (depth > 12) return '[超过展示深度]'; if (value === null || value === undefined) return value ?? null; if (typeof value === 'bigint') return value.toString(); if (typeof value === 'string') return summarizeBinaryString(value); if (typeof value === 'number' || typeof value === 'boolean') return value; if (typeof File !== 'undefined' && value instanceof File) { return { file_name: value.name, mime_type: value.type || 'application/octet-stream', size_bytes: value.size, last_modified: value.lastModified }; } if (typeof Blob !== 'undefined' && value instanceof Blob) { return { mime_type: value.type || 'application/octet-stream', size_bytes: value.size }; } if (typeof FormData !== 'undefined' && value instanceof FormData) { const result: Record = {}; for (const [key, item] of value.entries()) { const normalized = normalizeInspectorValue(parseFormValue(key, item), depth + 1, seen); const current = result[key]; result[key] = current === undefined ? normalized : Array.isArray(current) ? [...current, normalized] : [current, normalized]; } return result; } if (Array.isArray(value)) { return value.map((item) => normalizeInspectorValue(item, depth + 1, seen)); } if (typeof value === 'object') { if (seen.has(value)) return '[循环引用]'; seen.add(value); const result: Record = {}; for (const [key, item] of Object.entries(value)) { result[key] = isSecretKey(key) ? '[已隐藏敏感字段]' : normalizeInspectorValue(item, depth + 1, seen); } return result; } return String(value); } function parseFormValue(key: string, value: FormDataEntryValue) { if (typeof value !== 'string') return value; if (!/(?:_json|params|input)$/i.test(key)) return value; try { return JSON.parse(value) as unknown; } catch { return value; } } function summarizeBinaryString(value: string) { if (/^data:[^;]+;base64,/i.test(value)) { const commaIndex = value.indexOf(','); return `[Base64 二进制已省略,共 ${Math.max(0, value.length - commaIndex - 1)} 字符]`; } if (value.length > 10_000 && /^[A-Za-z0-9+/=\r\n]+$/.test(value)) { return `[疑似 Base64 二进制已省略,共 ${value.length} 字符]`; } return value; } function isSecretKey(key: string) { return /(?:password|authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|secret)/i.test(key); } function extractExecutionEvidence(value: unknown) { if (!value || typeof value !== 'object') return null; const evidenceKeys = /^(?:task|tasks|input_json|request_preview|request_params|request_params_override|generation_plan|generation_plans|provider|provider_log|route_decision|router_decision|compiled_request|final_request|cost_estimate|estimated_cost)$/i; const evidence: Record = {}; function visit(node: unknown, depth: number, prefix: string) { if (!node || typeof node !== 'object' || depth > 5) return; if (Array.isArray(node)) { node.forEach((item, index) => visit(item, depth + 1, `${prefix}[${index}]`)); return; } for (const [key, item] of Object.entries(node)) { const path = prefix ? `${prefix}.${key}` : key; if (evidenceKeys.test(key)) evidence[path] = normalizeInspectorValue(item); if (!evidenceKeys.test(key)) visit(item, depth + 1, path); } } visit(value, 0, ''); return Object.keys(evidence).length > 0 ? evidence : null; } function createInspectionId() { return `ai_req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; } function readStoredRecords(): AiRequestInspectionRecord[] { if (typeof window === 'undefined') return []; try { const parsed = JSON.parse(window.sessionStorage.getItem(STORAGE_KEY) || '[]') as unknown; return Array.isArray(parsed) ? parsed.slice(0, MAX_RECORDS) as AiRequestInspectionRecord[] : []; } catch { return []; } } function writeStoredRecords(nextRecords: AiRequestInspectionRecord[]) { if (typeof window === 'undefined') return; try { window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(nextRecords)); } catch { // Inspection history is diagnostic only; generation must not fail if storage is full. } }