feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
@@ -0,0 +1,246 @@
import { z } from "zod";
import {
parseAnswerUiBlock,
presentationCommandSchema,
type AnswerUiBlock
} from "@/features/agent";
import { type InvestigationPresentationCommand } from "../api/investigation-presentation";
import type { InvestigationMapTarget } from "../api/investigation-report";
import type { OperationalTask } from "../api/operational-task-client";
export type InvestigationDevPreview = {
commands: InvestigationPresentationCommand[];
blocks: AnswerUiBlock[];
records: Array<Record<string, unknown>>;
};
export type InvestigationPresentationActivity = {
id: number;
action: InvestigationPresentationCommand["action"];
source: "live" | "preview" | "chat";
status: "running" | "completed" | "error";
message?: string;
};
export const investigationActionLabels: Record<InvestigationPresentationCommand["action"], string> =
{
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<string>();
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<string, unknown>;
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 normalizeDevCenter([longitude, latitude]: [number, number]): [number, number] {
if (!Number.isFinite(longitude) || !Number.isFinite(latitude)) throw new Error("地图中心坐标必须为有限数值。");
return [((((longitude + 180) % 360) + 360) % 360) - 180, Math.max(-89.99, Math.min(89.99, latitude))];
}
export function createInvestigationDevSample(
center: [number, number],
target?: InvestigationMapTarget
): string {
center = normalizeDevCenter(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: [
["DEMO-PIPE-01", "1", "仅测试表格展示"],
["DEMO-PIPE-02", "2", "未执行定位分析"]
]
},
children: []
}
}
}
};
const records: unknown[] = [];
if (target)
records.push(
createPresentationRecord({
action: "highlight_features",
layer: target.source_id,
feature_ids: [target.feature_id],
effect: "pulse"
})
);
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: { 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: "DEMO-PIPE-01",
rank: 1,
status: "example_only"
}
);
return serializeInvestigationDevJsonl(records);
}