refactor(agent)!: focus tools on SCADA
This commit is contained in:
@@ -4,7 +4,7 @@ mode: primary
|
|||||||
model: deepseek/deepseek-v4-flash
|
model: deepseek/deepseek-v4-flash
|
||||||
temperature: 0.2
|
temperature: 0.2
|
||||||
---
|
---
|
||||||
你是 TJWater 实验性供水管网分析 Agent。回复用户时使用简体中文,内容简洁准确。
|
你是 TJWater 实验性 SCADA 数据 Agent。回复用户时使用简体中文,内容简洁准确。
|
||||||
|
|
||||||
## 核心定位
|
## 核心定位
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ Agent 负责:
|
|||||||
1. 理解用户意图。
|
1. 理解用户意图。
|
||||||
2. 调用受控工具获取有限上下文或表达展示意图。
|
2. 调用受控工具获取有限上下文或表达展示意图。
|
||||||
3. 输出文字摘要、结论、追问和建议。
|
3. 输出文字摘要、结论、追问和建议。
|
||||||
4. 通过前端展示工具表达 UI 意图。
|
4. 通过受控地图工具表达定位意图。
|
||||||
|
|
||||||
服务端负责把前端展示工具转换成 `agent-ui@1` 的 UIEnvelope;前端只渲染可信组件、图表和地图动作。
|
服务端负责把受控地图工具转换成 `agent-ui@1` 的 UIEnvelope;前端只执行可信地图动作。
|
||||||
|
|
||||||
## 工具选择
|
## 工具选择
|
||||||
|
|
||||||
@@ -25,27 +25,19 @@ Agent 负责:
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 查询实时公开网页信息 | `web_search` |
|
| 查询实时公开网页信息 | `web_search` |
|
||||||
| 地址/地点转经纬度 | `geocode` |
|
| 地址/地点转经纬度 | `geocode` |
|
||||||
| 地图定位 | `locate_features`、`zoom_to_map` |
|
| 地图定位 | `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` |
|
|
||||||
| 历史会话检索 | `session_search` |
|
| 历史会话检索 | `session_search` |
|
||||||
| 用户偏好和项目事实 | `memory_manager` |
|
| 用户偏好和项目事实 | `memory_manager` |
|
||||||
| 可复用流程沉淀 | `skill_manager` |
|
| 可复用流程沉淀 | `skill_manager` |
|
||||||
| 项目背景 | `get_project_context` |
|
| 项目背景 | `get_project_context` |
|
||||||
| 监测设备/点位 | `list_monitoring_assets` |
|
| SCADA 资产 | `list_scada_assets` |
|
||||||
| 监测时序读数 | `query_monitoring_readings`(必须给出 `observed_from`、`observed_to`) |
|
| SCADA 时序读数 | `query_scada_readings`(必须给出 `observed_from`、`observed_to`) |
|
||||||
| 数据质量分析 | `start_data_quality_analysis`、`get_job_status` |
|
|
||||||
| SWMM 仿真 | `start_simulation`、`get_job_status` |
|
|
||||||
|
|
||||||
## UI 约束
|
## UI 约束
|
||||||
|
|
||||||
1. 前端工具仅表达展示意图,不返回业务数据。
|
1. 前端地图工具仅表达定位意图,不返回业务数据。
|
||||||
2. 每次工具调用必须填写具体 `reason`。
|
2. 每次工具调用必须填写具体 `reason`。
|
||||||
3. 图表使用 `show_chart`,折线图/柱状图必须用 `x_data` 作为横轴标签,`series[].data` 作为同长度数值数组。
|
3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。
|
||||||
4. 大型 junction 渲染必须使用 `render_ref`,不要直接把完整 payload 传给前端。
|
|
||||||
5. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。
|
|
||||||
|
|
||||||
## 执行约束
|
## 执行约束
|
||||||
|
|
||||||
@@ -66,13 +58,10 @@ Agent 负责:
|
|||||||
仅通过领域工具访问 Server;不得构造 URL、HTTP header、SQL、命令或项目 ID。
|
仅通过领域工具访问 Server;不得构造 URL、HTTP header、SQL、命令或项目 ID。
|
||||||
|
|
||||||
- 项目背景:`get_project_context`
|
- 项目背景:`get_project_context`
|
||||||
- 设备/监测点:`list_monitoring_assets`
|
- SCADA 资产:`list_scada_assets`
|
||||||
- 时序读数:`query_monitoring_readings`
|
- SCADA 时序读数:`query_scada_readings`
|
||||||
- 数据质量任务:`start_data_quality_analysis`(写操作,必须等待审批)
|
|
||||||
- 仿真任务:`start_simulation`(写操作,必须等待审批)
|
|
||||||
- 任务状态:`get_job_status`
|
|
||||||
|
|
||||||
查询历史监测数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_monitoring_readings`,传入 `observed_from`、`observed_to`、`granularity`、`metrics` 和设备 ID,再调用 `show_chart` 或 `view_history` 展示。`view_history` 只负责打开前端历史面板,也必须传入 `start_time`、`end_time`。
|
查询历史 SCADA 数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_scada_readings`,传入 `observed_from`、`observed_to`、`granularity`、`metrics` 和 `asset_ids`,再用文字摘要说明结果。
|
||||||
先查询数据再作判断。回答必须说明查询时间范围、粒度、指标,以及已创建任务的 ID 和状态。
|
先查询数据再作判断。回答必须说明查询时间范围、粒度和指标。
|
||||||
遇到空数据、拒绝、权限错误、超时或服务不可用时,明确说明事实,不得补全或猜测结果。
|
遇到空数据、拒绝、权限错误、超时或服务不可用时,明确说明事实,不得补全或猜测结果。
|
||||||
工具返回 `result_ref` 时,说明完整结果已安全存储,并使用 preview 中的样本和统计作答。
|
工具返回 `result_ref` 时,说明完整结果已安全存储,并使用 preview 中的样本和统计作答。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Plugin } from "@opencode-ai/plugin";
|
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 () => ({
|
export const frontendActionCallIdPlugin: Plugin = async () => ({
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
|
|||||||
@@ -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}图层应用样式。`;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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) });
|
|
||||||
@@ -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) });
|
|
||||||
@@ -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),
|
||||||
|
});
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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) });
|
|
||||||
@@ -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),
|
||||||
|
});
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { tool } from "@opencode-ai/plugin";
|
|
||||||
|
|
||||||
export default tool({
|
|
||||||
description:
|
|
||||||
"在前端地图上对 junctions 图层应用分区渲染。使用前必须完成两步:① 准备数据结构(JSON 文件,结构为 { node_area_map: Record<string, string>, area_ids?: string[], area_colors?: Record<string, string> },其中 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 "已在地图上应用节点分区渲染。";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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 "图表将在对话中显示。";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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) });
|
|
||||||
@@ -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) });
|
|
||||||
@@ -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;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+171
-1047
File diff suppressed because it is too large
Load Diff
@@ -3,36 +3,8 @@ import type { FrontendActionManifest } from "./types.js";
|
|||||||
|
|
||||||
type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny };
|
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 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[] = [
|
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: "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);
|
export const frontendActionRegistry = definitions.map((item) => item.manifest);
|
||||||
|
|||||||
Vendored
+75
-682
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/api/v1/meta/projects": {
|
"/api/v1/projects": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -12,7 +12,7 @@ export interface paths {
|
|||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
/** Projects */
|
/** Projects */
|
||||||
get: operations["projects_api_v1_meta_projects_get"];
|
get: operations["projects_api_v1_projects_get"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: never;
|
post?: never;
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -21,15 +21,15 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/v1/meta/project": {
|
"/api/v1/scada/assets": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
path?: never;
|
path?: never;
|
||||||
cookie?: never;
|
cookie?: never;
|
||||||
};
|
};
|
||||||
/** Project */
|
/** Assets */
|
||||||
get: operations["project_api_v1_meta_project_get"];
|
get: operations["assets_api_v1_scada_assets_get"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: never;
|
post?: never;
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@@ -38,58 +38,7 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/v1/meta/db/health": {
|
"/api/v1/scada/readings/query": {
|
||||||
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": {
|
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -99,133 +48,14 @@ export interface paths {
|
|||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
/** Readings */
|
/** Readings */
|
||||||
post: operations["readings_api_v1_monitoring_readings_query_post"];
|
post: operations["readings_api_v1_scada_readings_query_post"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/api/v1/analyses": {
|
"/api/v1/geocode": {
|
||||||
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": {
|
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -235,7 +65,7 @@ export interface paths {
|
|||||||
get?: never;
|
get?: never;
|
||||||
put?: never;
|
put?: never;
|
||||||
/** Geocode */
|
/** Geocode */
|
||||||
post: operations["geocode_api_v1_tianditu_geocode_post"];
|
post: operations["geocode_api_v1_geocode_post"];
|
||||||
delete?: never;
|
delete?: never;
|
||||||
options?: never;
|
options?: never;
|
||||||
head?: never;
|
head?: never;
|
||||||
@@ -280,80 +110,39 @@ export interface paths {
|
|||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
/** AnalysisCreate */
|
/** GeocodeLocation */
|
||||||
AnalysisCreate: {
|
GeocodeLocation: {
|
||||||
/**
|
/** Lon */
|
||||||
* Algorithm
|
lon: number;
|
||||||
* @constant
|
/** Lat */
|
||||||
*/
|
lat: number;
|
||||||
algorithm: "data-quality";
|
/** Level */
|
||||||
/** Device Ids */
|
level?: string | null;
|
||||||
device_ids: string[];
|
/** Score */
|
||||||
/**
|
score?: number | null;
|
||||||
* Observed From
|
/** Keyword */
|
||||||
* Format: date-time
|
keyWord?: string | null;
|
||||||
*/
|
|
||||||
observed_from: string;
|
|
||||||
/**
|
|
||||||
* Observed To
|
|
||||||
* Format: date-time
|
|
||||||
*/
|
|
||||||
observed_to: string;
|
|
||||||
};
|
};
|
||||||
/** Body_upload_api_v1_simulation_models_post */
|
/** GeocodeRequest */
|
||||||
Body_upload_api_v1_simulation_models_post: {
|
GeocodeRequest: {
|
||||||
/** Name */
|
/** Keyword */
|
||||||
name: string;
|
keyword: string;
|
||||||
/** File */
|
|
||||||
file: string;
|
|
||||||
};
|
};
|
||||||
/** DatabaseHealthResponse */
|
/** GeocodeResponse */
|
||||||
DatabaseHealthResponse: {
|
GeocodeResponse: {
|
||||||
/** Postgres */
|
/** Status */
|
||||||
postgres: string;
|
status: string | number;
|
||||||
/** Timescale */
|
/** Msg */
|
||||||
timescale: string;
|
msg?: string | null;
|
||||||
};
|
location?: components["schemas"]["GeocodeLocation"] | null;
|
||||||
/** DeviceOut */
|
/** Searchversion */
|
||||||
DeviceOut: {
|
searchVersion?: string | null;
|
||||||
/**
|
|
||||||
* Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* Monitoring Point Id
|
|
||||||
* Format: uuid
|
|
||||||
*/
|
|
||||||
monitoring_point_id: string;
|
|
||||||
/** External Id */
|
|
||||||
external_id: string;
|
|
||||||
/** Active */
|
|
||||||
active: boolean;
|
|
||||||
};
|
};
|
||||||
/** HTTPValidationError */
|
/** HTTPValidationError */
|
||||||
HTTPValidationError: {
|
HTTPValidationError: {
|
||||||
/** Detail */
|
/** Detail */
|
||||||
detail?: components["schemas"]["ValidationError"][];
|
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 */
|
||||||
MapExtent: {
|
MapExtent: {
|
||||||
/** Xmin */
|
/** Xmin */
|
||||||
@@ -365,37 +154,8 @@ export interface components {
|
|||||||
/** Ymax */
|
/** Ymax */
|
||||||
ymax: number;
|
ymax: number;
|
||||||
};
|
};
|
||||||
/** ModelUploadOut */
|
/** ProjectResponse */
|
||||||
ModelUploadOut: {
|
ProjectResponse: {
|
||||||
/**
|
|
||||||
* 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: {
|
|
||||||
/**
|
/**
|
||||||
* Project Id
|
* Project Id
|
||||||
* Format: uuid
|
* Format: uuid
|
||||||
@@ -418,31 +178,36 @@ export interface components {
|
|||||||
gs_workspace: string;
|
gs_workspace: string;
|
||||||
map_extent: components["schemas"]["MapExtent"] | null;
|
map_extent: components["schemas"]["MapExtent"] | null;
|
||||||
};
|
};
|
||||||
/** ProjectSummaryResponse */
|
/** ScadaAssetOut */
|
||||||
ProjectSummaryResponse: {
|
ScadaAssetOut: {
|
||||||
/**
|
/**
|
||||||
* Project Id
|
* Scada Id
|
||||||
* Format: uuid
|
* Format: uuid
|
||||||
*/
|
*/
|
||||||
project_id: string;
|
scada_id: string;
|
||||||
/** Code */
|
/** External Id */
|
||||||
code: string;
|
external_id: string;
|
||||||
|
/** Site External Id */
|
||||||
|
site_external_id: string;
|
||||||
/** Name */
|
/** Name */
|
||||||
name: string;
|
name: string;
|
||||||
/** Description */
|
|
||||||
description: string | null;
|
|
||||||
/**
|
/**
|
||||||
* Status
|
* Kind
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
*/
|
*/
|
||||||
status: "active" | "inactive";
|
kind: "water_quality" | "radar_level" | "ultrasonic_flow";
|
||||||
/** Project Role */
|
/** Active */
|
||||||
project_role: string;
|
active: boolean;
|
||||||
};
|
};
|
||||||
/** ReadingQuery */
|
/** ScadaAssetsResponse */
|
||||||
ReadingQuery: {
|
ScadaAssetsResponse: {
|
||||||
/** Device Ids */
|
/** Assets */
|
||||||
device_ids: string[];
|
assets: components["schemas"]["ScadaAssetOut"][];
|
||||||
|
};
|
||||||
|
/** ScadaReadingQuery */
|
||||||
|
ScadaReadingQuery: {
|
||||||
|
/** Asset Ids */
|
||||||
|
asset_ids: string[];
|
||||||
/**
|
/**
|
||||||
* Observed From
|
* Observed From
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
@@ -460,24 +225,24 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
granularity: "5m" | "1h" | "1d";
|
granularity: "5m" | "1h" | "1d";
|
||||||
/** Metrics */
|
/** Metrics */
|
||||||
metrics: string[];
|
metrics: ("conductivity_mean" | "level_mean" | "flow_mean" | "temperature_mean")[];
|
||||||
};
|
};
|
||||||
/** ReadingQueryResponse */
|
/** ScadaReadingQueryResponse */
|
||||||
ReadingQueryResponse: {
|
ScadaReadingQueryResponse: {
|
||||||
/** Data */
|
/** Data */
|
||||||
data: components["schemas"]["ReadingRow"][];
|
data: components["schemas"]["ScadaReadingRow"][];
|
||||||
/** Meta */
|
/** Meta */
|
||||||
meta: {
|
meta: {
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** ReadingRow */
|
/** ScadaReadingRow */
|
||||||
ReadingRow: {
|
ScadaReadingRow: {
|
||||||
/**
|
/**
|
||||||
* Device Id
|
* Asset Id
|
||||||
* Format: uuid
|
* Format: uuid
|
||||||
*/
|
*/
|
||||||
device_id: string;
|
asset_id: string;
|
||||||
/**
|
/**
|
||||||
* Observed At
|
* Observed At
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
@@ -488,46 +253,6 @@ export interface components {
|
|||||||
[key: string]: number | null;
|
[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 */
|
||||||
ValidationError: {
|
ValidationError: {
|
||||||
/** Location */
|
/** Location */
|
||||||
@@ -550,7 +275,7 @@ export interface components {
|
|||||||
}
|
}
|
||||||
export type $defs = Record<string, never>;
|
export type $defs = Record<string, never>;
|
||||||
export interface operations {
|
export interface operations {
|
||||||
projects_api_v1_meta_projects_get: {
|
projects_api_v1_projects_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -565,12 +290,12 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProjectSummaryResponse"][];
|
"application/json": components["schemas"]["ProjectResponse"][];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
project_api_v1_meta_project_get: {
|
assets_api_v1_scada_assets_get: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: {
|
header?: {
|
||||||
@@ -587,7 +312,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ProjectMetaResponse"];
|
"application/json": components["schemas"]["ScadaAssetsResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@@ -601,100 +326,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
health_api_v1_meta_db_health_get: {
|
readings_api_v1_scada_readings_query_post: {
|
||||||
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: {
|
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: {
|
header?: {
|
||||||
@@ -705,7 +337,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ReadingQuery"];
|
"application/json": components["schemas"]["ScadaReadingQuery"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@@ -715,7 +347,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["ReadingQueryResponse"];
|
"application/json": components["schemas"]["ScadaReadingQueryResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
@@ -729,246 +361,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
create_api_v1_analyses_post: {
|
geocode_api_v1_geocode_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: {
|
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: never;
|
header?: never;
|
||||||
@@ -977,7 +370,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["TiandituGeocodeRequest"];
|
"application/json": components["schemas"]["GeocodeRequest"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
@@ -987,7 +380,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["TiandituGeocodeResponse"];
|
"application/json": components["schemas"]["GeocodeResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Validation Error */
|
/** @description Validation Error */
|
||||||
|
|||||||
+1
-108
@@ -1,11 +1,8 @@
|
|||||||
import { readJsonFile } from "../utils/fileStore.js";
|
|
||||||
import {
|
import {
|
||||||
type ResultReferenceKind,
|
type ResultReferenceKind,
|
||||||
type ResultReferenceRecord,
|
type ResultReferenceRecord,
|
||||||
type ResultReferenceSource,
|
type ResultReferenceSource,
|
||||||
type RetrievalContext,
|
type RetrievalContext,
|
||||||
RESULT_REFERENCE_KIND,
|
|
||||||
RESULT_REFERENCE_SOURCE,
|
|
||||||
type ResultReferenceStore,
|
type ResultReferenceStore,
|
||||||
} from "./store.js";
|
} from "./store.js";
|
||||||
|
|
||||||
@@ -26,12 +23,6 @@ type RegisterResultReferenceInput = {
|
|||||||
traceId: string;
|
traceId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenderJunctionPayload = {
|
|
||||||
node_area_map: Record<string, string>;
|
|
||||||
area_ids?: string[];
|
|
||||||
area_colors?: Record<string, string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ResultReferenceResolver {
|
export class ResultReferenceResolver {
|
||||||
constructor(private readonly store: ResultReferenceStore) {}
|
constructor(private readonly store: ResultReferenceStore) {}
|
||||||
|
|
||||||
@@ -59,34 +50,6 @@ export class ResultReferenceResolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerRenderPayloadFile(
|
|
||||||
filePath: string,
|
|
||||||
input: Omit<RegisterResultReferenceInput, "data" | "kind" | "schemaVersion">,
|
|
||||||
) {
|
|
||||||
const raw = await readJsonFile<unknown>(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(
|
async getFullAuthorized(
|
||||||
resultRef: string,
|
resultRef: string,
|
||||||
context: RetrievalContext,
|
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 = (
|
const normalizeDataForKind = (
|
||||||
kind: ResultReferenceKind,
|
_kind: ResultReferenceKind,
|
||||||
data: unknown,
|
data: unknown,
|
||||||
schemaVersion: number,
|
schemaVersion: number,
|
||||||
): unknown | null => {
|
): unknown | null => {
|
||||||
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (kind === RESULT_REFERENCE_KIND.renderJunctionsPayload) {
|
|
||||||
return extractRenderJunctionPayload(data);
|
|
||||||
}
|
|
||||||
return 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<string, unknown> | 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<string, unknown>) =>
|
|
||||||
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<string, unknown> =>
|
|
||||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
||||||
|
|||||||
@@ -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)?$/;
|
const RESULT_REF_FILE_PATTERN = /^(res-[a-f0-9-]{8,64})(?:\.json)?$/;
|
||||||
|
|
||||||
export const RESULT_REFERENCE_KIND = {
|
export const RESULT_REFERENCE_KIND = {
|
||||||
renderJunctionsPayload: "render-junctions-payload",
|
|
||||||
serverApiPayload: "server_api_payload",
|
serverApiPayload: "server_api_payload",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ import { FrontendActionCoordinator } from "../frontendAction/coordinator.js";
|
|||||||
import { frontendActionRegistryResponse } from "../frontendAction/registry.js";
|
import { frontendActionRegistryResponse } from "../frontendAction/registry.js";
|
||||||
import type { UIEnvelopePayload } from "../uiEnvelope/types.js";
|
import type { UIEnvelopePayload } from "../uiEnvelope/types.js";
|
||||||
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
|
||||||
import {
|
|
||||||
createPressureTrendDemoEnvelopePayload,
|
|
||||||
createPressureTrendDemoToolCall,
|
|
||||||
isPressureTrendDemoPrompt,
|
|
||||||
} from "./pressureTrendDemo.js";
|
|
||||||
import {
|
import {
|
||||||
buildForkedSessionUiState,
|
buildForkedSessionUiState,
|
||||||
buildPromptWithLearningContext,
|
buildPromptWithLearningContext,
|
||||||
@@ -1059,39 +1054,6 @@ export const buildChatRouter = (
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
try {
|
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(
|
const preparedMessage = await buildPromptWithLearningContext(
|
||||||
memoryStore,
|
memoryStore,
|
||||||
requestContext.actorKey,
|
requestContext.actorKey,
|
||||||
|
|||||||
@@ -48,43 +48,6 @@ export const registerChatAuxiliaryRoutes = (
|
|||||||
frontendActionCoordinator,
|
frontendActionCoordinator,
|
||||||
}: RegisterAuxiliaryRoutesOptions,
|
}: 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) => {
|
chatRouter.get("/result-ref/:result_ref", async (req, res) => {
|
||||||
const resultRef = req.params.result_ref?.trim();
|
const resultRef = req.params.result_ref?.trim();
|
||||||
const context = getLocalAgentContext(req);
|
const context = getLocalAgentContext(req);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { MemoryStore } from "../memory/store.js";
|
|||||||
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
|
||||||
|
|
||||||
import { collectTextContent } from "./chatStream.js";
|
import { collectTextContent } from "./chatStream.js";
|
||||||
import { isPressureTrendDemoPrompt } from "./pressureTrendDemo.js";
|
|
||||||
|
|
||||||
const TITLE_PROMPT_TIMEOUT_MS = 5000;
|
const TITLE_PROMPT_TIMEOUT_MS = 5000;
|
||||||
const TITLE_CONTEXT_MESSAGE_LIMIT = 40;
|
const TITLE_CONTEXT_MESSAGE_LIMIT = 40;
|
||||||
@@ -241,9 +240,6 @@ export const resolveRequestedStreamSessionId = (input: {
|
|||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
if (isPressureTrendDemoPrompt(input.promptText)) {
|
|
||||||
return input.createClientSessionId();
|
|
||||||
}
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -830,7 +830,7 @@ export const streamPromptResponse = async ({
|
|||||||
params: toolParams,
|
params: toolParams,
|
||||||
reason,
|
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", {
|
write("ui_envelope", {
|
||||||
session_id: clientSessionId,
|
session_id: clientSessionId,
|
||||||
envelope_id: createEnvelopeId(part.tool),
|
envelope_id: createEnvelopeId(part.tool),
|
||||||
|
|||||||
@@ -65,12 +65,7 @@ const toolLabels: Record<string, string> = {
|
|||||||
session_search: "历史会话检索",
|
session_search: "历史会话检索",
|
||||||
skill_manager: "流程沉淀",
|
skill_manager: "流程沉淀",
|
||||||
web_search: "网页搜索",
|
web_search: "网页搜索",
|
||||||
locate_features: "地图定位",
|
|
||||||
zoom_to_map: "地图缩放",
|
zoom_to_map: "地图缩放",
|
||||||
view_history: "历史数据面板",
|
|
||||||
view_scada: "SCADA 面板",
|
|
||||||
show_chart: "图表渲染",
|
|
||||||
render_junctions: "节点渲染",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logDevelopmentDebug = (
|
export const logDevelopmentDebug = (
|
||||||
|
|||||||
@@ -207,17 +207,9 @@ export const updateLastAssistantQuestion = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getToolArtifactKind = (tool: string): ToolArtifactKind => {
|
const getToolArtifactKind = (tool: string): ToolArtifactKind => {
|
||||||
if (tool === "show_chart" || tool === "chart") return "chart";
|
if (tool === "zoom_to_map") {
|
||||||
if (
|
|
||||||
tool === "locate_features" ||
|
|
||||||
tool === "zoom_to_map" ||
|
|
||||||
tool === "render_junctions" ||
|
|
||||||
tool === "apply_layer_style" ||
|
|
||||||
tool.startsWith("locate_")
|
|
||||||
) {
|
|
||||||
return "map";
|
return "map";
|
||||||
}
|
}
|
||||||
if (tool === "view_history" || tool === "view_scada") return "panel";
|
|
||||||
return "tool";
|
return "tool";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -225,13 +217,7 @@ const getToolArtifactTitle = (tool: string, params: Record<string, unknown>) =>
|
|||||||
if (typeof params.title === "string" && params.title.trim()) {
|
if (typeof params.title === "string" && params.title.trim()) {
|
||||||
return params.title.trim();
|
return params.title.trim();
|
||||||
}
|
}
|
||||||
if (tool === "show_chart" || tool === "chart") return "生成图表";
|
|
||||||
if (tool === "zoom_to_map") 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 || "工具调用";
|
return tool || "工具调用";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
+2
-56
@@ -11,10 +11,7 @@ import { logger } from "./logger.js";
|
|||||||
import { LearningOrchestrator } from "./learning/orchestrator.js";
|
import { LearningOrchestrator } from "./learning/orchestrator.js";
|
||||||
import { MemoryStore } from "./memory/store.js";
|
import { MemoryStore } from "./memory/store.js";
|
||||||
import { ResultReferenceResolver } from "./results/resolver.js";
|
import { ResultReferenceResolver } from "./results/resolver.js";
|
||||||
import {
|
import { ResultReferenceStore } from "./results/store.js";
|
||||||
RESULT_REFERENCE_SOURCE,
|
|
||||||
ResultReferenceStore,
|
|
||||||
} from "./results/store.js";
|
|
||||||
import { buildChatRouter } from "./routes/chat.js";
|
import { buildChatRouter } from "./routes/chat.js";
|
||||||
import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js";
|
import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js";
|
||||||
import { opencodeRuntime } from "./runtime/opencode.js";
|
import { opencodeRuntime } from "./runtime/opencode.js";
|
||||||
@@ -44,7 +41,6 @@ const serverApiGateway = new ServerApiGateway(resultReferenceResolver);
|
|||||||
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID();
|
||||||
const frontendActionCoordinator = new FrontendActionCoordinator();
|
const frontendActionCoordinator = new FrontendActionCoordinator();
|
||||||
|
|
||||||
// 这个 token 只用于仍需服务端上下文的工具桥(store_render_ref)。
|
|
||||||
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken;
|
||||||
|
|
||||||
app.use(cors());
|
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) => {
|
app.post("/internal/tools/session-search", async (req, res) => {
|
||||||
if (req.header("x-agent-internal-token") !== internalToken) {
|
if (req.header("x-agent-internal-token") !== internalToken) {
|
||||||
res.status(403).json({ message: "forbidden" });
|
res.status(403).json({ message: "forbidden" });
|
||||||
@@ -340,7 +286,7 @@ app.post("/internal/tools/geocode", async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await callBackendJson(
|
const response = await callBackendJson(
|
||||||
"/api/v1/tianditu/geocode",
|
"/api/v1/geocode",
|
||||||
context,
|
context,
|
||||||
{ keyword },
|
{ keyword },
|
||||||
);
|
);
|
||||||
|
|||||||
+19
-26
@@ -1,4 +1,3 @@
|
|||||||
import { createHash } from "node:crypto";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
@@ -21,11 +20,8 @@ const responseSchemas: Record<DomainToolName, z.ZodTypeAny> = {
|
|||||||
gs_workspace: z.string(),
|
gs_workspace: z.string(),
|
||||||
map_extent: z.object({ xmin: z.number(), ymin: z.number(), xmax: z.number(), ymax: z.number() }).nullable(),
|
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() }),
|
list_scada_assets: z.object({ assets: z.array(z.unknown()) }),
|
||||||
query_monitoring_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }),
|
query_scada_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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ServerApiGateway {
|
export class ServerApiGateway {
|
||||||
@@ -62,33 +58,32 @@ export class ServerApiGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise<unknown> {
|
private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise<unknown> {
|
||||||
if (name === "list_monitoring_assets") {
|
if (name === "get_project_context") {
|
||||||
const result: Record<string, unknown> = {};
|
const projects = await this.request("GET", "/api/v1/projects", undefined, context);
|
||||||
if (args.include !== "points") result.devices = await this.request("GET", "/api/v1/monitoring/devices", undefined, context);
|
if (!Array.isArray(projects)) throw new GatewayHttpError(502);
|
||||||
if (args.include !== "devices") result.points = await this.request("GET", "/api/v1/monitoring/points", undefined, context);
|
const project = projects.find((item) => (
|
||||||
return result;
|
typeof item === "object"
|
||||||
|
&& item !== null
|
||||||
|
&& "project_id" in item
|
||||||
|
&& item.project_id === context.projectId
|
||||||
|
));
|
||||||
|
if (!project) throw new GatewayHttpError(404);
|
||||||
|
return project;
|
||||||
}
|
}
|
||||||
const routes = {
|
if (name === "list_scada_assets") {
|
||||||
get_project_context: ["GET", "/api/v1/meta/project"],
|
return this.request("GET", "/api/v1/scada/assets", undefined, context);
|
||||||
query_monitoring_readings: ["POST", "/api/v1/monitoring/readings/query"],
|
}
|
||||||
start_data_quality_analysis: ["POST", "/api/v1/analyses"],
|
return this.request("POST", "/api/v1/scada/readings/query", args, context);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
|
const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS);
|
||||||
try {
|
try {
|
||||||
const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), {
|
const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), {
|
||||||
method, signal: controller.signal, headers: {
|
method, signal: controller.signal, headers: {
|
||||||
Accept: "application/json", "Content-Type": "application/json", "X-Project-Id": context.projectId!,
|
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) }),
|
}, ...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new GatewayHttpError(response.status);
|
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"); } }
|
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 } : {}) } });
|
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;
|
|
||||||
|
|||||||
@@ -3,31 +3,21 @@ import { z } from "zod";
|
|||||||
const uuid = z.string().uuid();
|
const uuid = z.string().uuid();
|
||||||
const dateTime = z.string().datetime({ offset: true });
|
const dateTime = z.string().datetime({ offset: true });
|
||||||
const metrics = z.enum([
|
const metrics = z.enum([
|
||||||
"flow_mean", "flow_total", "conductivity_mean", "velocity_mean",
|
"conductivity_mean", "level_mean", "flow_mean", "temperature_mean",
|
||||||
"temperature_mean", "level_mean",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const domainToolSchemas = {
|
export const domainToolSchemas = {
|
||||||
get_project_context: z.object({}),
|
get_project_context: z.object({}),
|
||||||
list_monitoring_assets: z.object({ include: z.enum(["devices", "points", "all"]).default("all") }),
|
list_scada_assets: z.object({}),
|
||||||
query_monitoring_readings: z.object({
|
query_scada_readings: z.object({
|
||||||
device_ids: z.array(uuid).min(1).max(100),
|
asset_ids: z.array(uuid).min(1).max(100),
|
||||||
observed_from: dateTime,
|
observed_from: dateTime,
|
||||||
observed_to: dateTime,
|
observed_to: dateTime,
|
||||||
granularity: z.enum(["5m", "1h", "1d"]).default("5m"),
|
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), {
|
}).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), {
|
||||||
message: "observed_from must precede 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;
|
} as const;
|
||||||
|
|
||||||
export type DomainToolName = keyof typeof domainToolSchemas;
|
export type DomainToolName = keyof typeof domainToolSchemas;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { RESULT_REF_PATTERN } from "../results/store.js";
|
|
||||||
import { chartGrammar } from "./registry.js";
|
|
||||||
import type { UIEnvelope } from "./types.js";
|
import type { UIEnvelope } from "./types.js";
|
||||||
|
|
||||||
type ToolCallInput = {
|
type ToolCallInput = {
|
||||||
@@ -8,67 +6,12 @@ type ToolCallInput = {
|
|||||||
reason?: string;
|
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 = ({
|
export const toUiEnvelopeFromToolCall = ({
|
||||||
tool,
|
tool,
|
||||||
params,
|
params,
|
||||||
reason,
|
reason,
|
||||||
}: ToolCallInput): UIEnvelope | null => {
|
}: ToolCallInput): UIEnvelope | null => {
|
||||||
if (tool === "show_chart" || tool === "chart") {
|
if (tool === "zoom_to_map") {
|
||||||
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"
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
kind: "map_action",
|
kind: "map_action",
|
||||||
schemaVersion: "agent-ui@1",
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,51 +2,20 @@ import type { ActionManifest, ComponentManifest } from "./types.js";
|
|||||||
|
|
||||||
export const chartGrammar = "echarts-safe-subset" as const;
|
export const chartGrammar = "echarts-safe-subset" as const;
|
||||||
|
|
||||||
export const componentRegistry: ComponentManifest[] = [
|
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 actionRegistry: ActionManifest[] = [
|
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",
|
id: "zoom_to_map",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
description: "Zoom the map to a coordinate.",
|
description: "Zoom the map to a coordinate.",
|
||||||
supportedSurfaces: ["map_overlay"],
|
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 = {
|
export const uiRegistryResponse = {
|
||||||
schema_version: "agent-ui-registry@1",
|
schema_version: "agent-ui-registry@1",
|
||||||
chart_grammars: [chartGrammar],
|
chart_grammars: [],
|
||||||
components: componentRegistry,
|
components: componentRegistry,
|
||||||
actions: actionRegistry,
|
actions: actionRegistry,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,47 +44,4 @@ describe("FrontendActionCoordinator", () => {
|
|||||||
coordinator.registerSink("s", () => undefined);
|
coordinator.registerSink("s", () => undefined);
|
||||||
expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "unknown", params: {} })).toThrow("unknown frontend action");
|
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" });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ describe("MemoryStore", () => {
|
|||||||
|
|
||||||
it("dedupes exact duplicate memories", async () => {
|
it("dedupes exact duplicate memories", async () => {
|
||||||
const first = await store.upsert("workspace", "project-1", {
|
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",
|
source: "tool",
|
||||||
});
|
});
|
||||||
const second = await store.upsert("workspace", "project-1", {
|
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",
|
source: "tool",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -26,8 +26,8 @@ describe("skill_manager tool", () => {
|
|||||||
const skillDocument = (body: string) =>
|
const skillDocument = (body: string) =>
|
||||||
[
|
[
|
||||||
"---",
|
"---",
|
||||||
"name: pressure-review",
|
"name: scada-level-review",
|
||||||
"description: Pressure review workflow.",
|
"description: SCADA level review workflow.",
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
body,
|
body,
|
||||||
@@ -64,32 +64,32 @@ describe("skill_manager tool", () => {
|
|||||||
await tool.execute(
|
await tool.execute(
|
||||||
{
|
{
|
||||||
action: "write_skill",
|
action: "write_skill",
|
||||||
content: skillDocument("# Pressure Review"),
|
content: skillDocument("# SCADA Level Review"),
|
||||||
reason: "verified reusable workflow",
|
reason: "verified reusable workflow",
|
||||||
skill_path: "workflow/pressure-review",
|
skill_path: "workflow/scada-level-review",
|
||||||
},
|
},
|
||||||
toolContext,
|
toolContext,
|
||||||
) as string,
|
) as string,
|
||||||
);
|
);
|
||||||
expect(writeResult.decision).toBe("accepted");
|
expect(writeResult.decision).toBe("accepted");
|
||||||
await expect(readFile(writeResult.target, "utf8")).resolves.toContain(
|
await expect(readFile(writeResult.target, "utf8")).resolves.toContain(
|
||||||
"# Pressure Review\n",
|
"# SCADA Level Review\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateResult = JSON.parse(
|
const updateResult = JSON.parse(
|
||||||
await tool.execute(
|
await tool.execute(
|
||||||
{
|
{
|
||||||
action: "write_skill",
|
action: "write_skill",
|
||||||
content: skillDocument("# Updated Pressure Review"),
|
content: skillDocument("# Updated SCADA Level Review"),
|
||||||
reason: "verified reusable workflow overwrite",
|
reason: "verified reusable workflow overwrite",
|
||||||
skill_path: "workflow/pressure-review",
|
skill_path: "workflow/scada-level-review",
|
||||||
},
|
},
|
||||||
toolContext,
|
toolContext,
|
||||||
) as string,
|
) as string,
|
||||||
);
|
);
|
||||||
expect(updateResult.decision).toBe("accepted");
|
expect(updateResult.decision).toBe("accepted");
|
||||||
await expect(readFile(updateResult.target, "utf8")).resolves.toContain(
|
await expect(readFile(updateResult.target, "utf8")).resolves.toContain(
|
||||||
"# Updated Pressure Review\n",
|
"# Updated SCADA Level Review\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeResult = JSON.parse(
|
const removeResult = JSON.parse(
|
||||||
@@ -97,7 +97,7 @@ describe("skill_manager tool", () => {
|
|||||||
{
|
{
|
||||||
action: "remove_skill",
|
action: "remove_skill",
|
||||||
reason: "workflow is obsolete",
|
reason: "workflow is obsolete",
|
||||||
skill_path: "workflow/pressure-review",
|
skill_path: "workflow/scada-level-review",
|
||||||
},
|
},
|
||||||
toolContext,
|
toolContext,
|
||||||
) as string,
|
) as string,
|
||||||
|
|||||||
+16
-94
@@ -26,28 +26,26 @@ describe("ResultReferenceResolver", () => {
|
|||||||
await rm(tempDir, { force: true, recursive: true });
|
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({
|
const record = await resolver.register({
|
||||||
actorKey: "actor-1",
|
actorKey: "actor-1",
|
||||||
clientSessionId: "client-1",
|
clientSessionId: "client-1",
|
||||||
data: {
|
data: {
|
||||||
node_area_map: {
|
data: [{ asset_id: "asset-1", value: 1.23 }],
|
||||||
J1: "DMA-1",
|
meta: { rows: 1 },
|
||||||
J2: "DMA-2",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
kind: RESULT_REFERENCE_KIND.serverApiPayload,
|
||||||
projectId: "project-1",
|
projectId: "project-1",
|
||||||
projectKey: "project-key-1",
|
projectKey: "project-key-1",
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
sessionId: "session-1",
|
sessionId: "session-1",
|
||||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
source: RESULT_REFERENCE_SOURCE.serverApi,
|
||||||
traceId: "trace-1",
|
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.schemaVersion).toBe(1);
|
||||||
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated);
|
expect(record.source).toBe(RESULT_REFERENCE_SOURCE.serverApi);
|
||||||
|
|
||||||
const result = await resolver.getFullAuthorized(
|
const result = await resolver.getFullAuthorized(
|
||||||
record.resultRef,
|
record.resultRef,
|
||||||
@@ -58,14 +56,12 @@ describe("ResultReferenceResolver", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
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?.schema_version).toBe(1);
|
||||||
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.agentGenerated);
|
expect(result?.source).toBe(RESULT_REFERENCE_SOURCE.serverApi);
|
||||||
expect(result?.data).toEqual({
|
expect(result?.data).toEqual({
|
||||||
node_area_map: {
|
data: [{ asset_id: "asset-1", value: 1.23 }],
|
||||||
J1: "DMA-1",
|
meta: { rows: 1 },
|
||||||
J2: "DMA-2",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -102,97 +98,23 @@ describe("ResultReferenceResolver", () => {
|
|||||||
});
|
});
|
||||||
expect(malformed).toBeNull();
|
expect(malformed).toBeNull();
|
||||||
|
|
||||||
const renderRecord = await resolver.register({
|
const serverRecord = await resolver.register({
|
||||||
actorKey: "actor-2",
|
actorKey: "actor-2",
|
||||||
clientSessionId: "client-2",
|
clientSessionId: "client-2",
|
||||||
data: {
|
data: { data: [{ asset_id: "asset-2" }] },
|
||||||
node_area_map: {
|
kind: RESULT_REFERENCE_KIND.serverApiPayload,
|
||||||
J1: "DMA-1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
kind: RESULT_REFERENCE_KIND.renderJunctionsPayload,
|
|
||||||
projectId: "project-2",
|
projectId: "project-2",
|
||||||
projectKey: "project-key-2",
|
projectKey: "project-key-2",
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
sessionId: "session-2",
|
sessionId: "session-2",
|
||||||
source: RESULT_REFERENCE_SOURCE.agentGenerated,
|
source: RESULT_REFERENCE_SOURCE.serverApi,
|
||||||
traceId: "trace-2",
|
traceId: "trace-2",
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrongActor = await resolver.getFullAuthorized(renderRecord.resultRef, {
|
const wrongActor = await resolver.getFullAuthorized(serverRecord.resultRef, {
|
||||||
actorKey: "actor-other",
|
actorKey: "actor-other",
|
||||||
projectId: "project-2",
|
projectId: "project-2",
|
||||||
});
|
});
|
||||||
expect(wrongActor).toBeNull();
|
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",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,26 +22,26 @@ describe("resolveRequestedStreamSessionId", () => {
|
|||||||
expect(
|
expect(
|
||||||
resolveRequestedStreamSessionId({
|
resolveRequestedStreamSessionId({
|
||||||
sessionId: " existing-session ",
|
sessionId: " existing-session ",
|
||||||
promptText: "检查压力异常",
|
promptText: "检查 SCADA 液位异常",
|
||||||
createClientSessionId,
|
createClientSessionId,
|
||||||
}),
|
}),
|
||||||
).toBe("existing-session");
|
).toBe("existing-session");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
resolveRequestedStreamSessionId({
|
resolveRequestedStreamSessionId({
|
||||||
promptText: "检查压力异常",
|
promptText: "检查 SCADA 液位异常",
|
||||||
createClientSessionId,
|
createClientSessionId,
|
||||||
}),
|
}),
|
||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the pressure trend demo on its generated client session id", () => {
|
it("does not special-case removed SCADA trend demo prompts", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveRequestedStreamSessionId({
|
resolveRequestedStreamSessionId({
|
||||||
promptText: "展示最近压力趋势",
|
promptText: "展示最近液位趋势",
|
||||||
createClientSessionId: () => "demo-session",
|
createClientSessionId: () => "demo-session",
|
||||||
}),
|
}),
|
||||||
).toBe("demo-session");
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ describe("generateSessionTitle", () => {
|
|||||||
messages: async () => [
|
messages: async () => [
|
||||||
{
|
{
|
||||||
info: { role: "assistant" },
|
info: { role: "assistant" },
|
||||||
parts: [{ type: "text", text: "标题:泵站压力异常排查。" }],
|
parts: [{ type: "text", text: "标题:SCADA 液位异常排查。" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
abortSession: async () => undefined,
|
abortSession: async () => undefined,
|
||||||
@@ -100,14 +100,14 @@ describe("generateSessionTitle", () => {
|
|||||||
|
|
||||||
const title = await generateSessionTitle(runtime, {
|
const title = await generateSessionTitle(runtime, {
|
||||||
sessionId: "chat-session",
|
sessionId: "chat-session",
|
||||||
latestUserMessage: "检查一下三号泵站最近压力波动的原因",
|
latestUserMessage: "检查一下 82c7d04c 资产最近液位波动的原因",
|
||||||
latestAssistantMessage: "三号泵站压力波动主要与夜间阀门开度变化有关。",
|
latestAssistantMessage: "该 SCADA 资产液位波动主要与夜间水位快速变化时段重合。",
|
||||||
fallbackTitle: "新对话",
|
fallbackTitle: "新对话",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(title).toBe("泵站压力异常排查");
|
expect(title).toBe("SCADA 液位异常排查");
|
||||||
expect(titlePrompt).toContain("用户:检查一下三号泵站最近压力波动的原因");
|
expect(titlePrompt).toContain("用户:检查一下 82c7d04c 资产最近液位波动的原因");
|
||||||
expect(titlePrompt).toContain("助手:三号泵站压力波动主要与夜间阀门开度变化有关。");
|
expect(titlePrompt).toContain("助手:该 SCADA 资产液位波动主要与夜间水位快速变化时段重合。");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -124,32 +124,32 @@ describe("buildPromptWithLearningContext", () => {
|
|||||||
{
|
{
|
||||||
recentTurns: [],
|
recentTurns: [],
|
||||||
persistedMessages: [
|
persistedMessages: [
|
||||||
{ role: "user", content: "先分析 3 号泵站夜间压力波动" },
|
{ role: "user", content: "先分析 82c7d04c 资产夜间液位波动" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。",
|
content: "已定位到夜间液位变化与读数异常时段重合,下一步准备对比温度和液位曲线。",
|
||||||
isError: true,
|
isError: true,
|
||||||
},
|
},
|
||||||
{ role: "assistant", content: "⚠️ **请求已中断**", isError: true },
|
{ role: "assistant", content: "⚠️ **请求已中断**", isError: true },
|
||||||
],
|
],
|
||||||
message: "继续刚才的分析,并补充相邻支路影响",
|
message: "继续刚才的分析,并补充温度指标对照",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(prompt).toContain("用户:先分析 3 号泵站夜间压力波动");
|
expect(prompt).toContain("用户:先分析 82c7d04c 资产夜间液位波动");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"助手:已定位到夜间阀门开度变化与压力波动时间段重合,下一步准备对比相邻支路。",
|
"助手:已定位到夜间液位变化与读数异常时段重合,下一步准备对比温度和液位曲线。",
|
||||||
);
|
);
|
||||||
expect(prompt).not.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 () => {
|
it("falls back to history turns when frontend state is unavailable", async () => {
|
||||||
const recentTurns: SessionTurnRecord[] = [
|
const recentTurns: SessionTurnRecord[] = [
|
||||||
{
|
{
|
||||||
id: "turn-1",
|
id: "turn-1",
|
||||||
userMessage: "检查 DMA-2 夜间漏损异常",
|
userMessage: "检查 82c7d04c 资产夜间液位异常",
|
||||||
assistantMessage: "DMA-2 在 02:00-04:00 出现持续最小夜流抬升。",
|
assistantMessage: "82c7d04c 在 02:00-04:00 出现持续液位抬升。",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
toolCallCount: 1,
|
toolCallCount: 1,
|
||||||
},
|
},
|
||||||
@@ -161,12 +161,12 @@ describe("buildPromptWithLearningContext", () => {
|
|||||||
"project-1",
|
"project-1",
|
||||||
{
|
{
|
||||||
recentTurns,
|
recentTurns,
|
||||||
message: "继续给出排查建议",
|
message: "继续给出数据核查建议",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(prompt).toContain("用户:检查 DMA-2 夜间漏损异常");
|
expect(prompt).toContain("用户:检查 82c7d04c 资产夜间液位异常");
|
||||||
expect(prompt).toContain("助手:DMA-2 在 02:00-04:00 出现持续最小夜流抬升。");
|
expect(prompt).toContain("助手:82c7d04c 在 02:00-04:00 出现持续液位抬升。");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips restored conversation injection when reusing an existing opencode session", async () => {
|
it("skips restored conversation injection when reusing an existing opencode session", async () => {
|
||||||
@@ -216,20 +216,20 @@ describe("buildPromptWithLearningContext", () => {
|
|||||||
describe("extractLatestFrontendTurn", () => {
|
describe("extractLatestFrontendTurn", () => {
|
||||||
it("extracts the latest valid frontend user and assistant turn", () => {
|
it("extracts the latest valid frontend user and assistant turn", () => {
|
||||||
const turn = extractLatestFrontendTurn([
|
const turn = extractLatestFrontendTurn([
|
||||||
{ role: "user", content: "检查 DMA-2 漏损" },
|
{ role: "user", content: "检查 82c7d04c 液位异常" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "DMA-2 夜间最小流量持续抬升。",
|
content: "82c7d04c 夜间液位持续抬升。",
|
||||||
progress: [{ id: "tool-dma", phase: "tool" }],
|
progress: [{ id: "tool-dma", phase: "tool" }],
|
||||||
},
|
},
|
||||||
{ role: "user", content: "继续分析相邻分区" },
|
{ role: "user", content: "继续分析同类资产" },
|
||||||
{ role: "assistant", content: "⚠️ **请求已中断**", isError: true },
|
{ role: "assistant", content: "⚠️ **请求已中断**", isError: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(turn).toEqual({
|
expect(turn).toEqual({
|
||||||
assistantMessage: "DMA-2 夜间最小流量持续抬升。",
|
assistantMessage: "82c7d04c 夜间液位持续抬升。",
|
||||||
toolCallCount: 1,
|
toolCallCount: 1,
|
||||||
userMessage: "检查 DMA-2 漏损",
|
userMessage: "检查 82c7d04c 液位异常",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -239,7 +239,7 @@ describe("buildForkedSessionUiState", () => {
|
|||||||
const forked = buildForkedSessionUiState(
|
const forked = buildForkedSessionUiState(
|
||||||
{
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "user", content: "画压力曲线" },
|
{ role: "user", content: "画液位曲线" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "已生成图表",
|
content: "已生成图表",
|
||||||
@@ -265,7 +265,7 @@ describe("buildForkedSessionUiState", () => {
|
|||||||
sessionId: "forked-session",
|
sessionId: "forked-session",
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: "user", content: "画压力曲线" },
|
{ role: "user", content: "画液位曲线" },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "已生成图表",
|
content: "已生成图表",
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|||||||
|
|
||||||
describe("streamPromptResponse", () => {
|
describe("streamPromptResponse", () => {
|
||||||
it("does not split short Chinese text only because it has punctuation", () => {
|
it("does not split short Chinese text only because it has punctuation", () => {
|
||||||
expect(detectTokenSmoothingChunk("管网压力异常,需要继续分析", "zh")).toBeNull();
|
expect(detectTokenSmoothingChunk("SCADA 液位异常,需要继续分析", "zh")).toBeNull();
|
||||||
expect(detectTokenSmoothingChunk("管网压力异常需要继续分析。", "zh")).toBeNull();
|
expect(detectTokenSmoothingChunk("SCADA 液位异常需要继续分析。", "zh")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not immediately release a single Chinese character", () => {
|
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", () => {
|
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", () => {
|
it("falls back to bounded Chinese chunks when punctuation is absent", () => {
|
||||||
const content = "管网压力异常需要继续分析东部主干供水走廊和相关阀门状态变化";
|
const content = "SCADA 液位异常需要继续分析水质温度和液位读数变化";
|
||||||
const chunk = detectTokenSmoothingChunk(content, "zh");
|
const chunk = detectTokenSmoothingChunk(content, "zh");
|
||||||
|
|
||||||
expect(chunk).not.toBeNull();
|
expect(chunk).not.toBeNull();
|
||||||
@@ -72,11 +72,11 @@ describe("streamPromptResponse", () => {
|
|||||||
write: (event, data) => events.push({ event, data }),
|
write: (event, data) => events.push({ event, data }),
|
||||||
});
|
});
|
||||||
|
|
||||||
smoother.writeToken("管");
|
smoother.writeToken("液");
|
||||||
await sleep(8);
|
await sleep(8);
|
||||||
expect(events).toHaveLength(0);
|
expect(events).toHaveLength(0);
|
||||||
|
|
||||||
const content = "网压力异常需要继续分析东部主干供水走廊和相关阀门状态";
|
const content = "位异常需要继续分析水质温度和液位读数持续变化趋势";
|
||||||
for (const char of Array.from(content)) {
|
for (const char of Array.from(content)) {
|
||||||
smoother.writeToken(char);
|
smoother.writeToken(char);
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ describe("streamPromptResponse", () => {
|
|||||||
|
|
||||||
expect(events.length).toBeGreaterThan(0);
|
expect(events.length).toBeGreaterThan(0);
|
||||||
await smoother.flush();
|
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 () => {
|
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 }),
|
write: (event, data) => events.push({ event, data }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const content = "管网压力异常需要继续分析东部主干供水走廊和相关阀门状态变化";
|
const content = "SCADA 液位异常需要继续分析水质温度和液位读数变化";
|
||||||
for (const char of Array.from(content)) {
|
for (const char of Array.from(content)) {
|
||||||
smoother.writeToken(char);
|
smoother.writeToken(char);
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ describe("streamPromptResponse", () => {
|
|||||||
sessionID: "runtime-session-1",
|
sessionID: "runtime-session-1",
|
||||||
partID: "text-part-1",
|
partID: "text-part-1",
|
||||||
field: "text",
|
field: "text",
|
||||||
delta: "管网压力异常需要继续分析",
|
delta: "SCADA 液位异常需要继续分析",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -142,7 +142,7 @@ describe("streamPromptResponse", () => {
|
|||||||
sessionID: "runtime-session-1",
|
sessionID: "runtime-session-1",
|
||||||
partID: "text-part-1",
|
partID: "text-part-1",
|
||||||
field: "text",
|
field: "text",
|
||||||
delta: "东部主干供水走廊和相关阀门状态。",
|
delta: "水质温度和液位读数。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -172,7 +172,7 @@ describe("streamPromptResponse", () => {
|
|||||||
expect(tokenEvents.length).toBeGreaterThan(0);
|
expect(tokenEvents.length).toBeGreaterThan(0);
|
||||||
expect(tokenEvents.every((item) => events.indexOf(item) < doneIndex)).toBe(true);
|
expect(tokenEvents.every((item) => events.indexOf(item) < doneIndex)).toBe(true);
|
||||||
expect(tokenEvents.map((item) => item.data.content).join("")).toBe(
|
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 = {
|
const runtime = {
|
||||||
subscribeEvents: async () =>
|
subscribeEvents: async () =>
|
||||||
createEventStream([
|
createEventStream([
|
||||||
@@ -544,20 +544,19 @@ describe("streamPromptResponse", () => {
|
|||||||
properties: {
|
properties: {
|
||||||
sessionID: "runtime-session-1",
|
sessionID: "runtime-session-1",
|
||||||
part: {
|
part: {
|
||||||
id: "tool-part-chart",
|
id: "tool-part-map",
|
||||||
sessionID: "runtime-session-1",
|
sessionID: "runtime-session-1",
|
||||||
messageID: "message-1",
|
messageID: "message-1",
|
||||||
type: "tool",
|
type: "tool",
|
||||||
callID: "call-chart",
|
callID: "call-map",
|
||||||
tool: "show_chart",
|
tool: "zoom_to_map",
|
||||||
state: {
|
state: {
|
||||||
status: "running",
|
status: "running",
|
||||||
input: {
|
input: {
|
||||||
reason: "展示压力趋势",
|
reason: "定位 SCADA 资产附近位置",
|
||||||
title: "压力趋势",
|
x: 121.47,
|
||||||
chart_type: "line",
|
y: 31.23,
|
||||||
x_data: ["00:00", "01:00"],
|
source_crs: "EPSG:4326",
|
||||||
series: [{ name: "P-1", data: [0.4, 0.42] }],
|
|
||||||
},
|
},
|
||||||
time: { start: Date.now() },
|
time: { start: Date.now() },
|
||||||
},
|
},
|
||||||
@@ -587,14 +586,15 @@ describe("streamPromptResponse", () => {
|
|||||||
|
|
||||||
expect(events.find((item) => item.event === "tool_call")?.data).toMatchObject({
|
expect(events.find((item) => item.event === "tool_call")?.data).toMatchObject({
|
||||||
session_id: "client-session-1",
|
session_id: "client-session-1",
|
||||||
tool: "show_chart",
|
tool: "zoom_to_map",
|
||||||
});
|
});
|
||||||
expect(events.find((item) => item.event === "ui_envelope")?.data).toMatchObject({
|
expect(events.find((item) => item.event === "ui_envelope")?.data).toMatchObject({
|
||||||
session_id: "client-session-1",
|
session_id: "client-session-1",
|
||||||
envelope: {
|
envelope: {
|
||||||
kind: "chart",
|
kind: "map_action",
|
||||||
schemaVersion: "agent-ui@1",
|
schemaVersion: "agent-ui@1",
|
||||||
grammar: "echarts-safe-subset",
|
action: "zoom_to_map",
|
||||||
|
surface: "map_overlay",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ import {
|
|||||||
} from "../../src/routes/chatUiState.js";
|
} from "../../src/routes/chatUiState.js";
|
||||||
|
|
||||||
describe("appendBackendToolArtifact", () => {
|
describe("appendBackendToolArtifact", () => {
|
||||||
it("persists show_chart tool calls as chart artifacts", () => {
|
it("persists removed visual tool calls as generic tool artifacts", () => {
|
||||||
const artifacts = appendBackendToolArtifact([], {
|
const artifacts = appendBackendToolArtifact([], {
|
||||||
session_id: "session-1",
|
session_id: "session-1",
|
||||||
tool: "show_chart",
|
tool: "show_chart",
|
||||||
reason: "测试折线图渲染",
|
reason: "测试折线图渲染",
|
||||||
params: {
|
params: {
|
||||||
title: "压力曲线",
|
title: "液位曲线",
|
||||||
chart_type: "line",
|
chart_type: "line",
|
||||||
x_data: ["00:00", "01:00"],
|
x_data: ["00:00", "01:00"],
|
||||||
series: [{ name: "P-101", data: [0.42, 0.41] }],
|
series: [{ name: "P-101", data: [0.42, 0.41] }],
|
||||||
@@ -24,8 +24,8 @@ describe("appendBackendToolArtifact", () => {
|
|||||||
expect(artifacts).toHaveLength(1);
|
expect(artifacts).toHaveLength(1);
|
||||||
expect(artifacts[0]).toMatchObject({
|
expect(artifacts[0]).toMatchObject({
|
||||||
tool: "show_chart",
|
tool: "show_chart",
|
||||||
kind: "chart",
|
kind: "tool",
|
||||||
title: "压力曲线",
|
title: "液位曲线",
|
||||||
description: "测试折线图渲染",
|
description: "测试折线图渲染",
|
||||||
params: {
|
params: {
|
||||||
chart_type: "line",
|
chart_type: "line",
|
||||||
@@ -50,9 +50,9 @@ describe("appendBackendUiEnvelope", () => {
|
|||||||
envelope: {
|
envelope: {
|
||||||
kind: "map_action",
|
kind: "map_action",
|
||||||
schemaVersion: "agent-ui@1",
|
schemaVersion: "agent-ui@1",
|
||||||
action: "locate_features",
|
action: "zoom_to_map",
|
||||||
surface: "map_overlay",
|
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: {
|
envelope: {
|
||||||
kind: "map_action",
|
kind: "map_action",
|
||||||
schemaVersion: "agent-ui@1",
|
schemaVersion: "agent-ui@1",
|
||||||
action: "locate_features",
|
action: "zoom_to_map",
|
||||||
surface: "map_overlay",
|
surface: "map_overlay",
|
||||||
params: { ids: ["J-1"], feature_type: "junction" },
|
params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" },
|
||||||
},
|
},
|
||||||
renderStatus: "pending",
|
renderStatus: "pending",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -16,38 +16,38 @@ describe("ServerApiGateway project context", () => {
|
|||||||
let requestedPath = "";
|
let requestedPath = "";
|
||||||
const fetchImpl = (async (input: URL | RequestInfo) => {
|
const fetchImpl = (async (input: URL | RequestInfo) => {
|
||||||
requestedPath = new URL(input.toString()).pathname;
|
requestedPath = new URL(input.toString()).pathname;
|
||||||
return Response.json({
|
return Response.json([{
|
||||||
project_id: context.projectId,
|
project_id: context.projectId,
|
||||||
code: "lingang",
|
code: "lingang",
|
||||||
name: "临港排水",
|
name: "SCADA 物联网监测",
|
||||||
description: null,
|
description: null,
|
||||||
status: "active",
|
status: "active",
|
||||||
project_role: "owner",
|
project_role: "owner",
|
||||||
gs_workspace: "lingang",
|
gs_workspace: "lingang",
|
||||||
map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 },
|
map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 },
|
||||||
});
|
}]);
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
const gateway = new ServerApiGateway({} as never, fetchImpl);
|
const gateway = new ServerApiGateway({} as never, fetchImpl);
|
||||||
|
|
||||||
const result = await gateway.execute("get_project_context", {}, context);
|
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({
|
expect(result).toMatchObject({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: { project_id: context.projectId, status: "active", project_role: "owner" },
|
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 () =>
|
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 gateway = new ServerApiGateway({} as never, fetchImpl);
|
||||||
|
|
||||||
const result = await gateway.execute("get_project_context", {}, context);
|
const result = await gateway.execute("get_project_context", {}, context);
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: { code: "UPSTREAM_UNAVAILABLE" },
|
error: { code: "NOT_FOUND" },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +1,17 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { domainToolSchemas } from "../../src/serverApi/schemas.js";
|
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", () => {
|
describe("domain tool validation", () => {
|
||||||
test("rejects unknown metrics and reversed time", () => {
|
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" };
|
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_monitoring_readings.safeParse({ ...base, metrics: ["pressure"] }).success).toBeFalse();
|
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["ph_mean"] }).success).toBeFalse();
|
||||||
expect(domainToolSchemas.query_monitoring_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse();
|
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("rejects invalid UUID before dispatch", () => {
|
test("accepts four current metrics and rejects removed metrics", () => {
|
||||||
expect(domainToolSchemas.start_simulation.safeParse({ model_version_id: "bad", parameters: {} }).success).toBeFalse();
|
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();
|
||||||
test("idempotency key ignores object key order", () => {
|
expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["velocity_mean"] }).success).toBeFalse();
|
||||||
expect(createIdempotencyKey(context, "start_simulation", { b: 2, a: 1 })).toBe(createIdempotencyKey(context, "start_simulation", { a: 1, b: 2 }));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,9 +88,9 @@ describe("SessionTranscriptStore", () => {
|
|||||||
sessionId: "thread-3",
|
sessionId: "thread-3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
assistantMessage: "已完成压力波动分析。",
|
assistantMessage: "已完成液位波动分析。",
|
||||||
toolCallCount: 1,
|
toolCallCount: 1,
|
||||||
userMessage: "分析压力波动。",
|
userMessage: "分析液位波动。",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,9 +102,9 @@ describe("SessionTranscriptStore", () => {
|
|||||||
sessionId: "thread-3",
|
sessionId: "thread-3",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
assistantMessage: "已完成压力波动分析。",
|
assistantMessage: "已完成液位波动分析。",
|
||||||
toolCallCount: 2,
|
toolCallCount: 2,
|
||||||
userMessage: "分析压力波动。",
|
userMessage: "分析液位波动。",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe("SkillStore", () => {
|
|||||||
process.chdir(alternateCwd);
|
process.chdir(alternateCwd);
|
||||||
|
|
||||||
const result = await store.writeScript(
|
const result = await store.writeScript(
|
||||||
"workflow/hydraulic-bottleneck-analysis",
|
"workflow/scada-level-review",
|
||||||
"scripts/analyze.py",
|
"scripts/analyze.py",
|
||||||
"print('ok')\n",
|
"print('ok')\n",
|
||||||
);
|
);
|
||||||
@@ -53,7 +53,7 @@ describe("SkillStore", () => {
|
|||||||
target: join(
|
target: join(
|
||||||
skillsRoot,
|
skillsRoot,
|
||||||
"workflow",
|
"workflow",
|
||||||
"hydraulic-bottleneck-analysis",
|
"scada-level-review",
|
||||||
"scripts",
|
"scripts",
|
||||||
"analyze.py",
|
"analyze.py",
|
||||||
),
|
),
|
||||||
@@ -63,7 +63,7 @@ describe("SkillStore", () => {
|
|||||||
|
|
||||||
it("rejects script paths outside scripts/*.py", async () => {
|
it("rejects script paths outside scripts/*.py", async () => {
|
||||||
const result = await store.writeScript(
|
const result = await store.writeScript(
|
||||||
"workflow/hydraulic-bottleneck-analysis",
|
"workflow/scada-level-review",
|
||||||
"analyze.ts",
|
"analyze.ts",
|
||||||
"console.log('ok')\n",
|
"console.log('ok')\n",
|
||||||
);
|
);
|
||||||
@@ -76,21 +76,21 @@ describe("SkillStore", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("writes and overwrites the main skill file", async () => {
|
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(
|
const writeResult = await store.writeSkill(
|
||||||
skillPath,
|
skillPath,
|
||||||
skillDocument("pressure-review", "# Pressure Review"),
|
skillDocument("scada-level-review", "# SCADA Level Review"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(writeResult).toEqual({
|
expect(writeResult).toEqual({
|
||||||
changed: true,
|
changed: true,
|
||||||
detail: "skill written",
|
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(
|
const overwriteResult = await store.writeSkill(
|
||||||
skillPath,
|
skillPath,
|
||||||
skillDocument("pressure-review", "# Updated Pressure Review"),
|
skillDocument("scada-level-review", "# Updated SCADA Level Review"),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(overwriteResult).toEqual({
|
expect(overwriteResult).toEqual({
|
||||||
@@ -99,7 +99,7 @@ describe("SkillStore", () => {
|
|||||||
target: writeResult.target,
|
target: writeResult.target,
|
||||||
});
|
});
|
||||||
await expect(readFile(writeResult.target, "utf8")).resolves.toContain(
|
await expect(readFile(writeResult.target, "utf8")).resolves.toContain(
|
||||||
"# Updated Pressure Review\n",
|
"# Updated SCADA Level Review\n",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,51 +3,24 @@ import { describe, expect, it } from "bun:test";
|
|||||||
import { toUiEnvelopeFromToolCall } from "../../src/uiEnvelope/fromToolCall.js";
|
import { toUiEnvelopeFromToolCall } from "../../src/uiEnvelope/fromToolCall.js";
|
||||||
|
|
||||||
describe("toUiEnvelopeFromToolCall", () => {
|
describe("toUiEnvelopeFromToolCall", () => {
|
||||||
it("maps show_chart calls to chart envelopes", () => {
|
it("maps zoom_to_map calls to map action envelopes", () => {
|
||||||
const envelope = toUiEnvelopeFromToolCall({
|
const envelope = toUiEnvelopeFromToolCall({
|
||||||
tool: "show_chart",
|
tool: "zoom_to_map",
|
||||||
reason: "展示压力趋势",
|
reason: "定位 SCADA 资产附近位置",
|
||||||
params: {
|
params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" },
|
||||||
title: "压力趋势",
|
|
||||||
chart_type: "line",
|
|
||||||
x_data: ["00:00", "01:00"],
|
|
||||||
series: [{ name: "P-1", data: [0.41, 0.43] }],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(envelope).toMatchObject({
|
expect(envelope).toMatchObject({
|
||||||
kind: "chart",
|
kind: "map_action",
|
||||||
schemaVersion: "agent-ui@1",
|
action: "zoom_to_map",
|
||||||
grammar: "echarts-safe-subset",
|
surface: "map_overlay",
|
||||||
surface: "chat_inline",
|
params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" },
|
||||||
fallbackText: "展示压力趋势",
|
fallbackText: "定位 SCADA 资产附近位置",
|
||||||
data: {
|
|
||||||
x_data: ["00:00", "01:00"],
|
|
||||||
series: [{ name: "P-1", data: [0.41, 0.43] }],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects render_junctions without a valid render_ref", () => {
|
it("does not map removed visual tools", () => {
|
||||||
expect(
|
expect(toUiEnvelopeFromToolCall({ tool: "show_chart", params: {} })).toBeNull();
|
||||||
toUiEnvelopeFromToolCall({
|
expect(toUiEnvelopeFromToolCall({ tool: "view_scada", params: { asset_ids: ["A-1"] } })).toBeNull();
|
||||||
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" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user