import { z } from "zod"; import { parseAnswerUiBlock, presentationCommandSchema, type AnswerUiBlock } from "@/features/agent"; import { type InvestigationPresentationCommand } from "../api/investigation-presentation"; import { INVESTIGATION_DEV_NETWORK } from "./investigation-dev-network"; import type { OperationalTask } from "../api/operational-task-client"; export type InvestigationDevPreview = { commands: InvestigationPresentationCommand[]; blocks: AnswerUiBlock[]; records: Array>; }; export type InvestigationPresentationActivity = { id: number; action: InvestigationPresentationCommand["action"]; source: "live" | "preview" | "chat"; status: "running" | "completed" | "error"; message?: string; }; export const investigationActionLabels: Record = { locate_features: "地图定位", zoom_to_map: "调整视角", render_scada_analysis: "SCADA 分析", clear_scada_analysis: "清除 SCADA 分析", highlight_features: "地图高亮", clear_highlights: "清除高亮", open_chart: "打开图表", close_chart: "关闭图表", set_overlay: "显示覆盖层", clear_overlay: "清除覆盖层" }; const references = z.array(z.string().trim().min(1).max(128)).max(100); const command = presentationCommandSchema; const presentationRecord = z.object({ record_type: z.literal("presentation"), command }).strict(); const uiRecord = z .object({ record_type: z.literal("ui_block"), block: z.unknown(), evidence_refs: references.optional() }) .strict(); const dataRecord = z .object({ record_type: z.string().regex(/^[a-z][a-z0-9_]{0,63}$/), origin: z.enum(["system_data", "synthetic", "derived", "human_input"]), source_evidence_refs: references }) .passthrough(); /** Only explicit presentation records execute. Skill data records remain data. */ export function parseInvestigationDevJsonl(input: string): InvestigationDevPreview { if (new TextEncoder().encode(input).byteLength > 200_000) throw new Error("JSONL 超过 200 KB,请缩小数据范围。"); const lines = input .replace(/^\uFEFF/, "") .split(/\r?\n/) .map((text, index) => ({ text: text.trim(), line: index + 1 })) .filter((item) => item.text); if (!lines.length) throw new Error("请先载入样例、当前报告,或粘贴 JSONL。"); if (lines.length > 40) throw new Error("一次最多校验 40 行,请分批预览。"); const result: InvestigationDevPreview = { commands: [], blocks: [], records: [] }; const blockIds = new Set(); for (const { text, line } of lines) { let value: unknown; try { value = JSON.parse(text); } catch { throw new Error(`第 ${line} 行不是合法 JSON。`); } if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`第 ${line} 行必须是 JSON 对象。`); const record = value as Record; if (record.record_type === "presentation") { const parsed = presentationRecord.safeParse(value); if (!parsed.success) throw new Error(`第 ${line} 行展示指令不合法,请检查动作、参数和范围。`); const data = parsed.data; result.commands.push(data.command); } else if (record.record_type === "ui_block") { const wrapped = uiRecord.safeParse(value); const block = parseAnswerUiBlock(wrapped.success ? wrapped.data.block : null); if (!block || blockIds.has(block.block_id)) throw new Error(`第 ${line} 行界面块不合法或 block_id 重复。`); blockIds.add(block.block_id); result.blocks.push(block); if (result.blocks.length > 8) throw new Error("一次最多预览 8 个界面块。"); } else { const parsed = dataRecord.safeParse(value); if (!parsed.success) throw new Error(`第 ${line} 行不是已支持的展示指令、界面块或带来源的调查数据。`); result.records.push(parsed.data); } } return result; } /** Explicit records are the only export format; this never interprets legacy inputs. */ export function createPresentationRecord(command: InvestigationPresentationCommand) { return presentationRecord.parse({ record_type: "presentation", command }); } export function createUiRecord(block: AnswerUiBlock, evidenceRefs: string[] = []) { const normalized = parseAnswerUiBlock({ ...block, phase: block.phase ?? "final" }); if (!normalized) throw new Error("界面块未通过当前组件协议校验。"); return uiRecord.parse({ record_type: "ui_block", block: normalized, evidence_refs: evidenceRefs }); } export function serializeInvestigationDevJsonl(records: unknown[]) { const input = records.map((record) => JSON.stringify(record)).join("\n"); parseInvestigationDevJsonl(input); return input; } export function investigationReportJsonl(task: OperationalTask) { return (task.latest_report?.report.ui_blocks ?? []) .map((item) => JSON.stringify({ record_type: "ui_block", block: item.block, evidence_refs: item.evidence_refs }) ) .join("\n"); } export function investigationReportUiSummary(task: OperationalTask | null) { const counts = { blocks: 0, charts: 0, tables: 0, maps: 0, invalid: 0 }; for (const item of task?.latest_report?.report.ui_blocks ?? []) { const block = parseAnswerUiBlock(item.block); if (!block) { counts.invalid += 1; continue; } counts.blocks += 1; for (const node of Object.values(block.spec.elements)) { if (node.type === "Chart") counts.charts += 1; if (node.type === "Table") counts.tables += 1; if (node.type === "MapLocation") counts.maps += 1; } } return counts; } export function createInvestigationDevSample(): string { const center: [number, number] = [...INVESTIGATION_DEV_NETWORK.center]; const chart = { title: "本地合成压力趋势", chart_type: "line" as const, x_data: ["08:00", "08:15", "08:30"], series: [{ name: "压力水头(m)", data: [32, 28, 30] }] }; const block: AnswerUiBlock = { version: "answer-ui@1", block_id: "dev-investigation-summary", fallback_text: "仅供展示测试的合成候选对比,不是调查成果。", spec: { root: "summary", elements: { summary: { type: "Stack", props: { columns: "one", gap: "compact" }, children: ["notice", "candidates"] }, notice: { type: "Text", props: { text: "本地合成样例,不代表真实故障、阀门或现场结果。", tone: "warning" }, children: [] }, candidates: { type: "Table", props: { title: "合成候选对比", columns: ["候选", "排序", "说明"], rows: [ [INVESTIGATION_DEV_NETWORK.pipeIds[0], "1", "仅测试表格展示"], [INVESTIGATION_DEV_NETWORK.pipeIds[1], "2", "未执行定位分析"] ] }, children: [] } } } }; const records: unknown[] = []; records.push( createPresentationRecord({ action: "set_overlay", overlay_id: "dev-synthetic-area", title: "本地合成空间样例", mode: "geometry", evidence_id: "dev:synthetic", data: { type: "FeatureCollection", features: [ { type: "Feature", properties: { project_code: INVESTIGATION_DEV_NETWORK.projectCode, origin: "synthetic" }, geometry: { type: "Point", coordinates: center } } ] } }), createPresentationRecord({ action: "open_chart", chart_id: "dev-synthetic-pressure", chart, evidence_refs: ["dev:synthetic"] }), createUiRecord(block), { record_type: "candidate", origin: "synthetic", source_evidence_refs: [], element_id: INVESTIGATION_DEV_NETWORK.pipeIds[0], rank: 1, status: "example_only" } ); return serializeInvestigationDevJsonl(records); }