Files
next-tjwater-frontend/src/features/workbench/api/investigation-report.ts
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

109 lines
4.1 KiB
TypeScript

import { z } from "zod";
import type { AgentTaskDiscussionContext, AnswerUiSelection } from "@/features/agent";
import { reportUiBlocksSchema } from "@/features/agent";
export type InvestigationDiscussion = {
prompt?: string;
context?: AgentTaskDiscussionContext;
// Display metadata from the selected report, never the task's current round.
reportGeneration?: number | null;
reference?: AnswerUiSelection;
};
const text = z.string().trim().min(1);
export const investigationMapTargetSchema = z.object({
source_id: z.enum(["junctions", "pipes", "valves", "pumps", "tanks", "reservoirs", "scada"]),
feature_id: text.max(128)
}).strict();
export const investigationDiagramSchema = z
.object({
title: text,
mermaid: text
.max(20000)
.refine(
(source) =>
/^\s*(?:flowchart|graph)\s+(?:TD|TB|BT|LR|RL)\b/.test(source) &&
!source.includes("```") &&
!source.includes("%%{") &&
!/(?:^|[;\n])\s*click\b/.test(source),
"调查图只接受不含指令或回调的 Mermaid 流程描述"
),
node_refs: z
.array(
z
.object({
node_id: z.string().regex(/^[A-Za-z_][A-Za-z0-9_-]{0,79}$/),
label: text,
status: z.enum(["pending", "running", "completed", "waiting_human", "failed", "skipped", "cancelled"]).nullish(),
map_targets: z.array(investigationMapTargetSchema).max(100).optional(),
evidence_refs: z.array(text).default([]),
action_refs: z.array(z.string().uuid()).default([])
})
.strict()
)
.max(100)
.default([])
})
.strict()
.superRefine((diagram, ctx) => {
if (new Set(diagram.node_refs.map((node) => node.node_id)).size !== diagram.node_refs.length) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "调查图节点引用重复" });
}
});
const dataGapSchema = z
.object({
gap_id: text,
node_id: text.nullish(),
missing_data: text,
scope: text,
impact: text,
required_data: text,
evidence_refs: z.array(text).min(1),
blocking: z.boolean()
})
.strict();
/** Historical omissions remain unknown. Never infer outcomes from report prose. */
export const investigationReportSchema = z
.object({
title: text,
current_conclusion: text,
conclusion_boundary: text,
evidence_refs: z.array(text),
recommendations: z.array(text),
pending_items: z.array(text),
stage: z.enum(["interim", "final"]).nullish(),
outcome: z.enum(["concluded", "insufficient_data"]).nullish(),
stop_reason: text.nullish(),
data_gaps: z.array(dataGapSchema).nullish(),
diagram: investigationDiagramSchema.nullish(),
ui_blocks: reportUiBlocksSchema.optional()
})
.strict()
.superRefine((report, ctx) => {
const invalid = (message: string) => ctx.addIssue({ code: z.ZodIssueCode.custom, message });
if (report.outcome == null) {
if (report.stage != null || report.stop_reason != null || report.data_gaps != null)
invalid("报告结构化字段必须一起提供");
return;
}
if (report.stage == null || report.data_gaps == null) {
invalid("报告缺少阶段或数据缺口清单");
return;
}
if (new Set(report.data_gaps.map((gap) => gap.gap_id)).size !== report.data_gaps.length)
invalid("数据缺口编号重复");
const blocking = report.data_gaps.some((gap) => gap.blocking);
if (report.outcome === "insufficient_data") {
if (report.stage !== "final" || !report.stop_reason || !blocking)
invalid("证据不足必须提供最终报告、结束原因和关键缺口");
if (report.recommendations.length || report.pending_items.length)
invalid("证据不足退出后不再生成后续处置建议或待办");
} else if (report.stop_reason != null || blocking)
invalid("已形成结论不能包含结束原因或阻塞缺口");
});
export type InvestigationReport = z.infer<typeof investigationReportSchema>;
export type InvestigationDiagram = z.infer<typeof investigationDiagramSchema>;
export type InvestigationMapTarget = z.infer<typeof investigationMapTargetSchema>;