diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index 047486e..2b190ee 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -4,7 +4,7 @@ mode: primary model: deepseek/deepseek-v4-flash temperature: 0.2 --- -你是 TJWater 实验性供水管网分析 Agent。回复用户时使用简体中文,内容简洁准确。 +你是 TJWater 实验性 SCADA 数据 Agent。回复用户时使用简体中文,内容简洁准确。 ## 核心定位 @@ -15,9 +15,9 @@ Agent 负责: 1. 理解用户意图。 2. 调用受控工具获取有限上下文或表达展示意图。 3. 输出文字摘要、结论、追问和建议。 -4. 通过前端展示工具表达 UI 意图。 +4. 通过受控地图工具表达定位意图。 -服务端负责把前端展示工具转换成 `agent-ui@1` 的 UIEnvelope;前端只渲染可信组件、图表和地图动作。 +服务端负责把受控地图工具转换成 `agent-ui@1` 的 UIEnvelope;前端只执行可信地图动作。 ## 工具选择 @@ -25,27 +25,19 @@ Agent 负责: |------|------| | 查询实时公开网页信息 | `web_search` | | 地址/地点转经纬度 | `geocode` | -| 地图定位 | `locate_features`、`zoom_to_map` | -| 图表展示 | `show_chart` | -| 面板展示 | `view_history`、`view_scada` | -| 地图渲染/样式 | `render_junctions`、`apply_layer_style` | -| 持久化大渲染数据 | 先准备 `{ node_area_map }` JSON 文件,再用 `store_render_ref` 存为受控 `render_ref`,最后调用 `render_junctions` | +| 地图定位 | `zoom_to_map` | | 历史会话检索 | `session_search` | | 用户偏好和项目事实 | `memory_manager` | | 可复用流程沉淀 | `skill_manager` | | 项目背景 | `get_project_context` | -| 监测设备/点位 | `list_monitoring_assets` | -| 监测时序读数 | `query_monitoring_readings`(必须给出 `observed_from`、`observed_to`) | -| 数据质量分析 | `start_data_quality_analysis`、`get_job_status` | -| SWMM 仿真 | `start_simulation`、`get_job_status` | +| SCADA 资产 | `list_scada_assets` | +| SCADA 时序读数 | `query_scada_readings`(必须给出 `observed_from`、`observed_to`) | ## UI 约束 -1. 前端工具仅表达展示意图,不返回业务数据。 +1. 前端地图工具仅表达定位意图,不返回业务数据。 2. 每次工具调用必须填写具体 `reason`。 -3. 图表使用 `show_chart`,折线图/柱状图必须用 `x_data` 作为横轴标签,`series[].data` 作为同长度数值数组。 -4. 大型 junction 渲染必须使用 `render_ref`,不要直接把完整 payload 传给前端。 -5. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。 +3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。 ## 执行约束 @@ -66,13 +58,10 @@ Agent 负责: 仅通过领域工具访问 Server;不得构造 URL、HTTP header、SQL、命令或项目 ID。 - 项目背景:`get_project_context` -- 设备/监测点:`list_monitoring_assets` -- 时序读数:`query_monitoring_readings` -- 数据质量任务:`start_data_quality_analysis`(写操作,必须等待审批) -- 仿真任务:`start_simulation`(写操作,必须等待审批) -- 任务状态:`get_job_status` +- SCADA 资产:`list_scada_assets` +- SCADA 时序读数:`query_scada_readings` -查询历史监测数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_monitoring_readings`,传入 `observed_from`、`observed_to`、`granularity`、`metrics` 和设备 ID,再调用 `show_chart` 或 `view_history` 展示。`view_history` 只负责打开前端历史面板,也必须传入 `start_time`、`end_time`。 -先查询数据再作判断。回答必须说明查询时间范围、粒度、指标,以及已创建任务的 ID 和状态。 +查询历史 SCADA 数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_scada_readings`,传入 `observed_from`、`observed_to`、`granularity`、`metrics` 和 `asset_ids`,再用文字摘要说明结果。 +先查询数据再作判断。回答必须说明查询时间范围、粒度和指标。 遇到空数据、拒绝、权限错误、超时或服务不可用时,明确说明事实,不得补全或猜测结果。 工具返回 `result_ref` 时,说明完整结果已安全存储,并使用 preview 中的样本和统计作答。 diff --git a/.opencode/plugins/frontend-action-call-id.ts b/.opencode/plugins/frontend-action-call-id.ts index 6a9f5c3..46b4048 100644 --- a/.opencode/plugins/frontend-action-call-id.ts +++ b/.opencode/plugins/frontend-action-call-id.ts @@ -1,6 +1,6 @@ import type { Plugin } from "@opencode-ai/plugin"; -export const FRONTEND_ACTION_TOOLS = new Set(["zoom_to_map", "locate_features", "view_history", "view_scada"]); +export const FRONTEND_ACTION_TOOLS = new Set(["zoom_to_map"]); export const frontendActionCallIdPlugin: Plugin = async () => ({ "tool.execute.before": async (input, output) => { diff --git a/.opencode/tools/apply_layer_style.ts b/.opencode/tools/apply_layer_style.ts deleted file mode 100644 index 39fbd16..0000000 --- a/.opencode/tools/apply_layer_style.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; - -export default tool({ - description: - "在前端地图上对节点或管道图层应用样式,或重置为默认样式。样式参数应尽量与前端样式编辑器字段保持一致。", - args: { - reason: tool.schema - .string() - .describe( - "Why this style action is needed for the current user request.", - ), - layer_id: tool.schema - .enum(["junctions", "pipes"]) - .describe("Target layer id. Must be exactly 'junctions' or 'pipes'."), - reset_to_default: tool.schema - .boolean() - .optional() - .describe("Whether to reset the target layer to its default style."), - style_config: tool.schema - .object({ - property: tool.schema - .string() - .optional() - .describe("Data property to render."), - classification_method: tool.schema - .enum(["pretty_breaks", "custom_breaks"]) - .optional() - .describe("Classification method."), - segments: tool.schema - .number() - .optional() - .describe("Number of segments."), - min_size: tool.schema - .number() - .optional() - .describe("Minimum point radius."), - max_size: tool.schema - .number() - .optional() - .describe("Maximum point radius."), - min_stroke_width: tool.schema - .number() - .optional() - .describe("Minimum line width."), - max_stroke_width: tool.schema - .number() - .optional() - .describe("Maximum line width."), - fixed_stroke_width: tool.schema - .number() - .optional() - .describe("Fixed line width when width is not data-driven."), - color_type: tool.schema - .enum(["single", "gradient", "rainbow", "custom"]) - .optional() - .describe("Color strategy."), - single_palette_index: tool.schema.number().optional(), - gradient_palette_index: tool.schema.number().optional(), - rainbow_palette_index: tool.schema.number().optional(), - show_labels: tool.schema - .boolean() - .optional() - .describe("Whether to show labels."), - show_id: tool.schema - .boolean() - .optional() - .describe("Whether to show ids."), - opacity: tool.schema.number().optional().describe("Opacity in [0, 1]."), - adjust_width_by_property: tool.schema - .boolean() - .optional() - .describe("Whether line width is driven by the rendered property."), - custom_breaks: tool.schema - .array(tool.schema.number()) - .optional() - .describe("Custom break values."), - custom_colors: tool.schema - .array(tool.schema.string()) - .optional() - .describe("Custom rgba colors."), - }) - .optional() - .describe( - "Optional style config overrides. Omit when reset_to_default is true.", - ), - }, - async execute(args) { - const layerLabel = args.layer_id === "junctions" ? "节点" : "管道"; - if (args.reset_to_default) { - return `已将${layerLabel}图层重置为默认样式。`; - } - return `已对${layerLabel}图层应用样式。`; - }, -}); diff --git a/.opencode/tools/get_job_status.ts b/.opencode/tools/get_job_status.ts deleted file mode 100644 index c2d8569..0000000 --- a/.opencode/tools/get_job_status.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "读取分析或仿真任务的状态。", args: { job_id: tool.schema.string().uuid(), job_type: tool.schema.enum(["analysis", "simulation"]) }, execute: (args, context) => callServerApiTool("get_job_status", args, context.sessionID) }); diff --git a/.opencode/tools/list_monitoring_assets.ts b/.opencode/tools/list_monitoring_assets.ts deleted file mode 100644 index 4a6adb6..0000000 --- a/.opencode/tools/list_monitoring_assets.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "列出当前项目的监测设备和监测点。", args: { include: tool.schema.enum(["devices", "points", "all"]).default("all") }, execute: (args, context) => callServerApiTool("list_monitoring_assets", args, context.sessionID) }); diff --git a/.opencode/tools/list_scada_assets.ts b/.opencode/tools/list_scada_assets.ts new file mode 100644 index 0000000..e60a144 --- /dev/null +++ b/.opencode/tools/list_scada_assets.ts @@ -0,0 +1,10 @@ +import { tool } from "@opencode-ai/plugin"; +import { callServerApiTool } from "./server_api_shared.js"; + +export default tool({ + description: + "列出当前项目可查询的 SCADA 资产。无请求参数;返回 assets 数组,包含 assets[].scada_id、external_id、site_external_id、name、kind、active。后续查询读数时必须使用 assets[].scada_id 作为 query_scada_readings.asset_ids;不要使用 GeoServer feature id/fid。", + args: {}, + execute: (args, context) => + callServerApiTool("list_scada_assets", args, context.sessionID), +}); diff --git a/.opencode/tools/locate_features.ts b/.opencode/tools/locate_features.ts deleted file mode 100644 index 6731897..0000000 --- a/.opencode/tools/locate_features.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { executeFrontendAction } from "./frontend_action.js"; - -export default tool({ - description: "在前端地图上定位并高亮指定的管网要素。", - args: { - reason: tool.schema - .string() - .describe( - "Why this map positioning action is needed for the user request.", - ), - ids: tool.schema - .array(tool.schema.string().min(1)).min(1).max(100) - .describe("Feature ids to locate."), - feature_type: tool.schema - .enum(["junction", "pipe", "valve", "reservoir", "pump", "tank", "conduit", "orifice", "outfall"]) - .describe("Type of feature to locate."), - }, - async execute(args, context) { - return executeFrontendAction("locate_features", args, context); - }, -}); diff --git a/.opencode/tools/query_monitoring_readings.ts b/.opencode/tools/query_monitoring_readings.ts deleted file mode 100644 index 41378f5..0000000 --- a/.opencode/tools/query_monitoring_readings.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "按设备、时间范围、粒度和允许指标查询监测读数。", args: { device_ids: tool.schema.array(tool.schema.string().uuid()).min(1).max(100), observed_from: tool.schema.string().datetime(), observed_to: tool.schema.string().datetime(), granularity: tool.schema.enum(["5m", "1h", "1d"]).default("5m"), metrics: tool.schema.array(tool.schema.enum(["flow_mean", "flow_total", "conductivity_mean", "velocity_mean", "temperature_mean", "level_mean"])).min(1).max(6) }, execute: (args, context) => callServerApiTool("query_monitoring_readings", args, context.sessionID) }); diff --git a/.opencode/tools/query_scada_readings.ts b/.opencode/tools/query_scada_readings.ts new file mode 100644 index 0000000..4b2cbf8 --- /dev/null +++ b/.opencode/tools/query_scada_readings.ts @@ -0,0 +1,50 @@ +import { tool } from "@opencode-ai/plugin"; +import { callServerApiTool } from "./server_api_shared.js"; + +export default tool({ + description: + "查询一个或多个 SCADA 资产在指定时间范围内的时序读数。调用前通常先用 list_scada_assets 获取资产 id;必须给出明确的 observed_from 和 observed_to,不要省略或自行扩大时间范围。", + args: { + asset_ids: tool.schema + .array(tool.schema.string().uuid()) + .min(1) + .max(100) + .describe( + "SCADA asset UUID 数组,1 到 100 个。必须使用 list_scada_assets 返回的 assets[].scada_id,不要使用 GeoServer feature id/fid、external_id 或 site_external_id。", + ), + observed_from: tool.schema + .string() + .datetime() + .describe( + "查询开始时间,ISO8601 datetime 字符串,必须包含时区,例如 2026-04-10T07:15:00Z 或 2026-04-10T15:15:00+08:00。包含该时刻。", + ), + observed_to: tool.schema + .string() + .datetime() + .describe( + "查询结束时间,ISO8601 datetime 字符串,必须包含时区,且必须晚于 observed_from。服务端按半开区间处理:observed_at >= observed_from 且 observed_at < observed_to。", + ), + granularity: tool.schema + .enum(["5m", "1h", "1d"]) + .default("5m") + .describe( + "读数粒度:5m 为原始 5 分钟读数,1h 为小时聚合,1d 为日聚合。未指定时默认 5m。", + ), + metrics: tool.schema + .array( + tool.schema.enum([ + "conductivity_mean", + "level_mean", + "flow_mean", + "temperature_mean", + ]), + ) + .min(1) + .max(4) + .describe( + "要返回的指标数组,1 到 4 个。允许值:conductivity_mean(电导率均值)、level_mean(液位均值)、flow_mean(流量均值)、temperature_mean(温度均值)。", + ), + }, + execute: (args, context) => + callServerApiTool("query_scada_readings", args, context.sessionID), +}); diff --git a/.opencode/tools/render_junctions.ts b/.opencode/tools/render_junctions.ts deleted file mode 100644 index 49043f2..0000000 --- a/.opencode/tools/render_junctions.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; - -export default tool({ - description: - "在前端地图上对 junctions 图层应用分区渲染。使用前必须完成两步:① 准备数据结构(JSON 文件,结构为 { node_area_map: Record, area_ids?: string[], area_colors?: Record },其中 node_area_map 的 key 是 junction/node id,value 是 area id);② 调用 store_render_ref 将 JSON 文件存储到受控路径,获取 render_ref(格式为 res-...);③ 将 render_ref 传入本工具完成前端渲染。注意:不要先把 ref 内容完整读出再传给前端,也不要直接传本地文件路径。", - args: { - reason: tool.schema - .string() - .describe( - "Why this junction rendering action is needed for the user request.", - ), - render_ref: tool.schema - .string() - .describe( - "上一步通过 store_render_ref 获得的渲染引用 ID(res-...)。前端会按该引用拉取完整 payload 并渲染。不可直接传入本地文件路径或完整 JSON 数据。", - ), - }, - async execute() { - // 工具参数里只需要 render_ref;浏览器端会再用该引用回读完整 payload.data 并完成渲染。 - return "已在地图上应用节点分区渲染。"; - }, -}); diff --git a/.opencode/tools/show_chart.ts b/.opencode/tools/show_chart.ts deleted file mode 100644 index f514bd8..0000000 --- a/.opencode/tools/show_chart.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; - -export default tool({ - description: - "在前端对话界面中渲染图表。折线图/柱状图必须使用 x_data 作为横轴标签,series[].data 作为同长度的一维数值数组,不要把折线数据写成 ECharts 的 [x, y] 二维点数组。", - args: { - reason: tool.schema - .string() - .describe("Why this chart should be rendered for the user request."), - title: tool.schema.string().optional().describe("Chart title."), - chart_type: tool.schema - .enum(["line", "bar"]) - .optional() - .describe("Chart type."), - x_data: tool.schema - .array(tool.schema.string()) - .describe("X-axis labels. For line charts, put time/category labels here."), - series: tool.schema - .array( - tool.schema.object({ - name: tool.schema.string(), - data: tool.schema - .array(tool.schema.number()) - .describe("Y values only. Must align by index with x_data."), - type: tool.schema.enum(["line", "bar"]).optional(), - }), - ) - .describe("Series data."), - x_axis_name: tool.schema - .string() - .optional() - .describe("X-axis display name."), - y_axis_name: tool.schema - .string() - .optional() - .describe("Y-axis display name."), - }, - async execute() { - // 图表数据已经在工具参数里,前端收到 tool_call 后直接渲染,不再二次请求后端。 - return "图表将在对话中显示。"; - }, -}); diff --git a/.opencode/tools/start_data_quality_analysis.ts b/.opencode/tools/start_data_quality_analysis.ts deleted file mode 100644 index 10b49cc..0000000 --- a/.opencode/tools/start_data_quality_analysis.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "创建数据质量分析任务。此操作会创建后台任务,需要用户审批。", args: { device_ids: tool.schema.array(tool.schema.string().uuid()).min(1).max(100), observed_from: tool.schema.string().datetime(), observed_to: tool.schema.string().datetime() }, execute: (args, context) => callServerApiTool("start_data_quality_analysis", args, context.sessionID) }); diff --git a/.opencode/tools/start_simulation.ts b/.opencode/tools/start_simulation.ts deleted file mode 100644 index b24e08b..0000000 --- a/.opencode/tools/start_simulation.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "基于已注册模型版本创建仿真任务。此操作会创建后台任务,需要用户审批。", args: { model_version_id: tool.schema.string().uuid(), parameters: tool.schema.record(tool.schema.string(), tool.schema.unknown()).default({}) }, execute: (args, context) => callServerApiTool("start_simulation", args, context.sessionID) }); diff --git a/.opencode/tools/store_render_ref.ts b/.opencode/tools/store_render_ref.ts deleted file mode 100644 index e9fab74..0000000 --- a/.opencode/tools/store_render_ref.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; - -const internalBaseUrl = - process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; -const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; - -export default tool({ - description: - "将本地 JSON 渲染数据文件存储到受控路径,返回可供 render_junctions 使用的 render_ref(res-...)。前置步骤:先准备好符合 render_junctions 数据结构的 JSON 文件 { node_area_map, area_ids?, area_colors? },写入本地路径后再调用本工具传入该路径,获取 render_ref 后传给 render_junctions 完成前端渲染。", - args: { - reason: tool.schema - .string() - .describe( - "为何需要将此本地渲染数据持久化为 render_ref,以便后续通过 render_junctions 渲染到前端。", - ), - file_path: tool.schema - .string() - .describe( - "本地 JSON 文件的绝对路径,内容为 render_junctions 所需的数据结构 { node_area_map, area_ids?, area_colors? }。", - ), - }, - async execute(args, context) { - const response = await fetch( - `${internalBaseUrl}/internal/tools/store-render-ref`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, - }, - body: JSON.stringify({ - session_id: context.sessionID, - file_path: args.file_path, - }), - }, - ); - - const text = await response.text(); - if (!response.ok) { - throw new Error(text); - } - return text; - }, -}); diff --git a/.opencode/tools/view_history.ts b/.opencode/tools/view_history.ts deleted file mode 100644 index 05d5822..0000000 --- a/.opencode/tools/view_history.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { executeFrontendAction } from "./frontend_action.js"; - -export default tool({ - description: "为选定的管网要素打开前端的历史记录或计算结果面板;必须提供 ISO8601 时间区间 start_time 和 end_time。", - args: { - reason: tool.schema - .string() - .describe( - "Why this history panel should be opened for the current task.", - ), - feature_infos: tool.schema - .array(tool.schema.tuple([tool.schema.string(), tool.schema.string()])) - .describe("List of [id, type] pairs."), - data_type: tool.schema - .enum(["realtime", "scheme", "none"]) - .describe("History data source type."), - start_time: tool.schema - .string() - .datetime() - .describe("Required ISO8601 inclusive start time for the history query."), - end_time: tool.schema - .string() - .datetime() - .describe("Required ISO8601 exclusive end time for the history query; must be after start_time."), - }, - async execute(args, context) { - return executeFrontendAction("view_history", args, context); - }, -}); diff --git a/.opencode/tools/view_scada.ts b/.opencode/tools/view_scada.ts deleted file mode 100644 index 0bfa51c..0000000 --- a/.opencode/tools/view_scada.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { executeFrontendAction } from "./frontend_action.js"; - -export default tool({ - description: "打开前端的 SCADA 监测数据历史面板。", - args: { - reason: tool.schema - .string() - .describe("Why SCADA panel interaction is required for this request."), - device_ids: tool.schema - .array(tool.schema.string()) - .optional() - .describe("Preferred SCADA device ids."), - device_id: tool.schema - .string() - .optional() - .describe("Single SCADA device id."), - start_time: tool.schema - .string() - .optional() - .describe("Optional ISO8601 start time."), - end_time: tool.schema - .string() - .optional() - .describe("Optional ISO8601 end time."), - result_ref: tool.schema.string().optional().describe("Authorized large monitoring result reference."), - metrics: tool.schema.array(tool.schema.string()).optional().describe("Metrics represented by the result."), - granularity: tool.schema.enum(["5m", "1h", "1d"]).optional().describe("Reading aggregation granularity."), - }, - async execute(args, context) { - return executeFrontendAction("view_scada", args, context); - }, -}); diff --git a/openapi/openapi.json b/openapi/openapi.json index 7554a5d..9f035b9 100644 --- a/openapi/openapi.json +++ b/openapi/openapi.json @@ -5,13 +5,13 @@ "version": "0.1.0" }, "paths": { - "/api/v1/meta/projects": { + "/api/v1/projects": { "get": { "tags": [ "projects" ], "summary": "Projects", - "operationId": "projects_api_v1_meta_projects_get", + "operationId": "projects_api_v1_projects_get", "responses": { "200": { "description": "Successful Response", @@ -19,10 +19,10 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/ProjectSummaryResponse" + "$ref": "#/components/schemas/ProjectResponse" }, "type": "array", - "title": "Response Projects Api V1 Meta Projects Get" + "title": "Response Projects Api V1 Projects Get" } } } @@ -35,13 +35,13 @@ ] } }, - "/api/v1/meta/project": { + "/api/v1/scada/assets": { "get": { "tags": [ - "projects" + "scada" ], - "summary": "Project", - "operationId": "project_api_v1_meta_project_get", + "summary": "Assets", + "operationId": "assets_api_v1_scada_assets_get", "security": [ { "OAuth2PasswordBearer": [] @@ -72,7 +72,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProjectMetaResponse" + "$ref": "#/components/schemas/ScadaAssetsResponse" } } } @@ -90,186 +90,13 @@ } } }, - "/api/v1/meta/db/health": { - "get": { - "tags": [ - "projects" - ], - "summary": "Health", - "operationId": "health_api_v1_meta_db_health_get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DatabaseHealthResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/monitoring/devices": { - "get": { - "tags": [ - "monitoring" - ], - "summary": "Devices", - "operationId": "devices_api_v1_monitoring_devices_get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DeviceOut" - }, - "title": "Response Devices Api V1 Monitoring Devices Get" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/monitoring/points": { - "get": { - "tags": [ - "monitoring" - ], - "summary": "Points", - "operationId": "points_api_v1_monitoring_points_get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MonitoringPointOut" - }, - "title": "Response Points Api V1 Monitoring Points Get" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/monitoring/readings/query": { + "/api/v1/scada/readings/query": { "post": { "tags": [ - "monitoring" + "scada" ], "summary": "Readings", - "operationId": "readings_api_v1_monitoring_readings_query_post", + "operationId": "readings_api_v1_scada_readings_query_post", "security": [ { "OAuth2PasswordBearer": [] @@ -299,7 +126,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReadingQuery" + "$ref": "#/components/schemas/ScadaReadingQuery" } } } @@ -310,7 +137,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReadingQueryResponse" + "$ref": "#/components/schemas/ScadaReadingQueryResponse" } } } @@ -328,505 +155,18 @@ } } }, - "/api/v1/analyses": { + "/api/v1/geocode": { "post": { "tags": [ - "analyses" - ], - "summary": "Create", - "operationId": "create_api_v1_analyses_post", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "maxLength": 255 - }, - { - "type": "null" - } - ], - "title": "Idempotency-Key" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AnalysisCreate" - } - } - } - }, - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/analyses/{job_id}": { - "get": { - "tags": [ - "analyses" - ], - "summary": "Get", - "operationId": "get_api_v1_analyses__job_id__get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Job Id" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/analyses/{job_id}/cancel": { - "post": { - "tags": [ - "analyses" - ], - "summary": "Cancel", - "operationId": "cancel_api_v1_analyses__job_id__cancel_post", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Job Id" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/simulation/models": { - "post": { - "tags": [ - "simulations" - ], - "summary": "Upload", - "operationId": "upload_api_v1_simulation_models_post", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/Body_upload_api_v1_simulation_models_post" - } - } - } - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ModelUploadOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/simulations": { - "post": { - "tags": [ - "simulations" - ], - "summary": "Create", - "operationId": "create_api_v1_simulations_post", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "Idempotency-Key", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "maxLength": 255 - }, - { - "type": "null" - } - ], - "title": "Idempotency-Key" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SimulationCreate" - } - } - } - }, - "responses": { - "202": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/simulations/{job_id}": { - "get": { - "tags": [ - "simulations" - ], - "summary": "Get", - "operationId": "get_api_v1_simulations__job_id__get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Job Id" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/JobOut" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/jobs/{job_id}/events": { - "get": { - "tags": [ - "jobs" - ], - "summary": "Events", - "operationId": "events_api_v1_jobs__job_id__events_get", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "job_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Job Id" - } - }, - { - "name": "X-Project-Id", - "in": "header", - "required": false, - "schema": { - "anyOf": [ - { - "type": "string", - "format": "uuid" - }, - { - "type": "null" - } - ], - "title": "X-Project-Id" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v1/tianditu/geocode": { - "post": { - "tags": [ - "tianditu" + "geocode" ], "summary": "Geocode", - "operationId": "geocode_api_v1_tianditu_geocode_post", + "operationId": "geocode_api_v1_geocode_post", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TiandituGeocodeRequest" + "$ref": "#/components/schemas/GeocodeRequest" } } }, @@ -838,7 +178,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TiandituGeocodeResponse" + "$ref": "#/components/schemas/GeocodeResponse" } } } @@ -908,111 +248,123 @@ }, "components": { "schemas": { - "AnalysisCreate": { + "GeocodeLocation": { "properties": { - "algorithm": { - "type": "string", - "const": "data-quality", - "title": "Algorithm" + "lon": { + "type": "number", + "title": "Lon" }, - "device_ids": { - "items": { - "type": "string", - "format": "uuid" - }, - "type": "array", - "maxItems": 100, - "minItems": 1, - "title": "Device Ids" + "lat": { + "type": "number", + "title": "Lat" }, - "observed_from": { - "type": "string", - "format": "date-time", - "title": "Observed From" + "level": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Level" }, - "observed_to": { - "type": "string", - "format": "date-time", - "title": "Observed To" + "score": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Score" + }, + "keyWord": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Keyword" } }, "type": "object", "required": [ - "algorithm", - "device_ids", - "observed_from", - "observed_to" + "lon", + "lat" ], - "title": "AnalysisCreate" + "title": "GeocodeLocation" }, - "Body_upload_api_v1_simulation_models_post": { + "GeocodeRequest": { "properties": { - "name": { + "keyword": { "type": "string", "maxLength": 200, "minLength": 1, - "title": "Name" - }, - "file": { - "type": "string", - "contentMediaType": "application/octet-stream", - "title": "File" + "title": "Keyword" } }, "type": "object", "required": [ - "name", - "file" + "keyword" ], - "title": "Body_upload_api_v1_simulation_models_post" + "title": "GeocodeRequest" }, - "DatabaseHealthResponse": { + "GeocodeResponse": { "properties": { - "postgres": { - "type": "string", - "title": "Postgres" + "status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ], + "title": "Status" }, - "timescale": { - "type": "string", - "title": "Timescale" + "msg": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Msg" + }, + "location": { + "anyOf": [ + { + "$ref": "#/components/schemas/GeocodeLocation" + }, + { + "type": "null" + } + ] + }, + "searchVersion": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Searchversion" } }, "type": "object", "required": [ - "postgres", - "timescale" + "status" ], - "title": "DatabaseHealthResponse" - }, - "DeviceOut": { - "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "monitoring_point_id": { - "type": "string", - "format": "uuid", - "title": "Monitoring Point Id" - }, - "external_id": { - "type": "string", - "title": "External Id" - }, - "active": { - "type": "boolean", - "title": "Active" - } - }, - "type": "object", - "required": [ - "id", - "monitoring_point_id", - "external_id", - "active" - ], - "title": "DeviceOut" + "title": "GeocodeResponse" }, "HTTPValidationError": { "properties": { @@ -1027,59 +379,6 @@ "type": "object", "title": "HTTPValidationError" }, - "JobOut": { - "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "status": { - "type": "string", - "title": "Status" - }, - "progress": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ], - "title": "Progress" - }, - "result": { - "anyOf": [ - { - "additionalProperties": true, - "type": "object" - }, - { - "type": "null" - } - ], - "title": "Result" - }, - "error": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Error" - } - }, - "type": "object", - "required": [ - "id", - "status" - ], - "title": "JobOut" - }, "MapExtent": { "properties": { "xmin": { @@ -1108,61 +407,7 @@ ], "title": "MapExtent" }, - "ModelUploadOut": { - "properties": { - "model_id": { - "type": "string", - "format": "uuid", - "title": "Model Id" - }, - "model_version_id": { - "type": "string", - "format": "uuid", - "title": "Model Version Id" - }, - "version": { - "type": "integer", - "title": "Version" - }, - "sha256": { - "type": "string", - "title": "Sha256" - } - }, - "type": "object", - "required": [ - "model_id", - "model_version_id", - "version", - "sha256" - ], - "title": "ModelUploadOut" - }, - "MonitoringPointOut": { - "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "external_id": { - "type": "string", - "title": "External Id" - }, - "name": { - "type": "string", - "title": "Name" - } - }, - "type": "object", - "required": [ - "id", - "external_id", - "name" - ], - "title": "MonitoringPointOut" - }, - "ProjectMetaResponse": { + "ProjectResponse": { "properties": { "project_id": { "type": "string", @@ -1226,61 +471,71 @@ "gs_workspace", "map_extent" ], - "title": "ProjectMetaResponse" + "title": "ProjectResponse" }, - "ProjectSummaryResponse": { + "ScadaAssetOut": { "properties": { - "project_id": { + "scada_id": { "type": "string", "format": "uuid", - "title": "Project Id" + "title": "Scada Id" }, - "code": { + "external_id": { "type": "string", - "title": "Code" + "title": "External Id" + }, + "site_external_id": { + "type": "string", + "title": "Site External Id" }, "name": { "type": "string", "title": "Name" }, - "description": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Description" - }, - "status": { + "kind": { "type": "string", "enum": [ - "active", - "inactive" + "water_quality", + "radar_level", + "ultrasonic_flow" ], - "title": "Status" + "title": "Kind" }, - "project_role": { - "type": "string", - "title": "Project Role" + "active": { + "type": "boolean", + "title": "Active" } }, "type": "object", "required": [ - "project_id", - "code", + "scada_id", + "external_id", + "site_external_id", "name", - "description", - "status", - "project_role" + "kind", + "active" ], - "title": "ProjectSummaryResponse" + "title": "ScadaAssetOut" }, - "ReadingQuery": { + "ScadaAssetsResponse": { "properties": { - "device_ids": { + "assets": { + "items": { + "$ref": "#/components/schemas/ScadaAssetOut" + }, + "type": "array", + "title": "Assets" + } + }, + "type": "object", + "required": [ + "assets" + ], + "title": "ScadaAssetsResponse" + }, + "ScadaReadingQuery": { + "properties": { + "asset_ids": { "items": { "type": "string", "format": "uuid" @@ -1288,7 +543,7 @@ "type": "array", "maxItems": 100, "minItems": 1, - "title": "Device Ids" + "title": "Asset Ids" }, "observed_from": { "type": "string", @@ -1312,28 +567,34 @@ }, "metrics": { "items": { - "type": "string" + "type": "string", + "enum": [ + "conductivity_mean", + "level_mean", + "flow_mean", + "temperature_mean" + ] }, "type": "array", - "maxItems": 6, + "maxItems": 4, "minItems": 1, "title": "Metrics" } }, "type": "object", "required": [ - "device_ids", + "asset_ids", "observed_from", "observed_to", "metrics" ], - "title": "ReadingQuery" + "title": "ScadaReadingQuery" }, - "ReadingQueryResponse": { + "ScadaReadingQueryResponse": { "properties": { "data": { "items": { - "$ref": "#/components/schemas/ReadingRow" + "$ref": "#/components/schemas/ScadaReadingRow" }, "type": "array", "title": "Data" @@ -1349,14 +610,14 @@ "data", "meta" ], - "title": "ReadingQueryResponse" + "title": "ScadaReadingQueryResponse" }, - "ReadingRow": { + "ScadaReadingRow": { "properties": { - "device_id": { + "asset_id": { "type": "string", "format": "uuid", - "title": "Device Id" + "title": "Asset Id" }, "observed_at": { "type": "string", @@ -1380,148 +641,11 @@ }, "type": "object", "required": [ - "device_id", + "asset_id", "observed_at", "values" ], - "title": "ReadingRow" - }, - "SimulationCreate": { - "properties": { - "model_version_id": { - "type": "string", - "format": "uuid", - "title": "Model Version Id" - }, - "parameters": { - "additionalProperties": true, - "type": "object", - "title": "Parameters" - } - }, - "type": "object", - "required": [ - "model_version_id" - ], - "title": "SimulationCreate" - }, - "TiandituGeocodeLocation": { - "properties": { - "lon": { - "type": "number", - "title": "Lon" - }, - "lat": { - "type": "number", - "title": "Lat" - }, - "level": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Level" - }, - "score": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Score" - }, - "keyWord": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Keyword" - } - }, - "type": "object", - "required": [ - "lon", - "lat" - ], - "title": "TiandituGeocodeLocation" - }, - "TiandituGeocodeRequest": { - "properties": { - "keyword": { - "type": "string", - "maxLength": 200, - "minLength": 1, - "title": "Keyword" - } - }, - "type": "object", - "required": [ - "keyword" - ], - "title": "TiandituGeocodeRequest" - }, - "TiandituGeocodeResponse": { - "properties": { - "status": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ], - "title": "Status" - }, - "msg": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Msg" - }, - "location": { - "anyOf": [ - { - "$ref": "#/components/schemas/TiandituGeocodeLocation" - }, - { - "type": "null" - } - ] - }, - "searchVersion": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "title": "Searchversion" - } - }, - "type": "object", - "required": [ - "status" - ], - "title": "TiandituGeocodeResponse" + "title": "ScadaReadingRow" }, "ValidationError": { "properties": { diff --git a/src/frontendAction/registry.ts b/src/frontendAction/registry.ts index 0f694d9..3ce401a 100644 --- a/src/frontendAction/registry.ts +++ b/src/frontendAction/registry.ts @@ -3,36 +3,8 @@ import type { FrontendActionManifest } from "./types.js"; type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny }; const zoomInput = z.object({ x: z.number().finite(), y: z.number().finite(), source_crs: z.enum(["EPSG:3857", "EPSG:4326"]).optional(), zoom: z.number().finite().optional(), duration_ms: z.number().finite().nonnegative().optional() }).strict(); -const id = z.string().trim().min(1).max(256); -const isoTime = z.string().datetime({ offset: true }); -const timeRange = { - start_time: isoTime.optional(), - end_time: isoTime.optional(), -}; -const requiredTimeRange = { - start_time: isoTime, - end_time: isoTime, -}; -const locateInput = z.object({ - ids: z.array(id).min(1).max(100), - feature_type: z.enum(["junction", "pipe", "valve", "reservoir", "pump", "tank", "conduit", "orifice", "outfall"]), -}).strict(); -const historyInput = z.object({ - feature_infos: z.array(z.tuple([id, id])).min(1).max(100), - data_type: z.enum(["realtime", "scheme", "none"]), - ...requiredTimeRange, -}).strict().refine((value) => Date.parse(value.start_time) < Date.parse(value.end_time), "start_time must precede end_time"); -const scadaInput = z.object({ - device_id: id.optional(), - device_ids: z.array(id).min(1).max(100).optional(), - ...timeRange, -}).strict().refine((value) => Boolean(value.device_id) !== Boolean(value.device_ids), "provide exactly one of device_id or device_ids") - .refine((value) => !value.start_time || !value.end_time || Date.parse(value.start_time) <= Date.parse(value.end_time), "start_time must not be after end_time"); const definitions: Definition[] = [ { manifest: { id: "zoom_to_map", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["map_overlay"] }, inputSchema: zoomInput, outputSchema: z.object({}).passthrough() }, - { manifest: { id: "locate_features", version: "frontend-action@1", risk: "view", timeoutMs: 15_000, supportedSurfaces: ["map_overlay"] }, inputSchema: locateInput, outputSchema: z.object({ locatedIds: z.array(id), missingIds: z.array(id), featureType: id, bounds: z.tuple([z.number(), z.number(), z.number(), z.number()]) }).strict() }, - { manifest: { id: "view_history", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: historyInput, outputSchema: z.object({ component: z.literal("HistoryPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() }, - { manifest: { id: "view_scada", version: "frontend-action@1", risk: "view", timeoutMs: 10_000, supportedSurfaces: ["side_panel", "canvas"] }, inputSchema: scadaInput, outputSchema: z.object({ component: z.literal("ScadaPanel"), rendered: z.literal(true), itemCount: z.number().int().nonnegative() }).strict() }, ]; export const frontendActionRegistry = definitions.map((item) => item.manifest); diff --git a/src/generated/server-api.d.ts b/src/generated/server-api.d.ts index a469c10..efcabdd 100644 --- a/src/generated/server-api.d.ts +++ b/src/generated/server-api.d.ts @@ -4,7 +4,7 @@ */ export interface paths { - "/api/v1/meta/projects": { + "/api/v1/projects": { parameters: { query?: never; header?: never; @@ -12,7 +12,7 @@ export interface paths { cookie?: never; }; /** Projects */ - get: operations["projects_api_v1_meta_projects_get"]; + get: operations["projects_api_v1_projects_get"]; put?: never; post?: never; delete?: never; @@ -21,15 +21,15 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/meta/project": { + "/api/v1/scada/assets": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Project */ - get: operations["project_api_v1_meta_project_get"]; + /** Assets */ + get: operations["assets_api_v1_scada_assets_get"]; put?: never; post?: never; delete?: never; @@ -38,58 +38,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/meta/db/health": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Health */ - get: operations["health_api_v1_meta_db_health_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitoring/devices": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Devices */ - get: operations["devices_api_v1_monitoring_devices_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitoring/points": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Points */ - get: operations["points_api_v1_monitoring_points_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/monitoring/readings/query": { + "/api/v1/scada/readings/query": { parameters: { query?: never; header?: never; @@ -99,133 +48,14 @@ export interface paths { get?: never; put?: never; /** Readings */ - post: operations["readings_api_v1_monitoring_readings_query_post"]; + post: operations["readings_api_v1_scada_readings_query_post"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/api/v1/analyses": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create */ - post: operations["create_api_v1_analyses_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/analyses/{job_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get */ - get: operations["get_api_v1_analyses__job_id__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/analyses/{job_id}/cancel": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Cancel */ - post: operations["cancel_api_v1_analyses__job_id__cancel_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/simulation/models": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Upload */ - post: operations["upload_api_v1_simulation_models_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/simulations": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Create */ - post: operations["create_api_v1_simulations_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/simulations/{job_id}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get */ - get: operations["get_api_v1_simulations__job_id__get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/jobs/{job_id}/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Events */ - get: operations["events_api_v1_jobs__job_id__events_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/tianditu/geocode": { + "/api/v1/geocode": { parameters: { query?: never; header?: never; @@ -235,7 +65,7 @@ export interface paths { get?: never; put?: never; /** Geocode */ - post: operations["geocode_api_v1_tianditu_geocode_post"]; + post: operations["geocode_api_v1_geocode_post"]; delete?: never; options?: never; head?: never; @@ -280,80 +110,39 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { - /** AnalysisCreate */ - AnalysisCreate: { - /** - * Algorithm - * @constant - */ - algorithm: "data-quality"; - /** Device Ids */ - device_ids: string[]; - /** - * Observed From - * Format: date-time - */ - observed_from: string; - /** - * Observed To - * Format: date-time - */ - observed_to: string; + /** GeocodeLocation */ + GeocodeLocation: { + /** Lon */ + lon: number; + /** Lat */ + lat: number; + /** Level */ + level?: string | null; + /** Score */ + score?: number | null; + /** Keyword */ + keyWord?: string | null; }; - /** Body_upload_api_v1_simulation_models_post */ - Body_upload_api_v1_simulation_models_post: { - /** Name */ - name: string; - /** File */ - file: string; + /** GeocodeRequest */ + GeocodeRequest: { + /** Keyword */ + keyword: string; }; - /** DatabaseHealthResponse */ - DatabaseHealthResponse: { - /** Postgres */ - postgres: string; - /** Timescale */ - timescale: string; - }; - /** DeviceOut */ - DeviceOut: { - /** - * Id - * Format: uuid - */ - id: string; - /** - * Monitoring Point Id - * Format: uuid - */ - monitoring_point_id: string; - /** External Id */ - external_id: string; - /** Active */ - active: boolean; + /** GeocodeResponse */ + GeocodeResponse: { + /** Status */ + status: string | number; + /** Msg */ + msg?: string | null; + location?: components["schemas"]["GeocodeLocation"] | null; + /** Searchversion */ + searchVersion?: string | null; }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; - /** JobOut */ - JobOut: { - /** - * Id - * Format: uuid - */ - id: string; - /** Status */ - status: string; - /** Progress */ - progress?: number | null; - /** Result */ - result?: { - [key: string]: unknown; - } | null; - /** Error */ - error?: string | null; - }; /** MapExtent */ MapExtent: { /** Xmin */ @@ -365,37 +154,8 @@ export interface components { /** Ymax */ ymax: number; }; - /** ModelUploadOut */ - ModelUploadOut: { - /** - * Model Id - * Format: uuid - */ - model_id: string; - /** - * Model Version Id - * Format: uuid - */ - model_version_id: string; - /** Version */ - version: number; - /** Sha256 */ - sha256: string; - }; - /** MonitoringPointOut */ - MonitoringPointOut: { - /** - * Id - * Format: uuid - */ - id: string; - /** External Id */ - external_id: string; - /** Name */ - name: string; - }; - /** ProjectMetaResponse */ - ProjectMetaResponse: { + /** ProjectResponse */ + ProjectResponse: { /** * Project Id * Format: uuid @@ -418,31 +178,36 @@ export interface components { gs_workspace: string; map_extent: components["schemas"]["MapExtent"] | null; }; - /** ProjectSummaryResponse */ - ProjectSummaryResponse: { + /** ScadaAssetOut */ + ScadaAssetOut: { /** - * Project Id + * Scada Id * Format: uuid */ - project_id: string; - /** Code */ - code: string; + scada_id: string; + /** External Id */ + external_id: string; + /** Site External Id */ + site_external_id: string; /** Name */ name: string; - /** Description */ - description: string | null; /** - * Status + * Kind * @enum {string} */ - status: "active" | "inactive"; - /** Project Role */ - project_role: string; + kind: "water_quality" | "radar_level" | "ultrasonic_flow"; + /** Active */ + active: boolean; }; - /** ReadingQuery */ - ReadingQuery: { - /** Device Ids */ - device_ids: string[]; + /** ScadaAssetsResponse */ + ScadaAssetsResponse: { + /** Assets */ + assets: components["schemas"]["ScadaAssetOut"][]; + }; + /** ScadaReadingQuery */ + ScadaReadingQuery: { + /** Asset Ids */ + asset_ids: string[]; /** * Observed From * Format: date-time @@ -460,24 +225,24 @@ export interface components { */ granularity: "5m" | "1h" | "1d"; /** Metrics */ - metrics: string[]; + metrics: ("conductivity_mean" | "level_mean" | "flow_mean" | "temperature_mean")[]; }; - /** ReadingQueryResponse */ - ReadingQueryResponse: { + /** ScadaReadingQueryResponse */ + ScadaReadingQueryResponse: { /** Data */ - data: components["schemas"]["ReadingRow"][]; + data: components["schemas"]["ScadaReadingRow"][]; /** Meta */ meta: { [key: string]: unknown; }; }; - /** ReadingRow */ - ReadingRow: { + /** ScadaReadingRow */ + ScadaReadingRow: { /** - * Device Id + * Asset Id * Format: uuid */ - device_id: string; + asset_id: string; /** * Observed At * Format: date-time @@ -488,46 +253,6 @@ export interface components { [key: string]: number | null; }; }; - /** SimulationCreate */ - SimulationCreate: { - /** - * Model Version Id - * Format: uuid - */ - model_version_id: string; - /** Parameters */ - parameters?: { - [key: string]: unknown; - }; - }; - /** TiandituGeocodeLocation */ - TiandituGeocodeLocation: { - /** Lon */ - lon: number; - /** Lat */ - lat: number; - /** Level */ - level?: string | null; - /** Score */ - score?: number | null; - /** Keyword */ - keyWord?: string | null; - }; - /** TiandituGeocodeRequest */ - TiandituGeocodeRequest: { - /** Keyword */ - keyword: string; - }; - /** TiandituGeocodeResponse */ - TiandituGeocodeResponse: { - /** Status */ - status: string | number; - /** Msg */ - msg?: string | null; - location?: components["schemas"]["TiandituGeocodeLocation"] | null; - /** Searchversion */ - searchVersion?: string | null; - }; /** ValidationError */ ValidationError: { /** Location */ @@ -550,7 +275,7 @@ export interface components { } export type $defs = Record; export interface operations { - projects_api_v1_meta_projects_get: { + projects_api_v1_projects_get: { parameters: { query?: never; header?: never; @@ -565,12 +290,12 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectSummaryResponse"][]; + "application/json": components["schemas"]["ProjectResponse"][]; }; }; }; }; - project_api_v1_meta_project_get: { + assets_api_v1_scada_assets_get: { parameters: { query?: never; header?: { @@ -587,7 +312,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ProjectMetaResponse"]; + "application/json": components["schemas"]["ScadaAssetsResponse"]; }; }; /** @description Validation Error */ @@ -601,100 +326,7 @@ export interface operations { }; }; }; - health_api_v1_meta_db_health_get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DatabaseHealthResponse"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - devices_api_v1_monitoring_devices_get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["DeviceOut"][]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - points_api_v1_monitoring_points_get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["MonitoringPointOut"][]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - readings_api_v1_monitoring_readings_query_post: { + readings_api_v1_scada_readings_query_post: { parameters: { query?: never; header?: { @@ -705,7 +337,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["ReadingQuery"]; + "application/json": components["schemas"]["ScadaReadingQuery"]; }; }; responses: { @@ -715,7 +347,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["ReadingQueryResponse"]; + "application/json": components["schemas"]["ScadaReadingQueryResponse"]; }; }; /** @description Validation Error */ @@ -729,246 +361,7 @@ export interface operations { }; }; }; - create_api_v1_analyses_post: { - parameters: { - query?: never; - header?: { - "Idempotency-Key"?: string | null; - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["AnalysisCreate"]; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["JobOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_api_v1_analyses__job_id__get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path: { - job_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["JobOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - cancel_api_v1_analyses__job_id__cancel_post: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path: { - job_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["JobOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - upload_api_v1_simulation_models_post: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "multipart/form-data": components["schemas"]["Body_upload_api_v1_simulation_models_post"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["ModelUploadOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - create_api_v1_simulations_post: { - parameters: { - query?: never; - header?: { - "Idempotency-Key"?: string | null; - "X-Project-Id"?: string | null; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["SimulationCreate"]; - }; - }; - responses: { - /** @description Successful Response */ - 202: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["JobOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - get_api_v1_simulations__job_id__get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path: { - job_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["JobOut"]; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - events_api_v1_jobs__job_id__events_get: { - parameters: { - query?: never; - header?: { - "X-Project-Id"?: string | null; - }; - path: { - job_id: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; - geocode_api_v1_tianditu_geocode_post: { + geocode_api_v1_geocode_post: { parameters: { query?: never; header?: never; @@ -977,7 +370,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["TiandituGeocodeRequest"]; + "application/json": components["schemas"]["GeocodeRequest"]; }; }; responses: { @@ -987,7 +380,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["TiandituGeocodeResponse"]; + "application/json": components["schemas"]["GeocodeResponse"]; }; }; /** @description Validation Error */ diff --git a/src/results/resolver.ts b/src/results/resolver.ts index 7b6995a..2f20c62 100644 --- a/src/results/resolver.ts +++ b/src/results/resolver.ts @@ -1,11 +1,8 @@ -import { readJsonFile } from "../utils/fileStore.js"; import { type ResultReferenceKind, type ResultReferenceRecord, type ResultReferenceSource, type RetrievalContext, - RESULT_REFERENCE_KIND, - RESULT_REFERENCE_SOURCE, type ResultReferenceStore, } from "./store.js"; @@ -26,12 +23,6 @@ type RegisterResultReferenceInput = { traceId: string; }; -export type RenderJunctionPayload = { - node_area_map: Record; - area_ids?: string[]; - area_colors?: Record; -}; - export class ResultReferenceResolver { constructor(private readonly store: ResultReferenceStore) {} @@ -59,34 +50,6 @@ export class ResultReferenceResolver { }); } - async registerRenderPayloadFile( - filePath: string, - input: Omit, - ) { - const raw = await readJsonFile(filePath); - if (raw === null) { - throw new Error(`render payload file not found: ${filePath}`); - } - - const wrapper = normalizeRenderPayloadFile(raw, filePath); - if (!wrapper) { - throw new Error("render payload file must use the wrapped { metadata, location, data } format"); - } - - const payload = extractRenderJunctionPayload(wrapper.data); - if (!payload) { - throw new Error("render payload file does not contain a valid junction render payload"); - } - - return this.register({ - ...input, - data: payload, - kind: RESULT_REFERENCE_KIND.renderJunctionsPayload, - schemaVersion: 1, - source: RESULT_REFERENCE_SOURCE.agentGenerated, - }); - } - async getFullAuthorized( resultRef: string, context: RetrievalContext, @@ -136,83 +99,13 @@ export class ResultReferenceResolver { } } -export const extractRenderJunctionPayload = ( - value: unknown, -): RenderJunctionPayload | null => { - const candidate = unwrapReferencePayload(value); - if (!candidate || !isRecord(candidate.node_area_map)) { - return null; - } - - // 节点渲染结果只保留前端真正需要的映射字段,剔除空值并统一转为字符串。 - const nodeAreaMap = normalizeStringRecord(candidate.node_area_map); - if (Object.keys(nodeAreaMap).length === 0) { - return null; - } - - const areaIds = Array.isArray(candidate.area_ids) - ? candidate.area_ids.map((entry) => String(entry).trim()).filter(Boolean) - : undefined; - - const areaColors = isRecord(candidate.area_colors) - ? normalizeStringRecord(candidate.area_colors) - : undefined; - - return { - node_area_map: nodeAreaMap, - ...(areaIds && areaIds.length > 0 ? { area_ids: areaIds } : {}), - ...(areaColors && Object.keys(areaColors).length > 0 - ? { area_colors: areaColors } - : {}), - }; -}; - const normalizeDataForKind = ( - kind: ResultReferenceKind, + _kind: ResultReferenceKind, data: unknown, schemaVersion: number, ): unknown | null => { if (!Number.isInteger(schemaVersion) || schemaVersion < 1) { return null; } - if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) { - return extractRenderJunctionPayload(data); - } return data; }; - -const normalizeRenderPayloadFile = ( - value: unknown, - filePath: string, -): { data: unknown } | null => { - if (!isRecord(value) || !("data" in value)) { - return null; - } - if (!isRecord(value.metadata) || !isRecord(value.location)) { - return null; - } - if (value.location.file_path !== filePath) { - return null; - } - return { data: value.data }; -}; - -const unwrapReferencePayload = (value: unknown): Record | null => { - if (!isRecord(value)) { - return null; - } - if ("data" in value && value.data !== undefined && value.data !== null) { - return isRecord(value.data) ? value.data : null; - } - return value; -}; - -const normalizeStringRecord = (value: Record) => - Object.fromEntries( - Object.entries(value) - .map(([key, entry]) => [String(key), String(entry ?? "").trim()]) - .filter(([, entry]) => entry.length > 0), - ); - -const isRecord = (value: unknown): value is Record => - typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/src/results/store.ts b/src/results/store.ts index 2113a1b..d4bcd5a 100644 --- a/src/results/store.ts +++ b/src/results/store.ts @@ -16,7 +16,6 @@ export const RESULT_REF_PATTERN = /^res-[a-f0-9-]{8,64}$/; const RESULT_REF_FILE_PATTERN = /^(res-[a-f0-9-]{8,64})(?:\.json)?$/; export const RESULT_REFERENCE_KIND = { - renderJunctionsPayload: "render-junctions-payload", serverApiPayload: "server_api_payload", } as const; diff --git a/src/routes/chat.ts b/src/routes/chat.ts index b884549..d3d7f69 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -31,11 +31,6 @@ import { FrontendActionCoordinator } from "../frontendAction/coordinator.js"; import { frontendActionRegistryResponse } from "../frontendAction/registry.js"; import type { UIEnvelopePayload } from "../uiEnvelope/types.js"; import { toActorKey, toProjectKey } from "../utils/fileStore.js"; -import { - createPressureTrendDemoEnvelopePayload, - createPressureTrendDemoToolCall, - isPressureTrendDemoPrompt, -} from "./pressureTrendDemo.js"; import { buildForkedSessionUiState, buildPromptWithLearningContext, @@ -1059,39 +1054,6 @@ export const buildChatRouter = ( : undefined; try { - if (isPressureTrendDemoPrompt(promptText)) { - const toolCall = createPressureTrendDemoToolCall(); - publish("progress", { - session_id: clientSessionId, - id: "pressure-trend-demo", - phase: "tool", - status: "running", - title: "生成压力趋势图", - detail: "已命中受控演示用例,正在通过 show_chart 生成可信 UIEnvelope。", - started_at: Date.now(), - elapsed_ms: 0, - }); - publish("tool_call", { - session_id: clientSessionId, - tool: toolCall.tool, - params: toolCall.params, - reason: toolCall.reason, - }); - publish( - "ui_envelope", - createPressureTrendDemoEnvelopePayload(clientSessionId, toolCall), - ); - publish("token", { - session_id: clientSessionId, - content: "已生成最近压力趋势图,并通过可信 UIEnvelope 发送到前端。", - }); - publish("done", { - session_id: clientSessionId, - duration_ms: 0, - }); - return; - } - const preparedMessage = await buildPromptWithLearningContext( memoryStore, requestContext.actorKey, diff --git a/src/routes/chatAuxiliaryRoutes.ts b/src/routes/chatAuxiliaryRoutes.ts index c3a91c0..0bebe4c 100644 --- a/src/routes/chatAuxiliaryRoutes.ts +++ b/src/routes/chatAuxiliaryRoutes.ts @@ -48,43 +48,6 @@ export const registerChatAuxiliaryRoutes = ( frontendActionCoordinator, }: RegisterAuxiliaryRoutesOptions, ) => { - chatRouter.get("/render-ref/:render_ref", async (req, res) => { - const renderRef = req.params.render_ref?.trim(); - const authContext = getLocalAgentContext(req); - const userId = authContext.userId; - const projectId = authContext.projectId; - const clientSessionId = - typeof req.query.session_id === "string" - ? req.query.session_id.trim() - : undefined; - - if (!renderRef) { - res.status(400).json({ - message: "render_ref is required", - }); - return; - } - - const result = await resultReferenceResolver.getFullAuthorized( - renderRef, - { - actorKey: toActorKey(userId), - clientSessionId, - projectId, - }, - { - expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload, - }, - ); - - if (!result) { - res.status(404).json({ message: "render_ref not found" }); - return; - } - - res.json(result); - }); - chatRouter.get("/result-ref/:result_ref", async (req, res) => { const resultRef = req.params.result_ref?.trim(); const context = getLocalAgentContext(req); diff --git a/src/routes/chatSession.ts b/src/routes/chatSession.ts index 834e504..e602faf 100644 --- a/src/routes/chatSession.ts +++ b/src/routes/chatSession.ts @@ -4,7 +4,6 @@ import { MemoryStore } from "../memory/store.js"; import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js"; import { collectTextContent } from "./chatStream.js"; -import { isPressureTrendDemoPrompt } from "./pressureTrendDemo.js"; const TITLE_PROMPT_TIMEOUT_MS = 5000; const TITLE_CONTEXT_MESSAGE_LIMIT = 40; @@ -241,9 +240,6 @@ export const resolveRequestedStreamSessionId = (input: { if (sessionId) { return sessionId; } - if (isPressureTrendDemoPrompt(input.promptText)) { - return input.createClientSessionId(); - } return undefined; }; diff --git a/src/routes/chatStream.ts b/src/routes/chatStream.ts index 662475d..55e1a46 100644 --- a/src/routes/chatStream.ts +++ b/src/routes/chatStream.ts @@ -830,7 +830,7 @@ export const streamPromptResponse = async ({ params: toolParams, reason, }); - if (envelope && !(suppressLegacyFrontendActions && ["zoom_to_map", "locate_features", "view_history", "view_scada"].includes(part.tool))) { + if (envelope && !(suppressLegacyFrontendActions && part.tool === "zoom_to_map")) { write("ui_envelope", { session_id: clientSessionId, envelope_id: createEnvelopeId(part.tool), diff --git a/src/routes/chatStreamEvents.ts b/src/routes/chatStreamEvents.ts index ecb44d1..62fe7bf 100644 --- a/src/routes/chatStreamEvents.ts +++ b/src/routes/chatStreamEvents.ts @@ -65,12 +65,7 @@ const toolLabels: Record = { session_search: "历史会话检索", skill_manager: "流程沉淀", web_search: "网页搜索", - locate_features: "地图定位", zoom_to_map: "地图缩放", - view_history: "历史数据面板", - view_scada: "SCADA 面板", - show_chart: "图表渲染", - render_junctions: "节点渲染", }; export const logDevelopmentDebug = ( diff --git a/src/routes/chatUiState.ts b/src/routes/chatUiState.ts index ea11709..ed19847 100644 --- a/src/routes/chatUiState.ts +++ b/src/routes/chatUiState.ts @@ -207,17 +207,9 @@ export const updateLastAssistantQuestion = ( }); const getToolArtifactKind = (tool: string): ToolArtifactKind => { - if (tool === "show_chart" || tool === "chart") return "chart"; - if ( - tool === "locate_features" || - tool === "zoom_to_map" || - tool === "render_junctions" || - tool === "apply_layer_style" || - tool.startsWith("locate_") - ) { + if (tool === "zoom_to_map") { return "map"; } - if (tool === "view_history" || tool === "view_scada") return "panel"; return "tool"; }; @@ -225,13 +217,7 @@ const getToolArtifactTitle = (tool: string, params: Record) => if (typeof params.title === "string" && params.title.trim()) { return params.title.trim(); } - if (tool === "show_chart" || tool === "chart") return "生成图表"; if (tool === "zoom_to_map") return "缩放到地图坐标"; - if (tool === "render_junctions") return "渲染节点分区"; - if (tool === "view_history") return "打开计算结果曲线"; - if (tool === "view_scada") return "打开 SCADA 数据面板"; - if (tool === "apply_layer_style") return "应用图层样式"; - if (tool === "locate_features" || tool.startsWith("locate_")) return "地图定位"; return tool || "工具调用"; }; diff --git a/src/routes/pressureTrendDemo.ts b/src/routes/pressureTrendDemo.ts deleted file mode 100644 index 346b820..0000000 --- a/src/routes/pressureTrendDemo.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createEnvelopeId } from "../uiEnvelope/ids.js"; -import { toUiEnvelopeFromToolCall } from "../uiEnvelope/fromToolCall.js"; -import type { UIEnvelopePayload } from "../uiEnvelope/types.js"; - -export type PressureTrendDemoToolCall = { - tool: "show_chart"; - params: { - reason: string; - title: string; - chart_type: "line"; - x_axis_name: string; - y_axis_name: string; - x_data: string[]; - series: Array<{ - name: string; - type: "line"; - data: number[]; - }>; - }; - reason: string; -}; - -export const isPressureTrendDemoPrompt = (message: string) => - message.replace(/\s+/g, "").includes("展示最近压力趋势"); - -export const createPressureTrendDemoToolCall = (): PressureTrendDemoToolCall => ({ - tool: "show_chart", - reason: "展示最近 6 小时压力趋势。", - params: { - reason: "展示最近 6 小时压力趋势。", - title: "最近压力趋势", - chart_type: "line", - x_axis_name: "时间", - y_axis_name: "压力 MPa", - x_data: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"], - series: [ - { - name: "东部主干压力", - type: "line", - data: [0.42, 0.41, 0.39, 0.36, 0.35, 0.37], - }, - { - name: "正常基线", - type: "line", - data: [0.43, 0.43, 0.42, 0.42, 0.41, 0.41], - }, - ], - }, -}); - -export const createPressureTrendDemoEnvelopePayload = ( - sessionId: string, - toolCall: PressureTrendDemoToolCall = createPressureTrendDemoToolCall(), -): UIEnvelopePayload => { - const envelope = toUiEnvelopeFromToolCall(toolCall); - if (!envelope) { - throw new Error("pressure trend demo tool call did not produce a UIEnvelope"); - } - - return { - session_id: sessionId, - envelope_id: createEnvelopeId(toolCall.tool), - created_at: Date.now(), - envelope, - }; -}; diff --git a/src/server.ts b/src/server.ts index 902d373..2c3c3da 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,10 +11,7 @@ import { logger } from "./logger.js"; import { LearningOrchestrator } from "./learning/orchestrator.js"; import { MemoryStore } from "./memory/store.js"; import { ResultReferenceResolver } from "./results/resolver.js"; -import { - RESULT_REFERENCE_SOURCE, - ResultReferenceStore, -} from "./results/store.js"; +import { ResultReferenceStore } from "./results/store.js"; import { buildChatRouter } from "./routes/chat.js"; import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js"; import { opencodeRuntime } from "./runtime/opencode.js"; @@ -44,7 +41,6 @@ const serverApiGateway = new ServerApiGateway(resultReferenceResolver); const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID(); const frontendActionCoordinator = new FrontendActionCoordinator(); -// 这个 token 只用于仍需服务端上下文的工具桥(store_render_ref)。 process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken; app.use(cors()); @@ -124,56 +120,6 @@ app.get("/health", async (_req, res) => { } }); -app.post("/internal/tools/store-render-ref", async (req, res) => { - if (req.header("x-agent-internal-token") !== internalToken) { - res.status(403).json({ message: "forbidden" }); - return; - } - - const sessionId = - typeof req.body?.session_id === "string" ? req.body.session_id.trim() : ""; - const filePath = typeof req.body?.file_path === "string" ? req.body.file_path.trim() : ""; - const context = sessionId ? getRuntimeSessionContext(sessionId) : null; - if (!context) { - res.status(404).json({ - message: "session context not found", - detail: sessionId, - }); - return; - } - if (!filePath) { - res.status(400).json({ message: "file_path is required" }); - return; - } - - try { - const record = await resultReferenceResolver.registerRenderPayloadFile(filePath, { - actorKey: context.actorKey, - clientSessionId: context.clientSessionId, - projectId: context.projectId, - projectKey: context.projectKey, - sessionId: context.clientSessionId, - source: RESULT_REFERENCE_SOURCE.agentGenerated, - traceId: context.traceId, - }); - res.json({ - ok: true, - render_ref: record.resultRef, - stored_at: record.createdAt, - preview: record.preview, - kind: record.kind, - schema_version: record.schemaVersion, - source: record.source, - }); - } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - res.status(400).json({ - message: "store render ref failed", - detail, - }); - } -}); - app.post("/internal/tools/session-search", async (req, res) => { if (req.header("x-agent-internal-token") !== internalToken) { res.status(403).json({ message: "forbidden" }); @@ -340,7 +286,7 @@ app.post("/internal/tools/geocode", async (req, res) => { try { const response = await callBackendJson( - "/api/v1/tianditu/geocode", + "/api/v1/geocode", context, { keyword }, ); diff --git a/src/serverApi/gateway.ts b/src/serverApi/gateway.ts index 7cbfe43..b4b76cd 100644 --- a/src/serverApi/gateway.ts +++ b/src/serverApi/gateway.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { z } from "zod"; import { config } from "../config.js"; @@ -21,11 +20,8 @@ const responseSchemas: Record = { gs_workspace: z.string(), map_extent: z.object({ xmin: z.number(), ymin: z.number(), xmax: z.number(), ymax: z.number() }).nullable(), }), - list_monitoring_assets: z.object({ devices: z.array(z.unknown()).optional(), points: z.array(z.unknown()).optional() }), - query_monitoring_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }), - start_data_quality_analysis: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(), - start_simulation: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(), - get_job_status: z.object({ id: z.string().uuid(), status: z.string() }).passthrough(), + list_scada_assets: z.object({ assets: z.array(z.unknown()) }), + query_scada_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }), }; export class ServerApiGateway { @@ -62,33 +58,32 @@ export class ServerApiGateway { } private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise { - if (name === "list_monitoring_assets") { - const result: Record = {}; - if (args.include !== "points") result.devices = await this.request("GET", "/api/v1/monitoring/devices", undefined, context); - if (args.include !== "devices") result.points = await this.request("GET", "/api/v1/monitoring/points", undefined, context); - return result; + if (name === "get_project_context") { + const projects = await this.request("GET", "/api/v1/projects", undefined, context); + if (!Array.isArray(projects)) throw new GatewayHttpError(502); + const project = projects.find((item) => ( + typeof item === "object" + && item !== null + && "project_id" in item + && item.project_id === context.projectId + )); + if (!project) throw new GatewayHttpError(404); + return project; } - const routes = { - get_project_context: ["GET", "/api/v1/meta/project"], - query_monitoring_readings: ["POST", "/api/v1/monitoring/readings/query"], - start_data_quality_analysis: ["POST", "/api/v1/analyses"], - start_simulation: ["POST", "/api/v1/simulations"], - get_job_status: ["GET", args.job_type === "analysis" ? `/api/v1/analyses/${args.job_id}` : `/api/v1/simulations/${args.job_id}`], - } as const; - const [method, path] = routes[name]; - const body = name === "start_data_quality_analysis" ? { ...args, algorithm: "data-quality" } : method === "POST" ? args : undefined; - const idempotencyKey = name.startsWith("start_") ? createIdempotencyKey(context, name, body) : undefined; - return this.request(method, path, body, context, idempotencyKey); + if (name === "list_scada_assets") { + return this.request("GET", "/api/v1/scada/assets", undefined, context); + } + return this.request("POST", "/api/v1/scada/readings/query", args, context); } - private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext, idempotencyKey?: string) { + private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS); try { const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), { method, signal: controller.signal, headers: { Accept: "application/json", "Content-Type": "application/json", "X-Project-Id": context.projectId!, - "X-Request-Id": context.traceId, ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}), + "X-Request-Id": context.traceId, }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); if (!response.ok) throw new GatewayHttpError(response.status); @@ -99,5 +94,3 @@ export class ServerApiGateway { class GatewayHttpError extends Error { constructor(readonly status: number) { super("upstream request failed"); } } const failure = (code: GatewayErrorCode, message: string, detail?: unknown) => ({ ok: false, error: { code, message, ...(detail ? { detail } : {}) } }); -export const createIdempotencyKey = (context: RuntimeSessionContext, name: string, args: unknown) => createHash("sha256").update(JSON.stringify([context.clientSessionId, name, canonicalize(args)])).digest("hex"); -const canonicalize = (value: unknown): unknown => Array.isArray(value) ? value.map(canonicalize) : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, canonicalize(entry)])) : value; diff --git a/src/serverApi/schemas.ts b/src/serverApi/schemas.ts index 0cc3fef..94973d2 100644 --- a/src/serverApi/schemas.ts +++ b/src/serverApi/schemas.ts @@ -3,31 +3,21 @@ import { z } from "zod"; const uuid = z.string().uuid(); const dateTime = z.string().datetime({ offset: true }); const metrics = z.enum([ - "flow_mean", "flow_total", "conductivity_mean", "velocity_mean", - "temperature_mean", "level_mean", + "conductivity_mean", "level_mean", "flow_mean", "temperature_mean", ]); export const domainToolSchemas = { get_project_context: z.object({}), - list_monitoring_assets: z.object({ include: z.enum(["devices", "points", "all"]).default("all") }), - query_monitoring_readings: z.object({ - device_ids: z.array(uuid).min(1).max(100), + list_scada_assets: z.object({}), + query_scada_readings: z.object({ + asset_ids: z.array(uuid).min(1).max(100), observed_from: dateTime, observed_to: dateTime, granularity: z.enum(["5m", "1h", "1d"]).default("5m"), - metrics: z.array(metrics).min(1).max(6), + metrics: z.array(metrics).min(1).max(4), }).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), { message: "observed_from must precede observed_to", }), - start_data_quality_analysis: z.object({ - device_ids: z.array(uuid).min(1).max(100), - observed_from: dateTime, - observed_to: dateTime, - }).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), { - message: "observed_from must precede observed_to", - }), - start_simulation: z.object({ model_version_id: uuid, parameters: z.record(z.unknown()).default({}) }), - get_job_status: z.object({ job_id: uuid, job_type: z.enum(["analysis", "simulation"]) }), } as const; export type DomainToolName = keyof typeof domainToolSchemas; diff --git a/src/uiEnvelope/fromToolCall.ts b/src/uiEnvelope/fromToolCall.ts index 0448309..35eef1c 100644 --- a/src/uiEnvelope/fromToolCall.ts +++ b/src/uiEnvelope/fromToolCall.ts @@ -1,5 +1,3 @@ -import { RESULT_REF_PATTERN } from "../results/store.js"; -import { chartGrammar } from "./registry.js"; import type { UIEnvelope } from "./types.js"; type ToolCallInput = { @@ -8,67 +6,12 @@ type ToolCallInput = { reason?: string; }; -const isStringArray = (value: unknown): value is string[] => - Array.isArray(value) && value.every((item) => typeof item === "string"); - -const normalizeSeries = (value: unknown) => - Array.isArray(value) - ? value - .filter( - (item): item is { name: string; data: number[]; type?: "line" | "bar" } => - typeof item === "object" && - item !== null && - "name" in item && - typeof item.name === "string" && - "data" in item && - Array.isArray(item.data) && - item.data.every((entry: unknown) => typeof entry === "number"), - ) - .map((item) => ({ - name: item.name, - data: item.data, - ...(item.type === "line" || item.type === "bar" ? { type: item.type } : {}), - })) - : []; - export const toUiEnvelopeFromToolCall = ({ tool, params, reason, }: ToolCallInput): UIEnvelope | null => { - if (tool === "show_chart" || tool === "chart") { - const xData = isStringArray(params.x_data) ? params.x_data : []; - const series = normalizeSeries(params.series); - if (xData.length === 0 || series.length === 0) { - return null; - } - const chartType = params.chart_type === "bar" ? "bar" : "line"; - return { - kind: "chart", - schemaVersion: "agent-ui@1", - grammar: chartGrammar, - surface: "chat_inline", - spec: { - title: typeof params.title === "string" ? params.title : undefined, - chart_type: chartType, - x_axis_name: - typeof params.x_axis_name === "string" ? params.x_axis_name : undefined, - y_axis_name: - typeof params.y_axis_name === "string" ? params.y_axis_name : undefined, - }, - data: { - x_data: xData, - series, - }, - fallbackText: reason || "已生成图表。", - }; - } - - if ( - tool === "locate_features" || - tool === "zoom_to_map" || - tool === "apply_layer_style" - ) { + if (tool === "zoom_to_map") { return { kind: "map_action", schemaVersion: "agent-ui@1", @@ -79,43 +22,5 @@ export const toUiEnvelopeFromToolCall = ({ }; } - if (tool === "render_junctions") { - const renderRef = - typeof params.render_ref === "string" ? params.render_ref.trim() : ""; - if (!RESULT_REF_PATTERN.test(renderRef)) { - return null; - } - return { - kind: "map_action", - schemaVersion: "agent-ui@1", - action: tool, - surface: "map_overlay", - params: { render_ref: renderRef }, - fallbackText: reason || "已生成节点渲染操作。", - }; - } - - if (tool === "view_history") { - return { - kind: "registered_component", - schemaVersion: "agent-ui@1", - component: "HistoryPanel", - surface: "side_panel", - props: params, - fallbackText: reason || "已打开历史数据面板。", - }; - } - - if (tool === "view_scada") { - return { - kind: "registered_component", - schemaVersion: "agent-ui@1", - component: "ScadaPanel", - surface: "side_panel", - props: params, - fallbackText: reason || "已打开 SCADA 面板。", - }; - } - return null; }; diff --git a/src/uiEnvelope/registry.ts b/src/uiEnvelope/registry.ts index b360c9f..29b825d 100644 --- a/src/uiEnvelope/registry.ts +++ b/src/uiEnvelope/registry.ts @@ -2,51 +2,20 @@ import type { ActionManifest, ComponentManifest } from "./types.js"; export const chartGrammar = "echarts-safe-subset" as const; -export const componentRegistry: ComponentManifest[] = [ - { - id: "HistoryPanel", - version: "1.0.0", - description: "Open a historical result panel for selected network features.", - supportedSurfaces: ["side_panel", "canvas"], - }, - { - id: "ScadaPanel", - version: "1.0.0", - description: "Open a SCADA history panel for selected devices.", - supportedSurfaces: ["side_panel", "canvas"], - }, -]; +export const componentRegistry: ComponentManifest[] = []; export const actionRegistry: ActionManifest[] = [ - { - id: "locate_features", - version: "1.0.0", - description: "Locate and highlight network features on the map.", - supportedSurfaces: ["map_overlay"], - }, { id: "zoom_to_map", version: "1.0.0", description: "Zoom the map to a coordinate.", supportedSurfaces: ["map_overlay"], }, - { - id: "render_junctions", - version: "1.0.0", - description: "Render junction categories from an authorized render_ref.", - supportedSurfaces: ["map_overlay"], - }, - { - id: "apply_layer_style", - version: "1.0.0", - description: "Apply or reset a map layer style.", - supportedSurfaces: ["map_overlay"], - }, ]; export const uiRegistryResponse = { schema_version: "agent-ui-registry@1", - chart_grammars: [chartGrammar], + chart_grammars: [], components: componentRegistry, actions: actionRegistry, }; diff --git a/tests/frontendAction/coordinator.test.ts b/tests/frontendAction/coordinator.test.ts index 998c7cf..c67494f 100644 --- a/tests/frontendAction/coordinator.test.ts +++ b/tests/frontendAction/coordinator.test.ts @@ -44,47 +44,4 @@ describe("FrontendActionCoordinator", () => { coordinator.registerSink("s", () => undefined); expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "unknown", params: {} })).toThrow("unknown frontend action"); }); - - it("requires a valid time range when opening history data", async () => { - const coordinator = new FrontendActionCoordinator(); - let emitted: FrontendActionRequest | undefined; - coordinator.registerSink("s", (request) => { emitted = request; }); - expect(() => coordinator.request({ - sessionId: "s", - toolCallId: "c", - name: "view_history", - params: { feature_infos: [["node-1", "junction"]], data_type: "realtime" }, - })).toThrow("invalid frontend action params"); - expect(() => coordinator.request({ - sessionId: "s", - toolCallId: "c", - name: "view_history", - params: { - feature_infos: [["node-1", "junction"]], - data_type: "realtime", - start_time: "2026-07-14T00:00:00Z", - end_time: "2026-07-14T00:00:00Z", - }, - })).toThrow("invalid frontend action params"); - const pending = coordinator.request({ - sessionId: "s", - toolCallId: "c", - name: "view_history", - params: { - feature_infos: [["node-1", "junction"]], - data_type: "realtime", - start_time: "2026-07-14T00:00:00Z", - end_time: "2026-07-14T01:00:00Z", - }, - }); - if (!emitted) throw new Error("request was not emitted"); - coordinator.submitResult("s", { - version: "frontend-action-result@1", - actionId: emitted.actionId, - status: "succeeded", - output: { component: "HistoryPanel", rendered: true, itemCount: 1 }, - completedAt: Date.now(), - }); - await expect(pending).resolves.toMatchObject({ status: "succeeded" }); - }); }); diff --git a/tests/memory/store.test.ts b/tests/memory/store.test.ts index e3bae00..5c07ade 100644 --- a/tests/memory/store.test.ts +++ b/tests/memory/store.test.ts @@ -24,11 +24,11 @@ describe("MemoryStore", () => { it("dedupes exact duplicate memories", async () => { const first = await store.upsert("workspace", "project-1", { - content: "DMA-2 nightly leakage analysis should compare against adjacent zones first.", + content: "SCADA level anomaly checks should compare level and temperature metrics first.", source: "tool", }); const second = await store.upsert("workspace", "project-1", { - content: "DMA-2 nightly leakage analysis should compare against adjacent zones first.", + content: "SCADA level anomaly checks should compare level and temperature metrics first.", source: "tool", }); diff --git a/tests/opencode/scadaToolsDescription.test.ts b/tests/opencode/scadaToolsDescription.test.ts new file mode 100644 index 0000000..b8eb912 --- /dev/null +++ b/tests/opencode/scadaToolsDescription.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test"; +import { readFileSync } from "node:fs"; + +describe("SCADA opencode tools", () => { + it("documents asset listing and reading query parameters", () => { + const listTool = readFileSync(".opencode/tools/list_scada_assets.ts", "utf8"); + const queryTool = readFileSync(".opencode/tools/query_scada_readings.ts", "utf8"); + + expect(listTool).toContain("无请求参数"); + expect(listTool).toContain("assets[].scada_id"); + expect(queryTool).toContain("asset_ids"); + expect(queryTool).toContain("observed_from"); + expect(queryTool).toContain("observed_to"); + expect(queryTool).toContain("granularity"); + expect(queryTool).toContain("metrics"); + expect(queryTool).toContain("半开区间"); + expect(queryTool).toContain("不要使用 GeoServer feature id/fid、external_id 或 site_external_id"); + }); +}); diff --git a/tests/opencode/skillManagerTool.test.ts b/tests/opencode/skillManagerTool.test.ts index a453f5d..d4c821c 100644 --- a/tests/opencode/skillManagerTool.test.ts +++ b/tests/opencode/skillManagerTool.test.ts @@ -26,8 +26,8 @@ describe("skill_manager tool", () => { const skillDocument = (body: string) => [ "---", - "name: pressure-review", - "description: Pressure review workflow.", + "name: scada-level-review", + "description: SCADA level review workflow.", "---", "", body, @@ -64,32 +64,32 @@ describe("skill_manager tool", () => { await tool.execute( { action: "write_skill", - content: skillDocument("# Pressure Review"), + content: skillDocument("# SCADA Level Review"), reason: "verified reusable workflow", - skill_path: "workflow/pressure-review", + skill_path: "workflow/scada-level-review", }, toolContext, ) as string, ); expect(writeResult.decision).toBe("accepted"); await expect(readFile(writeResult.target, "utf8")).resolves.toContain( - "# Pressure Review\n", + "# SCADA Level Review\n", ); const updateResult = JSON.parse( await tool.execute( { action: "write_skill", - content: skillDocument("# Updated Pressure Review"), + content: skillDocument("# Updated SCADA Level Review"), reason: "verified reusable workflow overwrite", - skill_path: "workflow/pressure-review", + skill_path: "workflow/scada-level-review", }, toolContext, ) as string, ); expect(updateResult.decision).toBe("accepted"); await expect(readFile(updateResult.target, "utf8")).resolves.toContain( - "# Updated Pressure Review\n", + "# Updated SCADA Level Review\n", ); const removeResult = JSON.parse( @@ -97,7 +97,7 @@ describe("skill_manager tool", () => { { action: "remove_skill", reason: "workflow is obsolete", - skill_path: "workflow/pressure-review", + skill_path: "workflow/scada-level-review", }, toolContext, ) as string, diff --git a/tests/results/store.test.ts b/tests/results/store.test.ts index 4c5f0ab..ca0c0be 100644 --- a/tests/results/store.test.ts +++ b/tests/results/store.test.ts @@ -26,28 +26,26 @@ describe("ResultReferenceResolver", () => { await rm(tempDir, { force: true, recursive: true }); }); - it("stores metadata for render refs and resolves them", async () => { + it("stores metadata for server API result refs and resolves them", async () => { const record = await resolver.register({ actorKey: "actor-1", clientSessionId: "client-1", data: { - node_area_map: { - J1: "DMA-1", - J2: "DMA-2", - }, + data: [{ asset_id: "asset-1", value: 1.23 }], + meta: { rows: 1 }, }, - kind: RESULT_REFERENCE_KIND.renderJunctionsPayload, + kind: RESULT_REFERENCE_KIND.serverApiPayload, projectId: "project-1", projectKey: "project-key-1", schemaVersion: 1, sessionId: "session-1", - source: RESULT_REFERENCE_SOURCE.agentGenerated, + source: RESULT_REFERENCE_SOURCE.serverApi, traceId: "trace-1", }); - expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload); + expect(record.kind).toBe(RESULT_REFERENCE_KIND.serverApiPayload); expect(record.schemaVersion).toBe(1); - expect(record.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated); + expect(record.source).toBe(RESULT_REFERENCE_SOURCE.serverApi); const result = await resolver.getFullAuthorized( record.resultRef, @@ -58,14 +56,12 @@ describe("ResultReferenceResolver", () => { ); expect(result).not.toBeNull(); - expect(result?.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload); + expect(result?.kind).toBe(RESULT_REFERENCE_KIND.serverApiPayload); expect(result?.schema_version).toBe(1); - expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated); + expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.serverApi); expect(result?.data).toEqual({ - node_area_map: { - J1: "DMA-1", - J2: "DMA-2", - }, + data: [{ asset_id: "asset-1", value: 1.23 }], + meta: { rows: 1 }, }); }); @@ -102,97 +98,23 @@ describe("ResultReferenceResolver", () => { }); expect(malformed).toBeNull(); - const renderRecord = await resolver.register({ + const serverRecord = await resolver.register({ actorKey: "actor-2", clientSessionId: "client-2", - data: { - node_area_map: { - J1: "DMA-1", - }, - }, - kind: RESULT_REFERENCE_KIND.renderJunctionsPayload, + data: { data: [{ asset_id: "asset-2" }] }, + kind: RESULT_REFERENCE_KIND.serverApiPayload, projectId: "project-2", projectKey: "project-key-2", schemaVersion: 1, sessionId: "session-2", - source: RESULT_REFERENCE_SOURCE.agentGenerated, + source: RESULT_REFERENCE_SOURCE.serverApi, traceId: "trace-2", }); - const wrongActor = await resolver.getFullAuthorized(renderRecord.resultRef, { + const wrongActor = await resolver.getFullAuthorized(serverRecord.resultRef, { actorKey: "actor-other", projectId: "project-2", }); expect(wrongActor).toBeNull(); }); - - it("registers render refs from local wrapper files and normalizes payloads", async () => { - const filePath = join(tempDir, "render-wrapper.json"); - await writeFile( - filePath, - JSON.stringify( - { - metadata: { - createdAt: "2026-05-21T00:00:00.000Z", - projectId: "project-3", - }, - location: { - file_path: filePath, - }, - data: { - node_area_map: { - J1: "DMA-1", - J2: 2, - }, - area_ids: ["DMA-1", " DMA-2 "], - area_colors: { - "DMA-1": "#ff0000", - "DMA-2": "#00ff00", - }, - }, - createdAt: "2026-05-21T00:00:00.000Z", - }, - null, - 2, - ), - "utf8", - ); - - const record = await resolver.registerRenderPayloadFile(filePath, { - actorKey: "actor-3", - clientSessionId: "client-3", - projectId: "project-3", - projectKey: "project-key-3", - sessionId: "session-3", - source: RESULT_REFERENCE_SOURCE.agentGenerated, - traceId: "trace-3", - }); - - expect(record.kind).toBe(RESULT_REFERENCE_KIND.renderJunctionsPayload); - expect(record.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated); - - const result = await resolver.getFullAuthorized( - record.resultRef, - { - actorKey: "actor-3", - projectId: "project-3", - }, - { - expectedKind: RESULT_REFERENCE_KIND.renderJunctionsPayload, - }, - ); - - expect(result?.data).toEqual({ - node_area_map: { - J1: "DMA-1", - J2: "2", - }, - area_ids: ["DMA-1", "DMA-2"], - area_colors: { - "DMA-1": "#ff0000", - "DMA-2": "#00ff00", - }, - }); - }); - }); diff --git a/tests/routes/chatSession.test.ts b/tests/routes/chatSession.test.ts index 72dd707..003d1c5 100644 --- a/tests/routes/chatSession.test.ts +++ b/tests/routes/chatSession.test.ts @@ -22,26 +22,26 @@ describe("resolveRequestedStreamSessionId", () => { expect( resolveRequestedStreamSessionId({ sessionId: " existing-session ", - promptText: "检查压力异常", + promptText: "检查 SCADA 液位异常", createClientSessionId, }), ).toBe("existing-session"); expect( resolveRequestedStreamSessionId({ - promptText: "检查压力异常", + promptText: "检查 SCADA 液位异常", createClientSessionId, }), ).toBeUndefined(); }); - it("keeps the pressure trend demo on its generated client session id", () => { + it("does not special-case removed SCADA trend demo prompts", () => { expect( resolveRequestedStreamSessionId({ - promptText: "展示最近压力趋势", + promptText: "展示最近液位趋势", createClientSessionId: () => "demo-session", }), - ).toBe("demo-session"); + ).toBeUndefined(); }); }); @@ -92,7 +92,7 @@ describe("generateSessionTitle", () => { messages: async () => [ { info: { role: "assistant" }, - parts: [{ type: "text", text: "标题:泵站压力异常排查。" }], + parts: [{ type: "text", text: "标题:SCADA 液位异常排查。" }], }, ], abortSession: async () => undefined, @@ -100,14 +100,14 @@ describe("generateSessionTitle", () => { const title = await generateSessionTitle(runtime, { sessionId: "chat-session", - latestUserMessage: "检查一下三号泵站最近压力波动的原因", - latestAssistantMessage: "三号泵站压力波动主要与夜间阀门开度变化有关。", + latestUserMessage: "检查一下 82c7d04c 资产最近液位波动的原因", + latestAssistantMessage: "该 SCADA 资产液位波动主要与夜间水位快速变化时段重合。", fallbackTitle: "新对话", }); - expect(title).toBe("泵站压力异常排查"); - expect(titlePrompt).toContain("用户:检查一下三号泵站最近压力波动的原因"); - expect(titlePrompt).toContain("助手:三号泵站压力波动主要与夜间阀门开度变化有关。"); + expect(title).toBe("SCADA 液位异常排查"); + expect(titlePrompt).toContain("用户:检查一下 82c7d04c 资产最近液位波动的原因"); + expect(titlePrompt).toContain("助手:该 SCADA 资产液位波动主要与夜间水位快速变化时段重合。"); }); }); @@ -124,32 +124,32 @@ describe("buildPromptWithLearningContext", () => { { recentTurns: [], persistedMessages: [ - { role: "user", content: "先分析 3 号泵站夜间压力波动" }, + { role: "user", content: "先分析 82c7d04c 资产夜间液位波动" }, { role: "assistant", - content: "已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。", + content: "已定位到夜间液位变化与读数异常时段重合,下一步准备对比温度和液位曲线。", isError: true, }, { role: "assistant", content: "⚠️ **请求已中断**", isError: true }, ], - message: "继续刚才的分析,并补充相邻支路影响", + message: "继续刚才的分析,并补充温度指标对照", }, ); - expect(prompt).toContain("用户:先分析 3 号泵站夜间压力波动"); + expect(prompt).toContain("用户:先分析 82c7d04c 资产夜间液位波动"); expect(prompt).toContain( - "助手:已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。", + "助手:已定位到夜间液位变化与读数异常时段重合,下一步准备对比温度和液位曲线。", ); expect(prompt).not.toContain("⚠️ **请求已中断**"); - expect(prompt).toContain("[Current user request]\n继续刚才的分析,并补充相邻支路影响"); + expect(prompt).toContain("[Current user request]\n继续刚才的分析,并补充温度指标对照"); }); it("falls back to history turns when frontend state is unavailable", async () => { const recentTurns: SessionTurnRecord[] = [ { id: "turn-1", - userMessage: "检查 DMA-2 夜间漏损异常", - assistantMessage: "DMA-2 在 02:00-04:00 出现持续最小夜流抬升。", + userMessage: "检查 82c7d04c 资产夜间液位异常", + assistantMessage: "82c7d04c 在 02:00-04:00 出现持续液位抬升。", timestamp: new Date().toISOString(), toolCallCount: 1, }, @@ -161,12 +161,12 @@ describe("buildPromptWithLearningContext", () => { "project-1", { recentTurns, - message: "继续给出排查建议", + message: "继续给出数据核查建议", }, ); - expect(prompt).toContain("用户:检查 DMA-2 夜间漏损异常"); - expect(prompt).toContain("助手:DMA-2 在 02:00-04:00 出现持续最小夜流抬升。"); + expect(prompt).toContain("用户:检查 82c7d04c 资产夜间液位异常"); + expect(prompt).toContain("助手:82c7d04c 在 02:00-04:00 出现持续液位抬升。"); }); it("skips restored conversation injection when reusing an existing opencode session", async () => { @@ -216,20 +216,20 @@ describe("buildPromptWithLearningContext", () => { describe("extractLatestFrontendTurn", () => { it("extracts the latest valid frontend user and assistant turn", () => { const turn = extractLatestFrontendTurn([ - { role: "user", content: "检查 DMA-2 漏损" }, + { role: "user", content: "检查 82c7d04c 液位异常" }, { role: "assistant", - content: "DMA-2 夜间最小流量持续抬升。", + content: "82c7d04c 夜间液位持续抬升。", progress: [{ id: "tool-dma", phase: "tool" }], }, - { role: "user", content: "继续分析相邻分区" }, + { role: "user", content: "继续分析同类资产" }, { role: "assistant", content: "⚠️ **请求已中断**", isError: true }, ]); expect(turn).toEqual({ - assistantMessage: "DMA-2 夜间最小流量持续抬升。", + assistantMessage: "82c7d04c 夜间液位持续抬升。", toolCallCount: 1, - userMessage: "检查 DMA-2 漏损", + userMessage: "检查 82c7d04c 液位异常", }); }); }); @@ -239,7 +239,7 @@ describe("buildForkedSessionUiState", () => { const forked = buildForkedSessionUiState( { messages: [ - { role: "user", content: "画压力曲线" }, + { role: "user", content: "画液位曲线" }, { role: "assistant", content: "已生成图表", @@ -265,7 +265,7 @@ describe("buildForkedSessionUiState", () => { sessionId: "forked-session", isTitleManuallyEdited: false, messages: [ - { role: "user", content: "画压力曲线" }, + { role: "user", content: "画液位曲线" }, { role: "assistant", content: "已生成图表", diff --git a/tests/routes/chatStream.test.ts b/tests/routes/chatStream.test.ts index d862149..de6d78c 100644 --- a/tests/routes/chatStream.test.ts +++ b/tests/routes/chatStream.test.ts @@ -20,12 +20,12 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); describe("streamPromptResponse", () => { it("does not split short Chinese text only because it has punctuation", () => { - expect(detectTokenSmoothingChunk("管网压力异常,需要继续分析", "zh")).toBeNull(); - expect(detectTokenSmoothingChunk("管网压力异常需要继续分析。", "zh")).toBeNull(); + expect(detectTokenSmoothingChunk("SCADA 液位异常,需要继续分析", "zh")).toBeNull(); + expect(detectTokenSmoothingChunk("SCADA 液位异常需要继续分析。", "zh")).toBeNull(); }); it("does not immediately release a single Chinese character", () => { - expect(detectTokenSmoothingChunk("管", "zh")).toBeNull(); + expect(detectTokenSmoothingChunk("液", "zh")).toBeNull(); }); it("keeps non-Chinese text on the word path even with zh locale", () => { @@ -33,7 +33,7 @@ describe("streamPromptResponse", () => { }); it("falls back to bounded Chinese chunks when punctuation is absent", () => { - const content = "管网压力异常需要继续分析东部主干供水走廊和相关阀门状态变化"; + const content = "SCADA 液位异常需要继续分析水质温度和液位读数变化"; const chunk = detectTokenSmoothingChunk(content, "zh"); expect(chunk).not.toBeNull(); @@ -72,11 +72,11 @@ describe("streamPromptResponse", () => { write: (event, data) => events.push({ event, data }), }); - smoother.writeToken("管"); + smoother.writeToken("液"); await sleep(8); expect(events).toHaveLength(0); - const content = "网压力异常需要继续分析东部主干供水走廊和相关阀门状态"; + const content = "位异常需要继续分析水质温度和液位读数持续变化趋势"; for (const char of Array.from(content)) { smoother.writeToken(char); } @@ -84,7 +84,7 @@ describe("streamPromptResponse", () => { expect(events.length).toBeGreaterThan(0); await smoother.flush(); - expect(events.map((item) => item.data.content).join("")).toBe(`管${content}`); + expect(events.map((item) => item.data.content).join("")).toBe(`液${content}`); }); it("smooths long Chinese text without waiting for paragraph-sized buffers", async () => { @@ -97,7 +97,7 @@ describe("streamPromptResponse", () => { write: (event, data) => events.push({ event, data }), }); - const content = "管网压力异常需要继续分析东部主干供水走廊和相关阀门状态变化"; + const content = "SCADA 液位异常需要继续分析水质温度和液位读数变化"; for (const char of Array.from(content)) { smoother.writeToken(char); } @@ -133,7 +133,7 @@ describe("streamPromptResponse", () => { sessionID: "runtime-session-1", partID: "text-part-1", field: "text", - delta: "管网压力异常需要继续分析", + delta: "SCADA 液位异常需要继续分析", }, }, { @@ -142,7 +142,7 @@ describe("streamPromptResponse", () => { sessionID: "runtime-session-1", partID: "text-part-1", field: "text", - delta: "东部主干供水走廊和相关阀门状态。", + delta: "水质温度和液位读数。", }, }, { @@ -172,7 +172,7 @@ describe("streamPromptResponse", () => { expect(tokenEvents.length).toBeGreaterThan(0); expect(tokenEvents.every((item) => events.indexOf(item) < doneIndex)).toBe(true); expect(tokenEvents.map((item) => item.data.content).join("")).toBe( - "管网压力异常需要继续分析东部主干供水走廊和相关阀门状态。", + "SCADA 液位异常需要继续分析水质温度和液位读数。", ); }); @@ -535,7 +535,7 @@ describe("streamPromptResponse", () => { }); }); - it("maps visual tool calls to UIEnvelope SSE payloads", async () => { + it("maps zoom_to_map tool calls to UIEnvelope SSE payloads", async () => { const runtime = { subscribeEvents: async () => createEventStream([ @@ -544,20 +544,19 @@ describe("streamPromptResponse", () => { properties: { sessionID: "runtime-session-1", part: { - id: "tool-part-chart", + id: "tool-part-map", sessionID: "runtime-session-1", messageID: "message-1", type: "tool", - callID: "call-chart", - tool: "show_chart", + callID: "call-map", + tool: "zoom_to_map", state: { status: "running", input: { - reason: "展示压力趋势", - title: "压力趋势", - chart_type: "line", - x_data: ["00:00", "01:00"], - series: [{ name: "P-1", data: [0.4, 0.42] }], + reason: "定位 SCADA 资产附近位置", + x: 121.47, + y: 31.23, + source_crs: "EPSG:4326", }, time: { start: Date.now() }, }, @@ -587,14 +586,15 @@ describe("streamPromptResponse", () => { expect(events.find((item) => item.event === "tool_call")?.data).toMatchObject({ session_id: "client-session-1", - tool: "show_chart", + tool: "zoom_to_map", }); expect(events.find((item) => item.event === "ui_envelope")?.data).toMatchObject({ session_id: "client-session-1", envelope: { - kind: "chart", + kind: "map_action", schemaVersion: "agent-ui@1", - grammar: "echarts-safe-subset", + action: "zoom_to_map", + surface: "map_overlay", }, }); }); diff --git a/tests/routes/chatUiState.test.ts b/tests/routes/chatUiState.test.ts index fd2ce6d..8ef72bc 100644 --- a/tests/routes/chatUiState.test.ts +++ b/tests/routes/chatUiState.test.ts @@ -8,13 +8,13 @@ import { } from "../../src/routes/chatUiState.js"; describe("appendBackendToolArtifact", () => { - it("persists show_chart tool calls as chart artifacts", () => { + it("persists removed visual tool calls as generic tool artifacts", () => { const artifacts = appendBackendToolArtifact([], { session_id: "session-1", tool: "show_chart", reason: "测试折线图渲染", params: { - title: "压力曲线", + title: "液位曲线", chart_type: "line", x_data: ["00:00", "01:00"], series: [{ name: "P-101", data: [0.42, 0.41] }], @@ -24,8 +24,8 @@ describe("appendBackendToolArtifact", () => { expect(artifacts).toHaveLength(1); expect(artifacts[0]).toMatchObject({ tool: "show_chart", - kind: "chart", - title: "压力曲线", + kind: "tool", + title: "液位曲线", description: "测试折线图渲染", params: { chart_type: "line", @@ -50,9 +50,9 @@ describe("appendBackendUiEnvelope", () => { envelope: { kind: "map_action", schemaVersion: "agent-ui@1", - action: "locate_features", + action: "zoom_to_map", surface: "map_overlay", - params: { ids: ["J-1"], feature_type: "junction" }, + params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, }, }); @@ -63,9 +63,9 @@ describe("appendBackendUiEnvelope", () => { envelope: { kind: "map_action", schemaVersion: "agent-ui@1", - action: "locate_features", + action: "zoom_to_map", surface: "map_overlay", - params: { ids: ["J-1"], feature_type: "junction" }, + params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, }, renderStatus: "pending", }, diff --git a/tests/routes/pressureTrendDemo.test.ts b/tests/routes/pressureTrendDemo.test.ts deleted file mode 100644 index 3c6a751..0000000 --- a/tests/routes/pressureTrendDemo.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from "bun:test"; - -import { - createPressureTrendDemoEnvelopePayload, - createPressureTrendDemoToolCall, - isPressureTrendDemoPrompt, -} from "../../src/routes/pressureTrendDemo.js"; - -describe("pressure trend demo", () => { - it("matches the controlled demo prompt", () => { - expect(isPressureTrendDemoPrompt("展示最近压力趋势")).toBe(true); - expect(isPressureTrendDemoPrompt("请 展示 最近 压力 趋势")).toBe(true); - expect(isPressureTrendDemoPrompt("展示最近流量趋势")).toBe(false); - }); - - it("creates a show_chart tool call that maps to a chart UIEnvelope", () => { - const toolCall = createPressureTrendDemoToolCall(); - const payload = createPressureTrendDemoEnvelopePayload("client-session-1", toolCall); - - expect(toolCall.tool).toBe("show_chart"); - expect(payload).toMatchObject({ - session_id: "client-session-1", - envelope: { - kind: "chart", - schemaVersion: "agent-ui@1", - grammar: "echarts-safe-subset", - surface: "chat_inline", - spec: { - title: "最近压力趋势", - chart_type: "line", - }, - data: { - x_data: ["10:00", "11:00", "12:00", "13:00", "14:00", "15:00"], - }, - }, - }); - }); -}); diff --git a/tests/serverApi/gateway.test.ts b/tests/serverApi/gateway.test.ts index dfaaaf3..0525990 100644 --- a/tests/serverApi/gateway.test.ts +++ b/tests/serverApi/gateway.test.ts @@ -16,38 +16,38 @@ describe("ServerApiGateway project context", () => { let requestedPath = ""; const fetchImpl = (async (input: URL | RequestInfo) => { requestedPath = new URL(input.toString()).pathname; - return Response.json({ - project_id: context.projectId, - code: "lingang", - name: "临港排水", - description: null, - status: "active", - project_role: "owner", - gs_workspace: "lingang", - map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 }, - }); + return Response.json([{ + project_id: context.projectId, + code: "lingang", + name: "SCADA 物联网监测", + description: null, + status: "active", + project_role: "owner", + gs_workspace: "lingang", + map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 }, + }]); }) as typeof fetch; const gateway = new ServerApiGateway({} as never, fetchImpl); const result = await gateway.execute("get_project_context", {}, context); - expect(requestedPath).toBe("/api/v1/meta/project"); + expect(requestedPath).toBe("/api/v1/projects"); expect(result).toMatchObject({ ok: true, data: { project_id: context.projectId, status: "active", project_role: "owner" }, }); }); - test("rejects the removed id response shape", async () => { + test("does not select the removed id response shape", async () => { const fetchImpl = (async () => - Response.json({ id: context.projectId, code: "lingang", name: "临港排水" })) as unknown as typeof fetch; + Response.json([{ id: context.projectId, code: "lingang", name: "SCADA 物联网监测" }])) as unknown as typeof fetch; const gateway = new ServerApiGateway({} as never, fetchImpl); const result = await gateway.execute("get_project_context", {}, context); expect(result).toMatchObject({ ok: false, - error: { code: "UPSTREAM_UNAVAILABLE" }, + error: { code: "NOT_FOUND" }, }); }); }); diff --git a/tests/serverApi/schemas.test.ts b/tests/serverApi/schemas.test.ts index 66ee038..63de63c 100644 --- a/tests/serverApi/schemas.test.ts +++ b/tests/serverApi/schemas.test.ts @@ -1,24 +1,17 @@ import { describe, expect, test } from "bun:test"; import { domainToolSchemas } from "../../src/serverApi/schemas.js"; -import { createIdempotencyKey } from "../../src/serverApi/gateway.js"; - -const context = { - actorKey: "user", clientSessionId: "session", projectId: "1c60fd8c-89fb-47ca-84f1-66ec10f5bf73", - projectKey: "project", sessionId: "runtime", traceId: "trace", -}; describe("domain tool validation", () => { test("rejects unknown metrics and reversed time", () => { - const base = { device_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T02:00:00Z", observed_to: "2026-07-14T01:00:00Z", granularity: "5m" }; - expect(domainToolSchemas.query_monitoring_readings.safeParse({ ...base, metrics: ["pressure"] }).success).toBeFalse(); - expect(domainToolSchemas.query_monitoring_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse(); + const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T02:00:00Z", observed_to: "2026-07-14T01:00:00Z", granularity: "5m" }; + expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["ph_mean"] }).success).toBeFalse(); + expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse(); }); - test("rejects invalid UUID before dispatch", () => { - expect(domainToolSchemas.start_simulation.safeParse({ model_version_id: "bad", parameters: {} }).success).toBeFalse(); - }); - - test("idempotency key ignores object key order", () => { - expect(createIdempotencyKey(context, "start_simulation", { b: 2, a: 1 })).toBe(createIdempotencyKey(context, "start_simulation", { a: 1, b: 2 })); + test("accepts four current metrics and rejects removed metrics", () => { + const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T01:00:00Z", observed_to: "2026-07-14T02:00:00Z", granularity: "5m" }; + expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["conductivity_mean", "level_mean", "flow_mean", "temperature_mean"] }).success).toBeTrue(); + expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_total"] }).success).toBeFalse(); + expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["velocity_mean"] }).success).toBeFalse(); }); }); diff --git a/tests/sessions/transcriptStore.test.ts b/tests/sessions/transcriptStore.test.ts index a03aff5..066eb1d 100644 --- a/tests/sessions/transcriptStore.test.ts +++ b/tests/sessions/transcriptStore.test.ts @@ -88,9 +88,9 @@ describe("SessionTranscriptStore", () => { sessionId: "thread-3", }, { - assistantMessage: "已完成压力波动分析。", + assistantMessage: "已完成液位波动分析。", toolCallCount: 1, - userMessage: "分析压力波动。", + userMessage: "分析液位波动。", }, ); @@ -102,9 +102,9 @@ describe("SessionTranscriptStore", () => { sessionId: "thread-3", }, { - assistantMessage: "已完成压力波动分析。", + assistantMessage: "已完成液位波动分析。", toolCallCount: 2, - userMessage: "分析压力波动。", + userMessage: "分析液位波动。", }, ); diff --git a/tests/skills/store.test.ts b/tests/skills/store.test.ts index afc488c..610f5b8 100644 --- a/tests/skills/store.test.ts +++ b/tests/skills/store.test.ts @@ -42,7 +42,7 @@ describe("SkillStore", () => { process.chdir(alternateCwd); const result = await store.writeScript( - "workflow/hydraulic-bottleneck-analysis", + "workflow/scada-level-review", "scripts/analyze.py", "print('ok')\n", ); @@ -53,7 +53,7 @@ describe("SkillStore", () => { target: join( skillsRoot, "workflow", - "hydraulic-bottleneck-analysis", + "scada-level-review", "scripts", "analyze.py", ), @@ -63,7 +63,7 @@ describe("SkillStore", () => { it("rejects script paths outside scripts/*.py", async () => { const result = await store.writeScript( - "workflow/hydraulic-bottleneck-analysis", + "workflow/scada-level-review", "analyze.ts", "console.log('ok')\n", ); @@ -76,21 +76,21 @@ describe("SkillStore", () => { }); it("writes and overwrites the main skill file", async () => { - const skillPath = "workflow/pressure-review"; + const skillPath = "workflow/scada-level-review"; const writeResult = await store.writeSkill( skillPath, - skillDocument("pressure-review", "# Pressure Review"), + skillDocument("scada-level-review", "# SCADA Level Review"), ); expect(writeResult).toEqual({ changed: true, detail: "skill written", - target: join(skillsRoot, "workflow", "pressure-review", "SKILL.md"), + target: join(skillsRoot, "workflow", "scada-level-review", "SKILL.md"), }); const overwriteResult = await store.writeSkill( skillPath, - skillDocument("pressure-review", "# Updated Pressure Review"), + skillDocument("scada-level-review", "# Updated SCADA Level Review"), ); expect(overwriteResult).toEqual({ @@ -99,7 +99,7 @@ describe("SkillStore", () => { target: writeResult.target, }); await expect(readFile(writeResult.target, "utf8")).resolves.toContain( - "# Updated Pressure Review\n", + "# Updated SCADA Level Review\n", ); }); diff --git a/tests/uiEnvelope/fromToolCall.test.ts b/tests/uiEnvelope/fromToolCall.test.ts index 40f7c2d..dd935bf 100644 --- a/tests/uiEnvelope/fromToolCall.test.ts +++ b/tests/uiEnvelope/fromToolCall.test.ts @@ -3,51 +3,24 @@ import { describe, expect, it } from "bun:test"; import { toUiEnvelopeFromToolCall } from "../../src/uiEnvelope/fromToolCall.js"; describe("toUiEnvelopeFromToolCall", () => { - it("maps show_chart calls to chart envelopes", () => { + it("maps zoom_to_map calls to map action envelopes", () => { const envelope = toUiEnvelopeFromToolCall({ - tool: "show_chart", - reason: "展示压力趋势", - params: { - title: "压力趋势", - chart_type: "line", - x_data: ["00:00", "01:00"], - series: [{ name: "P-1", data: [0.41, 0.43] }], - }, + tool: "zoom_to_map", + reason: "定位 SCADA 资产附近位置", + params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, }); expect(envelope).toMatchObject({ - kind: "chart", - schemaVersion: "agent-ui@1", - grammar: "echarts-safe-subset", - surface: "chat_inline", - fallbackText: "展示压力趋势", - data: { - x_data: ["00:00", "01:00"], - series: [{ name: "P-1", data: [0.41, 0.43] }], - }, + kind: "map_action", + action: "zoom_to_map", + surface: "map_overlay", + params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, + fallbackText: "定位 SCADA 资产附近位置", }); }); - it("rejects render_junctions without a valid render_ref", () => { - expect( - toUiEnvelopeFromToolCall({ - tool: "render_junctions", - params: { render_ref: "/tmp/payload.json" }, - }), - ).toBeNull(); - }); - - it("maps view_scada calls to registered component envelopes", () => { - const envelope = toUiEnvelopeFromToolCall({ - tool: "view_scada", - params: { device_id: "D-1" }, - }); - - expect(envelope).toMatchObject({ - kind: "registered_component", - component: "ScadaPanel", - surface: "side_panel", - props: { device_id: "D-1" }, - }); + it("does not map removed visual tools", () => { + expect(toUiEnvelopeFromToolCall({ tool: "show_chart", params: {} })).toBeNull(); + expect(toUiEnvelopeFromToolCall({ tool: "view_scada", params: { asset_ids: ["A-1"] } })).toBeNull(); }); });