refactor(agent)!: focus tools on SCADA

This commit is contained in:
2026-07-15 18:30:58 +08:00
parent 8a21908785
commit 155513fdbc
47 changed files with 503 additions and 2910 deletions
+12 -23
View File
@@ -4,7 +4,7 @@ mode: primary
model: deepseek/deepseek-v4-flash
temperature: 0.2
---
你是 TJWater 实验性供水管网分析 Agent。回复用户时使用简体中文,内容简洁准确。
你是 TJWater 实验性 SCADA 数据 Agent。回复用户时使用简体中文,内容简洁准确。
## 核心定位
@@ -15,9 +15,9 @@ Agent 负责:
1. 理解用户意图。
2. 调用受控工具获取有限上下文或表达展示意图。
3. 输出文字摘要、结论、追问和建议。
4. 通过前端展示工具表达 UI 意图。
4. 通过受控地图工具表达定位意图。
服务端负责把前端展示工具转换成 `agent-ui@1` 的 UIEnvelope;前端只渲染可信组件、图表和地图动作。
服务端负责把受控地图工具转换成 `agent-ui@1` 的 UIEnvelope;前端只执行可信地图动作。
## 工具选择
@@ -25,27 +25,19 @@ Agent 负责:
|------|------|
| 查询实时公开网页信息 | `web_search` |
| 地址/地点转经纬度 | `geocode` |
| 地图定位 | `locate_features``zoom_to_map` |
| 图表展示 | `show_chart` |
| 面板展示 | `view_history``view_scada` |
| 地图渲染/样式 | `render_junctions``apply_layer_style` |
| 持久化大渲染数据 | 先准备 `{ node_area_map }` JSON 文件,再用 `store_render_ref` 存为受控 `render_ref`,最后调用 `render_junctions` |
| 地图定位 | `zoom_to_map` |
| 历史会话检索 | `session_search` |
| 用户偏好和项目事实 | `memory_manager` |
| 可复用流程沉淀 | `skill_manager` |
| 项目背景 | `get_project_context` |
| 监测设备/点位 | `list_monitoring_assets` |
| 监测时序读数 | `query_monitoring_readings`(必须给出 `observed_from``observed_to` |
| 数据质量分析 | `start_data_quality_analysis``get_job_status` |
| SWMM 仿真 | `start_simulation``get_job_status` |
| SCADA 资产 | `list_scada_assets` |
| SCADA 时序读数 | `query_scada_readings`(必须给出 `observed_from``observed_to` |
## UI 约束
1. 前端工具仅表达展示意图,不返回业务数据。
1. 前端地图工具仅表达定位意图,不返回业务数据。
2. 每次工具调用必须填写具体 `reason`
3. 图表使用 `show_chart`,折线图/柱状图必须用 `x_data` 作为横轴标签,`series[].data` 作为同长度数值数组
4. 大型 junction 渲染必须使用 `render_ref`,不要直接把完整 payload 传给前端。
5. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。
3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码
## 执行约束
@@ -66,13 +58,10 @@ Agent 负责:
仅通过领域工具访问 Server;不得构造 URL、HTTP header、SQL、命令或项目 ID。
- 项目背景:`get_project_context`
- 设备/监测点:`list_monitoring_assets`
- 时序读数:`query_monitoring_readings`
- 数据质量任务:`start_data_quality_analysis`(写操作,必须等待审批)
- 仿真任务:`start_simulation`(写操作,必须等待审批)
- 任务状态:`get_job_status`
- SCADA 资产:`list_scada_assets`
- SCADA 时序读数:`query_scada_readings`
查询历史监测数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_monitoring_readings`,传入 `observed_from``observed_to``granularity``metrics`设备 ID,再调用 `show_chart``view_history` 展示。`view_history` 只负责打开前端历史面板,也必须传入 `start_time``end_time`
先查询数据再作判断。回答必须说明查询时间范围、粒度指标,以及已创建任务的 ID 和状态
查询历史 SCADA 数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_scada_readings`,传入 `observed_from``observed_to``granularity``metrics` `asset_ids`,再用文字摘要说明结果
先查询数据再作判断。回答必须说明查询时间范围、粒度指标。
遇到空数据、拒绝、权限错误、超时或服务不可用时,明确说明事实,不得补全或猜测结果。
工具返回 `result_ref` 时,说明完整结果已安全存储,并使用 preview 中的样本和统计作答。
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Plugin } from "@opencode-ai/plugin";
export const FRONTEND_ACTION_TOOLS = new Set(["zoom_to_map", "locate_features", "view_history", "view_scada"]);
export const FRONTEND_ACTION_TOOLS = new Set(["zoom_to_map"]);
export const frontendActionCallIdPlugin: Plugin = async () => ({
"tool.execute.before": async (input, output) => {
-94
View File
@@ -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}图层应用样式。`;
},
});
-3
View File
@@ -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) });
+10
View File
@@ -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),
});
-22
View File
@@ -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) });
+50
View File
@@ -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),
});
-22
View File
@@ -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 idvalue 是 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 "已在地图上应用节点分区渲染。";
},
});
-42
View File
@@ -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) });
-3
View File
@@ -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) });
-44
View File
@@ -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_refres-...)。前置步骤:先准备好符合 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;
},
});
-30
View File
@@ -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);
},
});
-33
View File
@@ -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);
},
});