From 72019ac1f25c3d31db4836e83db0fd157046bb57 Mon Sep 17 00:00:00 2001 From: Huarch Date: Mon, 20 Jul 2026 17:27:46 +0800 Subject: [PATCH] chore: disable external and frontend tools --- .opencode/agents/instruction.md | 39 ++--- .opencode/plugins/frontend-action-call-id.ts | 14 -- .opencode/tools/frontend_action.ts | 27 --- .opencode/tools/geocode.ts | 37 ---- .opencode/tools/get_project_context.ts | 3 - .opencode/tools/list_scada_assets.ts | 10 -- .opencode/tools/query_scada_readings.ts | 50 ------ .opencode/tools/server_api_shared.ts | 13 -- .opencode/tools/web_search.ts | 62 ------- .opencode/tools/zoom_to_map.ts | 33 ---- README.md | 34 ++-- docs/agent-ui-protocol.md | 13 +- opencode.json | 3 - src/config.ts | 14 -- src/frontendAction/registry.ts | 5 +- src/routes/chatStream.ts | 2 +- src/routes/chatStreamEvents.ts | 3 - src/routes/chatUiState.ts | 4 - src/server.ts | 175 +------------------ src/serverApi/gateway.ts | 96 ---------- src/serverApi/schemas.ts | 24 --- src/uiEnvelope/fromToolCall.ts | 18 +- src/uiEnvelope/registry.ts | 9 +- tests/frontendAction/callIdPlugin.test.ts | 42 ----- tests/frontendAction/coordinator.test.ts | 41 +---- tests/opencode/disabledTools.test.ts | 22 +++ tests/opencode/scadaToolsDescription.test.ts | 19 -- tests/routes/chatStream.test.ts | 63 ------- tests/serverApi/gateway.test.ts | 53 ------ tests/serverApi/schemas.test.ts | 17 -- tests/uiEnvelope/fromToolCall.test.ts | 17 +- 31 files changed, 63 insertions(+), 899 deletions(-) delete mode 100644 .opencode/plugins/frontend-action-call-id.ts delete mode 100644 .opencode/tools/frontend_action.ts delete mode 100644 .opencode/tools/geocode.ts delete mode 100644 .opencode/tools/get_project_context.ts delete mode 100644 .opencode/tools/list_scada_assets.ts delete mode 100644 .opencode/tools/query_scada_readings.ts delete mode 100644 .opencode/tools/server_api_shared.ts delete mode 100644 .opencode/tools/web_search.ts delete mode 100644 .opencode/tools/zoom_to_map.ts delete mode 100644 src/serverApi/gateway.ts delete mode 100644 src/serverApi/schemas.ts delete mode 100644 tests/frontendAction/callIdPlugin.test.ts create mode 100644 tests/opencode/disabledTools.test.ts delete mode 100644 tests/opencode/scadaToolsDescription.test.ts delete mode 100644 tests/serverApi/gateway.test.ts delete mode 100644 tests/serverApi/schemas.test.ts diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index 2b190ee..74bf75b 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -4,7 +4,7 @@ mode: primary model: deepseek/deepseek-v4-flash temperature: 0.2 --- -你是 TJWater 实验性 SCADA 数据 Agent。回复用户时使用简体中文,内容简洁准确。 +你是 TJWater 实验性排水数据 Agent。回复用户时使用简体中文,内容简洁准确。 ## 核心定位 @@ -15,53 +15,48 @@ Agent 负责: 1. 理解用户意图。 2. 调用受控工具获取有限上下文或表达展示意图。 3. 输出文字摘要、结论、追问和建议。 -4. 通过受控地图工具表达定位意图。 +4. 在没有可用前端工具时,仅输出文字摘要、结论和建议。 -服务端负责把受控地图工具转换成 `agent-ui@1` 的 UIEnvelope;前端只执行可信地图动作。 +当前不向 Agent 暴露前端动作工具。 ## 工具选择 | 场景 | 工具 | |------|------| -| 查询实时公开网页信息 | `web_search` | -| 地址/地点转经纬度 | `geocode` | -| 地图定位 | `zoom_to_map` | | 历史会话检索 | `session_search` | | 用户偏好和项目事实 | `memory_manager` | | 可复用流程沉淀 | `skill_manager` | -| 项目背景 | `get_project_context` | -| SCADA 资产 | `list_scada_assets` | -| SCADA 时序读数 | `query_scada_readings`(必须给出 `observed_from`、`observed_to`) | ## UI 约束 -1. 前端地图工具仅表达定位意图,不返回业务数据。 -2. 每次工具调用必须填写具体 `reason`。 +1. 不生成或调用前端动作。 +2. 每次普通工具调用必须填写具体 `reason`。 3. 不生成 JS、JSX、HTML、CSS 或可执行前端代码。 ## 执行约束 1. 无可用数据时不得编造结论。 -2. 不要调用 shell、Python 或 CLI 来拼装业务数据流程。 +2. 不要自由拼接 shell、Python 或 CLI 业务数据流程。仅可对 `source/` 数据执行已验证 Skill 内明确指定的只读分析脚本。 3. 不要读取或写入 token、password、secret、API key、system prompt。 4. 尽量不用子代理,保持过程可观测。 ## 工作流沉淀 -只有在当前对话流程已经验证有效、可被复用且用户明确需要保存时,才使用 `skill_manager` 沉淀 workflow。实验阶段 workflow 不应记录 CLI、shell pipe 或认证上下文。 +只有在当前对话流程已经验证有效、可被复用且用户明确需要保存时,才使用 `skill_manager` 沉淀 workflow。实验阶段 workflow 不应记录自由拼接的 CLI、shell pipe 或认证上下文;可引用 Skill 内已验证的固定只读分析脚本。 ## 记忆持久化 `memory_manager` 只保存长期有效的稳定事实或用户偏好。修改 memory 前先 list 当前 scope,再决定 add、replace 或 remove。 -## TJWater Server 受控领域工具 +## TJWater Server -仅通过领域工具访问 Server;不得构造 URL、HTTP header、SQL、命令或项目 ID。 +当前不提供依赖 TJWater Server 的工具。不得自行构造 URL、HTTP header、SQL、命令或项目 ID 访问 Server。监测数据统一从项目 `source/` 目录读取。 -- 项目背景:`get_project_context` -- SCADA 资产:`list_scada_assets` -- SCADA 时序读数:`query_scada_readings` +## 本地数据分析 -查询历史 SCADA 数据时,必须先明确时间区间;不清楚区间时先追问,不得省略或自行扩大范围。需要数值结论时先调用 `query_scada_readings`,传入 `observed_from`、`observed_to`、`granularity`、`metrics` 和 `asset_ids`,再用文字摘要说明结果。 -先查询数据再作判断。回答必须说明查询时间范围、粒度和指标。 -遇到空数据、拒绝、权限错误、超时或服务不可用时,明确说明事实,不得补全或猜测结果。 -工具返回 `result_ref` 时,说明完整结果已安全存储,并使用 preview 中的样本和统计作答。 +1. 先读取 `source/catalog.json` 选择数据源,再读取对应数据包的 `manifest.json` 和所需 JSONL。 +2. 用户明确指定编号或场景时,只使用对应数据包。例如只分析污染源接入时选择 `scenario_code=07`。 +3. 用户要求雨污混接、雨水混接、地下水入渗或外来水稀释分析,且未指定其他数据源时,默认选择 `scenario_code=01`。 +4. 用户未明确场景时,先用 `scenario_code=00` 总览判断匹配场景;除非用户要求综合对比,不得默认混合多个场景数据。 +5. 使用与任务匹配的 Skill 执行数据准入、计算和结论约束。 +6. 只读取与分析数据;不覆盖、删除或回写 `source/` 原始数据。 +7. 数值结论必须说明数据集、时间范围、粒度、指标和计算口径。 diff --git a/.opencode/plugins/frontend-action-call-id.ts b/.opencode/plugins/frontend-action-call-id.ts deleted file mode 100644 index 46b4048..0000000 --- a/.opencode/plugins/frontend-action-call-id.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Plugin } from "@opencode-ai/plugin"; - -export const FRONTEND_ACTION_TOOLS = new Set(["zoom_to_map"]); - -export const frontendActionCallIdPlugin: Plugin = async () => ({ - "tool.execute.before": async (input, output) => { - if (!FRONTEND_ACTION_TOOLS.has(input.tool)) return; - output.args = { ...(output.args ?? {}), __frontend_action_call_id: input.callID }; - }, -}); - -export const server = frontendActionCallIdPlugin; - -export default frontendActionCallIdPlugin; diff --git a/.opencode/tools/frontend_action.ts b/.opencode/tools/frontend_action.ts deleted file mode 100644 index 604f2cb..0000000 --- a/.opencode/tools/frontend_action.ts +++ /dev/null @@ -1,27 +0,0 @@ -type FrontendActionArgs = Record & { __frontend_action_call_id?: string; reason?: string }; - -const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; -const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; - -export const executeFrontendAction = async ( - name: string, - args: FrontendActionArgs, - context: { sessionID: string }, -) => { - const callId = args.__frontend_action_call_id; - if (!callId) throw new Error("frontend action bridge did not inject the OpenCode call ID"); - const { __frontend_action_call_id: _callId, reason, ...params } = args; - let response: Response; - try { - response = await fetch(`${internalBaseUrl}/internal/frontend-actions/request`, { - method: "POST", - headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken }, - body: JSON.stringify({ session_id: context.sessionID, call_id: callId, name, params, fallback_text: reason }), - }); - } catch (error) { - throw new Error(`frontend action bridge unavailable: ${error instanceof Error ? error.message : String(error)}`); - } - const text = await response.text(); - if (!response.ok) throw new Error(`frontend action bridge rejected request (${response.status}): ${text}`); - return text; -}; diff --git a/.opencode/tools/geocode.ts b/.opencode/tools/geocode.ts deleted file mode 100644 index d113ad5..0000000 --- a/.opencode/tools/geocode.ts +++ /dev/null @@ -1,37 +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: - "调用 TJWater 后端的天地图地理编码服务,将中国境内结构化地址或地点名称转换为经纬度。若需缩放地图,把返回的 location.lon/location.lat 传给 zoom_to_map,并设置 source_crs='EPSG:4326'。", - args: { - reason: tool.schema - .string() - .describe("Why geocoding is required for the current user request."), - keyword: tool.schema - .string() - .describe("Address or place name to geocode, such as 北京市人民政府."), - }, - async execute(args, context) { - const response = await fetch(`${internalBaseUrl}/internal/tools/geocode`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, - }, - body: JSON.stringify({ - session_id: context.sessionID, - keyword: args.keyword, - }), - }); - - const text = await response.text(); - if (!response.ok) { - throw new Error(text); - } - return text; - }, -}); diff --git a/.opencode/tools/get_project_context.ts b/.opencode/tools/get_project_context.ts deleted file mode 100644 index bbe9aa9..0000000 --- a/.opencode/tools/get_project_context.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; -export default tool({ description: "读取当前受控项目的基本信息。", args: {}, execute: (_args, context) => callServerApiTool("get_project_context", {}, context.sessionID) }); diff --git a/.opencode/tools/list_scada_assets.ts b/.opencode/tools/list_scada_assets.ts deleted file mode 100644 index e60a144..0000000 --- a/.opencode/tools/list_scada_assets.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; - -export default tool({ - description: - "列出当前项目可查询的 SCADA 资产。无请求参数;返回 assets 数组,包含 assets[].scada_id、external_id、site_external_id、name、kind、active。后续查询读数时必须使用 assets[].scada_id 作为 query_scada_readings.asset_ids;不要使用 GeoServer feature id/fid。", - args: {}, - execute: (args, context) => - callServerApiTool("list_scada_assets", args, context.sessionID), -}); diff --git a/.opencode/tools/query_scada_readings.ts b/.opencode/tools/query_scada_readings.ts deleted file mode 100644 index 4b2cbf8..0000000 --- a/.opencode/tools/query_scada_readings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { callServerApiTool } from "./server_api_shared.js"; - -export default tool({ - description: - "查询一个或多个 SCADA 资产在指定时间范围内的时序读数。调用前通常先用 list_scada_assets 获取资产 id;必须给出明确的 observed_from 和 observed_to,不要省略或自行扩大时间范围。", - args: { - asset_ids: tool.schema - .array(tool.schema.string().uuid()) - .min(1) - .max(100) - .describe( - "SCADA asset UUID 数组,1 到 100 个。必须使用 list_scada_assets 返回的 assets[].scada_id,不要使用 GeoServer feature id/fid、external_id 或 site_external_id。", - ), - observed_from: tool.schema - .string() - .datetime() - .describe( - "查询开始时间,ISO8601 datetime 字符串,必须包含时区,例如 2026-04-10T07:15:00Z 或 2026-04-10T15:15:00+08:00。包含该时刻。", - ), - observed_to: tool.schema - .string() - .datetime() - .describe( - "查询结束时间,ISO8601 datetime 字符串,必须包含时区,且必须晚于 observed_from。服务端按半开区间处理:observed_at >= observed_from 且 observed_at < observed_to。", - ), - granularity: tool.schema - .enum(["5m", "1h", "1d"]) - .default("5m") - .describe( - "读数粒度:5m 为原始 5 分钟读数,1h 为小时聚合,1d 为日聚合。未指定时默认 5m。", - ), - metrics: tool.schema - .array( - tool.schema.enum([ - "conductivity_mean", - "level_mean", - "flow_mean", - "temperature_mean", - ]), - ) - .min(1) - .max(4) - .describe( - "要返回的指标数组,1 到 4 个。允许值:conductivity_mean(电导率均值)、level_mean(液位均值)、flow_mean(流量均值)、temperature_mean(温度均值)。", - ), - }, - execute: (args, context) => - callServerApiTool("query_scada_readings", args, context.sessionID), -}); diff --git a/.opencode/tools/server_api_shared.ts b/.opencode/tools/server_api_shared.ts deleted file mode 100644 index 977425e..0000000 --- a/.opencode/tools/server_api_shared.ts +++ /dev/null @@ -1,13 +0,0 @@ -const internalBaseUrl = process.env.TJWATER_AGENT_INTERNAL_BASE_URL ?? "http://127.0.0.1:8787"; -const internalToken = process.env.TJWATER_AGENT_INTERNAL_TOKEN ?? ""; - -export const callServerApiTool = async (name: string, args: unknown, sessionID: string) => { - const response = await fetch(`${internalBaseUrl}/internal/tools/server-api/${name}`, { - method: "POST", - headers: { "Content-Type": "application/json", "x-agent-internal-token": internalToken }, - body: JSON.stringify({ session_id: sessionID, args }), - }); - const text = await response.text(); - if (!response.ok) throw new Error("TJWater domain tool bridge rejected the request"); - return text; -}; diff --git a/.opencode/tools/web_search.ts b/.opencode/tools/web_search.ts deleted file mode 100644 index fd64c2a..0000000 --- a/.opencode/tools/web_search.ts +++ /dev/null @@ -1,62 +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: - "调用 TJWater 后端的实时网页搜索服务。适合查询新闻、政策、规范、产品资料、公开网页事实等可能变化的信息。", - args: { - reason: tool.schema - .string() - .describe("Why web search is required for the current user request."), - query: tool.schema.string().describe("Search query text."), - freshness: tool.schema - .enum(["no_limit", "one_day", "one_week", "one_month", "one_year"]) - .optional() - .describe("Optional freshness filter. Defaults to no_limit."), - summary: tool.schema - .boolean() - .optional() - .describe("Whether the backend should include page summaries."), - count: tool.schema - .number() - .int() - .positive() - .optional() - .describe("Optional result count, backend accepts 1 to 50."), - include: tool.schema - .array(tool.schema.string()) - .optional() - .describe("Optional domains to include."), - exclude: tool.schema - .array(tool.schema.string()) - .optional() - .describe("Optional domains to exclude."), - }, - async execute(args, context) { - const response = await fetch(`${internalBaseUrl}/internal/tools/web-search`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-agent-internal-token": internalToken, - }, - body: JSON.stringify({ - session_id: context.sessionID, - query: args.query, - freshness: args.freshness, - summary: args.summary, - count: args.count, - include: args.include, - exclude: args.exclude, - }), - }); - - const text = await response.text(); - if (!response.ok) { - throw new Error(text); - } - return text; - }, -}); diff --git a/.opencode/tools/zoom_to_map.ts b/.opencode/tools/zoom_to_map.ts deleted file mode 100644 index a98efb1..0000000 --- a/.opencode/tools/zoom_to_map.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { tool } from "@opencode-ai/plugin"; -import { executeFrontendAction } from "./frontend_action.js"; - -export default tool({ - description: - "在前端地图上缩放定位到坐标。默认坐标为 EPSG:3857;如果来自天地图 geocode 的 lon/lat,传 source_crs='EPSG:4326',前端会转换为 EPSG:3857 后缩放。", - args: { - reason: tool.schema - .string() - .describe("Why this map zoom action is needed for the current request."), - x: tool.schema - .number() - .describe("X coordinate. For EPSG:4326 this is longitude; for EPSG:3857 this is meters."), - y: tool.schema - .number() - .describe("Y coordinate. For EPSG:4326 this is latitude; for EPSG:3857 this is meters."), - source_crs: tool.schema - .enum(["EPSG:3857", "EPSG:4326"]) - .optional() - .describe("Input coordinate CRS. Defaults to EPSG:3857."), - zoom: tool.schema - .number() - .optional() - .describe("Optional OpenLayers zoom level. Defaults to 18."), - duration_ms: tool.schema - .number() - .optional() - .describe("Optional animation duration in milliseconds. Defaults to 1000."), - }, - async execute(args, context) { - return executeFrontendAction("zoom_to_map", args, context); - }, -}); diff --git a/README.md b/README.md index ca5e302..569934a 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ real auth provider through `src/context/`. - No required `Authorization` or `X-Project-Id` headers. - No `cli/`, no `tjwater_cli` opencode tool, no free-form CLI command bridge. - Agent visual output is expressed through `agent-ui@1` UIEnvelope events. -- Backend business data is available through typed, allowlisted domain tools. - Arbitrary URLs, SQL, shell commands, and filesystem paths remain unavailable. +- Tools that depend on the TJWater Server are disabled. Arbitrary URLs, SQL, + shell commands, and filesystem paths remain unavailable. ## Main API @@ -51,22 +51,14 @@ Defaults: ```text AGENT_LOCAL_USER_ID=local-user -AGENT_LOCAL_PROJECT_ID= +AGENT_LOCAL_PROJECT_ID=local-project AGENT_LOCAL_NETWORK=local-network ``` -Local domain tools are enabled by default. Set `TJWATER_API_BASE_URL` to the -Server address and `AGENT_LOCAL_PROJECT_ID` to an enabled project UUID. Setting -`TJWATER_API_ENABLED=false` disables all Server calls. Production startup rejects -enabled Server tools while `TJWATER_AUTH_MODE=disabled`. - -Available domain tools: - -- `get_project_context` -- `list_scada_assets` -- `query_scada_readings` with required `observed_from` and `observed_to` - ISO8601 timestamps. The server rejects reversed or unbounded historical - queries. +Monitoring time-series data is read from the agent project's `source/` +directory and analyzed through validated local skills. `get_project_context`, +`list_scada_assets`, `query_scada_readings`, `web_search`, and `geocode` are not +exposed because they depend on the TJWater Server. Optional request headers can override the local context during experiments: @@ -85,15 +77,9 @@ The registry is exposed at: GET /api/v1/agent/chat/ui-registry ``` -Visual opencode tools are mapped by the service: - -- `show_chart` -> `chart` -- `zoom_to_map` -> `map_action` -- `view_scada` -> `registered_component` - -Use `query_scada_readings` first when the answer needs actual SCADA values. -Use `view_scada` only to ask the frontend to display the selected SCADA asset -panel. +No visual or frontend-action tools are currently exposed to the Agent. The +registry endpoints remain available for protocol compatibility and return no +registered charts, components, or actions. See [docs/agent-ui-protocol.md](docs/agent-ui-protocol.md). diff --git a/docs/agent-ui-protocol.md b/docs/agent-ui-protocol.md index 4f17e27..a1ec4be 100644 --- a/docs/agent-ui-protocol.md +++ b/docs/agent-ui-protocol.md @@ -77,15 +77,12 @@ is valid only on `map_overlay`; every action and parameter set must be validated by the frontend before it can affect MapLibre state. Invalid map actions should fall back to `fallbackText`. -Initial mappings: +There are currently no Agent tool mappings for charts, components, or map +actions. The protocol types remain documented for compatibility and future +allowlisted actions. -- `show_chart` -> `chart` -- `zoom_to_map` -> `map_action` -- `view_scada` -> `registered_component: ScadaPanel` - -When the Agent needs historical SCADA values for a chart or explanation, it -should call `query_scada_readings` first, then use `view_scada` only to open the -frontend panel. +Historical monitoring values for analysis come from the project `source/` +directory. ## State Persistence diff --git a/opencode.json b/opencode.json index 9e19ffc..09749a0 100644 --- a/opencode.json +++ b/opencode.json @@ -29,8 +29,5 @@ "edit": "ask", "task": "deny" }, - "plugin": [ - "./.opencode/plugins/frontend-action-call-id.ts" - ], "default_agent": "instruction" } diff --git a/src/config.ts b/src/config.ts index 8024e77..047d1a6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -82,13 +82,6 @@ const envSchema = z OPENCODE_CLIENT_BASE_URL: z.string().url().optional(), // 旧版 client 模式环境变量名,保留兼容,解析时会映射到 OPENCODE_CLIENT_BASE_URL。 OPENCODE_BASE_URL: z.string().url().optional(), - // TJWater 后端 API 的基础地址。 - TJWATER_API_BASE_URL: z.string().default("http://127.0.0.1:8000"), - // 本地实验环境默认开放受控领域工具;生产环境仍由下方鉴权校验保护。 - TJWATER_API_ENABLED: optionalBoolean(true), - TJWATER_AUTH_MODE: z.enum(["disabled"]).default("disabled"), - // 代理调用 TJWater 后端 API 的超时时间(毫秒)。 - TJWATER_API_TIMEOUT_MS: z.coerce.number().int().positive().default(30000), // 后端结果在直接内联返回给模型前允许的最大字节数。 MAX_INLINE_RESULT_BYTES: z.coerce.number().int().positive().default(12000), // 生成结果 preview 时最多抽样的条目数。 @@ -141,13 +134,6 @@ const envSchema = z .default(3600000), }) .superRefine((env, ctx) => { - if (env.NODE_ENV === "production" && env.TJWATER_API_ENABLED && env.TJWATER_AUTH_MODE === "disabled") { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - path: ["TJWATER_AUTH_MODE"], - message: "TJWATER API tools cannot run without authentication in production", - }); - } if (env.OPENCODE_MODE === "client" && !env.OPENCODE_CLIENT_BASE_URL) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/src/frontendAction/registry.ts b/src/frontendAction/registry.ts index 3ce401a..92949de 100644 --- a/src/frontendAction/registry.ts +++ b/src/frontendAction/registry.ts @@ -2,10 +2,7 @@ import { z } from "zod"; import type { FrontendActionManifest } from "./types.js"; type Definition = { manifest: FrontendActionManifest; inputSchema: z.ZodTypeAny; outputSchema: z.ZodTypeAny }; -const zoomInput = z.object({ x: z.number().finite(), y: z.number().finite(), source_crs: z.enum(["EPSG:3857", "EPSG:4326"]).optional(), zoom: z.number().finite().optional(), duration_ms: z.number().finite().nonnegative().optional() }).strict(); -const 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() }, -]; +const definitions: Definition[] = []; export const frontendActionRegistry = definitions.map((item) => item.manifest); export const getFrontendActionDefinition = (name: string) => definitions.find((item) => item.manifest.id === name); diff --git a/src/routes/chatStream.ts b/src/routes/chatStream.ts index 55e1a46..71cba06 100644 --- a/src/routes/chatStream.ts +++ b/src/routes/chatStream.ts @@ -830,7 +830,7 @@ export const streamPromptResponse = async ({ params: toolParams, reason, }); - if (envelope && !(suppressLegacyFrontendActions && part.tool === "zoom_to_map")) { + if (envelope) { write("ui_envelope", { session_id: clientSessionId, envelope_id: createEnvelopeId(part.tool), diff --git a/src/routes/chatStreamEvents.ts b/src/routes/chatStreamEvents.ts index 62fe7bf..f470366 100644 --- a/src/routes/chatStreamEvents.ts +++ b/src/routes/chatStreamEvents.ts @@ -61,11 +61,8 @@ const isDevelopmentDebugLoggingEnabled = process.env.NODE_ENV === "development"; const toolLabels: Record = { memory_manager: "记忆写入", - geocode: "地理编码", session_search: "历史会话检索", skill_manager: "流程沉淀", - web_search: "网页搜索", - zoom_to_map: "地图缩放", }; export const logDevelopmentDebug = ( diff --git a/src/routes/chatUiState.ts b/src/routes/chatUiState.ts index ed19847..b1d95cd 100644 --- a/src/routes/chatUiState.ts +++ b/src/routes/chatUiState.ts @@ -207,9 +207,6 @@ export const updateLastAssistantQuestion = ( }); const getToolArtifactKind = (tool: string): ToolArtifactKind => { - if (tool === "zoom_to_map") { - return "map"; - } return "tool"; }; @@ -217,7 +214,6 @@ const getToolArtifactTitle = (tool: string, params: Record) => if (typeof params.title === "string" && params.title.trim()) { return params.title.trim(); } - if (tool === "zoom_to_map") return "缩放到地图坐标"; return tool || "工具调用"; }; diff --git a/src/server.ts b/src/server.ts index 2c3c3da..fb08dfc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,12 +15,7 @@ import { ResultReferenceStore } from "./results/store.js"; import { buildChatRouter } from "./routes/chat.js"; import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js"; import { opencodeRuntime } from "./runtime/opencode.js"; -import { ServerApiGateway } from "./serverApi/gateway.js"; -import { isDomainToolName } from "./serverApi/schemas.js"; -import { - getRuntimeSessionContext, - type RuntimeSessionContext, -} from "./runtime/sessionContext.js"; +import { getRuntimeSessionContext } from "./runtime/sessionContext.js"; const app = express(); @@ -37,7 +32,6 @@ const learningOrchestrator = new LearningOrchestrator( ); const resultReferenceStore = new ResultReferenceStore(); const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore); -const serverApiGateway = new ServerApiGateway(resultReferenceResolver); const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID(); const frontendActionCoordinator = new FrontendActionCoordinator(); @@ -46,25 +40,6 @@ process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken; app.use(cors()); app.use(express.json({ limit: "1mb" })); -app.post("/internal/tools/server-api/:tool", async (req, res) => { - if (req.header("x-agent-internal-token") !== internalToken) { - res.status(403).json({ message: "forbidden" }); - return; - } - const name = req.params.tool; - if (!isDomainToolName(name)) { - res.status(404).json({ message: "unknown domain tool" }); - return; - } - const sessionId = typeof req.body?.session_id === "string" ? req.body.session_id.trim() : ""; - const context = sessionId ? getRuntimeSessionContext(sessionId) : null; - if (!context) { - res.status(404).json({ message: "session context not found" }); - return; - } - res.json(await serverApiGateway.execute(name, req.body?.args ?? {}, context)); -}); - app.post("/internal/frontend-actions/request", async (req, res) => { if (req.header("x-agent-internal-token") !== internalToken) { res.status(403).json({ message: "forbidden" }); @@ -155,154 +130,6 @@ app.post("/internal/tools/session-search", async (req, res) => { }); }); -const callBackendJson = async ( - path: string, - _context: RuntimeSessionContext, - payload: unknown, -) => { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS); - try { - const headers: Record = { - Accept: "application/json", - "Content-Type": "application/json", - }; - const response = await fetch(new URL(path, config.TJWATER_API_BASE_URL), { - method: "POST", - headers, - body: JSON.stringify(payload), - signal: controller.signal, - }); - const text = await response.text(); - return { - ok: response.ok, - status: response.status, - text, - }; - } finally { - clearTimeout(timer); - } -}; - -const parseStringArray = (value: unknown) => - Array.isArray(value) - ? value.filter((item): item is string => typeof item === "string") - : undefined; - -const webSearchFreshnessMap: Record = { - no_limit: "noLimit", - one_day: "oneDay", - one_week: "oneWeek", - one_month: "oneMonth", - one_year: "oneYear", -}; - -const normalizeWebSearchFreshness = (value: unknown) => { - if (typeof value !== "string") { - return undefined; - } - return webSearchFreshnessMap[value] ?? value; -}; - -app.post("/internal/tools/web-search", 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 context = sessionId ? getRuntimeSessionContext(sessionId) : null; - if (!context) { - res.status(404).json({ - message: "session context not found", - detail: sessionId, - }); - return; - } - - const query = typeof req.body?.query === "string" ? req.body.query.trim() : ""; - if (!query) { - res.status(400).json({ message: "query is required" }); - return; - } - - const count = - typeof req.body?.count === "number" && Number.isFinite(req.body.count) - ? Math.trunc(req.body.count) - : undefined; - const payload = { - query, - freshness: normalizeWebSearchFreshness(req.body?.freshness), - summary: - typeof req.body?.summary === "boolean" ? req.body.summary : undefined, - count, - include: parseStringArray(req.body?.include), - exclude: parseStringArray(req.body?.exclude), - }; - - try { - const response = await callBackendJson( - "/api/v1/web-search", - context, - payload, - ); - res - .status(response.ok ? 200 : response.status) - .type("application/json") - .send(response.text); - } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - res.status(503).json({ - message: "web search service unavailable", - detail, - }); - } -}); - -app.post("/internal/tools/geocode", 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 context = sessionId ? getRuntimeSessionContext(sessionId) : null; - if (!context) { - res.status(404).json({ - message: "session context not found", - detail: sessionId, - }); - return; - } - - const keyword = - typeof req.body?.keyword === "string" ? req.body.keyword.trim() : ""; - if (!keyword) { - res.status(400).json({ message: "keyword is required" }); - return; - } - - try { - const response = await callBackendJson( - "/api/v1/geocode", - context, - { keyword }, - ); - res - .status(response.ok ? 200 : response.status) - .type("application/json") - .send(response.text); - } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - res.status(503).json({ - message: "geocoding service unavailable", - detail, - }); - } -}); - app.use( "/api/v1/agent/chat", buildChatRouter( diff --git a/src/serverApi/gateway.ts b/src/serverApi/gateway.ts deleted file mode 100644 index b4b76cd..0000000 --- a/src/serverApi/gateway.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { z } from "zod"; - -import { config } from "../config.js"; -import { ResultReferenceResolver } from "../results/resolver.js"; -import { RESULT_REFERENCE_KIND, RESULT_REFERENCE_SOURCE } from "../results/store.js"; -import type { RuntimeSessionContext } from "../runtime/sessionContext.js"; -import { domainToolSchemas, type DomainToolName } from "./schemas.js"; - -export type GatewayErrorCode = "VALIDATION_FAILED" | "NOT_FOUND" | "UPSTREAM_UNAVAILABLE" | "TIMEOUT" | "PROJECT_CONTEXT_INVALID"; -type Fetch = typeof fetch; - -const responseSchemas: Record = { - get_project_context: z.object({ - project_id: z.string().uuid(), - code: z.string(), - name: z.string(), - description: z.string().nullable(), - status: z.enum(["active", "inactive"]), - project_role: z.string(), - gs_workspace: z.string(), - map_extent: z.object({ xmin: z.number(), ymin: z.number(), xmax: z.number(), ymax: z.number() }).nullable(), - }), - list_scada_assets: z.object({ assets: z.array(z.unknown()) }), - query_scada_readings: z.object({ data: z.array(z.unknown()), meta: z.record(z.unknown()) }), -}; - -export class ServerApiGateway { - constructor(private readonly resolver: ResultReferenceResolver, private readonly fetchImpl: Fetch = fetch) {} - - async execute(name: DomainToolName, rawArgs: unknown, context: RuntimeSessionContext) { - if (!config.TJWATER_API_ENABLED) return failure("UPSTREAM_UNAVAILABLE", "TJWater API tools are disabled"); - if (!context.projectId || !z.string().uuid().safeParse(context.projectId).success) { - return failure("PROJECT_CONTEXT_INVALID", "project context is unavailable or invalid"); - } - const parsed = domainToolSchemas[name].safeParse(rawArgs); - if (!parsed.success) return failure("VALIDATION_FAILED", "tool parameters are invalid", parsed.error.flatten()); - try { - const data = await this.dispatch(name, parsed.data, context); - const validated = responseSchemas[name].safeParse(data); - if (!validated.success) return failure("UPSTREAM_UNAVAILABLE", "upstream returned an invalid response"); - const bytes = Buffer.byteLength(JSON.stringify(validated.data)); - if (bytes <= config.MAX_INLINE_RESULT_BYTES) return { ok: true, data: validated.data, truncated: false }; - const record = await this.resolver.register({ - actorKey: context.actorKey, clientSessionId: context.clientSessionId, data: validated.data, - kind: RESULT_REFERENCE_KIND.serverApiPayload, projectId: context.projectId, - projectKey: context.projectKey, schemaVersion: 1, sessionId: context.clientSessionId, - source: RESULT_REFERENCE_SOURCE.serverApi, traceId: context.traceId, - }); - return { ok: true, result_ref: record.resultRef, preview: record.preview, result_size_bytes: bytes, truncated: true }; - } catch (error) { - if (error instanceof GatewayHttpError) { - if (error.status === 404) return failure("NOT_FOUND", "requested resource was not found"); - return failure("UPSTREAM_UNAVAILABLE", `upstream request failed (${error.status})`); - } - if (error instanceof DOMException && error.name === "AbortError") return failure("TIMEOUT", "upstream request timed out"); - return failure("UPSTREAM_UNAVAILABLE", "upstream service is unavailable"); - } - } - - private async dispatch(name: DomainToolName, args: any, context: RuntimeSessionContext): Promise { - if (name === "get_project_context") { - const projects = await this.request("GET", "/api/v1/projects", undefined, context); - if (!Array.isArray(projects)) throw new GatewayHttpError(502); - const project = projects.find((item) => ( - typeof item === "object" - && item !== null - && "project_id" in item - && item.project_id === context.projectId - )); - if (!project) throw new GatewayHttpError(404); - return project; - } - if (name === "list_scada_assets") { - return this.request("GET", "/api/v1/scada/assets", undefined, context); - } - return this.request("POST", "/api/v1/scada/readings/query", args, context); - } - - private async request(method: string, path: string, body: unknown, context: RuntimeSessionContext) { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), config.TJWATER_API_TIMEOUT_MS); - try { - const response = await this.fetchImpl(new URL(path, config.TJWATER_API_BASE_URL), { - method, signal: controller.signal, headers: { - Accept: "application/json", "Content-Type": "application/json", "X-Project-Id": context.projectId!, - "X-Request-Id": context.traceId, - }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), - }); - if (!response.ok) throw new GatewayHttpError(response.status); - return await response.json(); - } finally { clearTimeout(timer); } - } -} - -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 } : {}) } }); diff --git a/src/serverApi/schemas.ts b/src/serverApi/schemas.ts deleted file mode 100644 index 94973d2..0000000 --- a/src/serverApi/schemas.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z } from "zod"; - -const uuid = z.string().uuid(); -const dateTime = z.string().datetime({ offset: true }); -const metrics = z.enum([ - "conductivity_mean", "level_mean", "flow_mean", "temperature_mean", -]); - -export const domainToolSchemas = { - get_project_context: z.object({}), - list_scada_assets: z.object({}), - query_scada_readings: z.object({ - asset_ids: z.array(uuid).min(1).max(100), - observed_from: dateTime, - observed_to: dateTime, - granularity: z.enum(["5m", "1h", "1d"]).default("5m"), - metrics: z.array(metrics).min(1).max(4), - }).refine((value) => Date.parse(value.observed_from) < Date.parse(value.observed_to), { - message: "observed_from must precede observed_to", - }), -} as const; - -export type DomainToolName = keyof typeof domainToolSchemas; -export const isDomainToolName = (value: string): value is DomainToolName => value in domainToolSchemas; diff --git a/src/uiEnvelope/fromToolCall.ts b/src/uiEnvelope/fromToolCall.ts index 35eef1c..941911c 100644 --- a/src/uiEnvelope/fromToolCall.ts +++ b/src/uiEnvelope/fromToolCall.ts @@ -7,20 +7,4 @@ type ToolCallInput = { }; export const toUiEnvelopeFromToolCall = ({ - tool, - params, - reason, -}: ToolCallInput): UIEnvelope | null => { - if (tool === "zoom_to_map") { - return { - kind: "map_action", - schemaVersion: "agent-ui@1", - action: tool, - surface: "map_overlay", - params, - fallbackText: reason || "已生成地图操作。", - }; - } - - return null; -}; +}: ToolCallInput): UIEnvelope | null => null; diff --git a/src/uiEnvelope/registry.ts b/src/uiEnvelope/registry.ts index 29b825d..943e630 100644 --- a/src/uiEnvelope/registry.ts +++ b/src/uiEnvelope/registry.ts @@ -4,14 +4,7 @@ export const chartGrammar = "echarts-safe-subset" as const; export const componentRegistry: ComponentManifest[] = []; -export const actionRegistry: ActionManifest[] = [ - { - id: "zoom_to_map", - version: "1.0.0", - description: "Zoom the map to a coordinate.", - supportedSurfaces: ["map_overlay"], - }, -]; +export const actionRegistry: ActionManifest[] = []; export const uiRegistryResponse = { schema_version: "agent-ui-registry@1", diff --git a/tests/frontendAction/callIdPlugin.test.ts b/tests/frontendAction/callIdPlugin.test.ts deleted file mode 100644 index f20439a..0000000 --- a/tests/frontendAction/callIdPlugin.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { readdirSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -import { FRONTEND_ACTION_TOOLS, frontendActionCallIdPlugin, server } from "../../.opencode/plugins/frontend-action-call-id.js"; -import { frontendActionRegistry } from "../../src/frontendAction/registry.js"; - -const frontendActionToolNames = () => readdirSync(".opencode/tools") - .filter((file) => file.endsWith(".ts")) - .filter((file) => readFileSync(join(".opencode/tools", file), "utf8").includes("executeFrontendAction(")) - .map((file) => file.replace(/\.ts$/, "")) - .sort(); - -describe("frontend action call ID plugin", () => { - it("is registered in the OpenCode project config", () => { - const config = JSON.parse(readFileSync("opencode.json", "utf8")) as { plugin?: unknown[] }; - expect(config.plugin).toContain("./.opencode/plugins/frontend-action-call-id.ts"); - }); - - it("exposes the server export required by OpenCode", () => { - expect(server).toBe(frontendActionCallIdPlugin); - }); - - it("overwrites forged IDs only for allowlisted tools", async () => { - const hooks = await frontendActionCallIdPlugin({} as never); - const before = hooks["tool.execute.before"]; - if (!before) throw new Error("hook missing"); - const actionOutput = { args: { x: 1, __frontend_action_call_id: "forged" } }; - await before({ tool: "zoom_to_map", sessionID: "session", callID: "real" }, actionOutput); - expect(actionOutput.args.__frontend_action_call_id).toBe("real"); - - const ordinaryOutput = { args: { query: "water" } }; - await before({ tool: "web_search", sessionID: "session", callID: "ignored" }, ordinaryOutput); - expect(ordinaryOutput.args).toEqual({ query: "water" }); - }); - - it("covers every tool that uses the frontend action bridge", () => { - const bridgeTools = frontendActionToolNames(); - expect([...FRONTEND_ACTION_TOOLS].sort()).toEqual(bridgeTools); - expect(frontendActionRegistry.map((action) => action.id).sort()).toEqual(bridgeTools); - }); -}); diff --git a/tests/frontendAction/coordinator.test.ts b/tests/frontendAction/coordinator.test.ts index c67494f..7642f36 100644 --- a/tests/frontendAction/coordinator.test.ts +++ b/tests/frontendAction/coordinator.test.ts @@ -1,47 +1,12 @@ import { describe, expect, it } from "bun:test"; import { FrontendActionCoordinator, FrontendActionError } from "../../src/frontendAction/coordinator.js"; -import type { FrontendActionRequest } from "../../src/frontendAction/types.js"; - -const start = (coordinator: FrontendActionCoordinator, sessionId = "session-1") => { - let emitted: FrontendActionRequest | undefined; - coordinator.registerSink(sessionId, (request) => { emitted = request; }); - const pending = coordinator.request({ sessionId, toolCallId: "call-1", name: "zoom_to_map", params: { x: 117.2, y: 39.1 } }); - if (!emitted) throw new Error("request was not emitted"); - return { emitted, pending }; -}; describe("FrontendActionCoordinator", () => { - it("emits once and resolves with a validated browser result", async () => { + it("rejects frontend actions when the registry is empty", () => { const coordinator = new FrontendActionCoordinator(); - const { emitted, pending } = start(coordinator); - const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: { zoom: 14 }, completedAt: Date.now() }; - expect(coordinator.submitResult("session-1", result).duplicate).toBe(false); - expect((await pending).output).toEqual({ zoom: 14 }); - expect(coordinator.submitResult("session-1", result).duplicate).toBe(true); - }); - - it("rejects conflicting terminal results and cross-session submissions", async () => { - const coordinator = new FrontendActionCoordinator(); - const { emitted, pending } = start(coordinator); - const result = { version: "frontend-action-result@1" as const, actionId: emitted.actionId, status: "succeeded" as const, output: {}, completedAt: Date.now() }; - expect(() => coordinator.submitResult("other", result)).toThrow("session mismatch"); - coordinator.submitResult("session-1", result); - await pending; - expect(() => coordinator.submitResult("session-1", { ...result, completedAt: result.completedAt + 1 })).toThrow("conflicts"); - }); - - it("cancels all pending actions for a session", async () => { - const coordinator = new FrontendActionCoordinator(); - const { pending } = start(coordinator); - coordinator.cancelSession("session-1"); - expect((await pending).status).toBe("cancelled"); - }); - - it("validates params and requires an active browser sink", () => { - const coordinator = new FrontendActionCoordinator(); - expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "zoom_to_map", params: { x: "bad", y: 1 } })).toThrow(FrontendActionError); 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: "zoom_to_map", params: {} })).toThrow(FrontendActionError); + expect(() => coordinator.request({ sessionId: "s", toolCallId: "c", name: "zoom_to_map", params: {} })).toThrow("unknown frontend action"); }); }); diff --git a/tests/opencode/disabledTools.test.ts b/tests/opencode/disabledTools.test.ts new file mode 100644 index 0000000..27f3897 --- /dev/null +++ b/tests/opencode/disabledTools.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "bun:test"; +import { existsSync } from "node:fs"; + +const disabledServerTools = [ + "geocode", + "get_project_context", + "list_scada_assets", + "query_scada_readings", + "web_search", +]; + +describe("disabled opencode tools", () => { + it("does not expose Server-dependent or frontend-action tools", () => { + for (const toolName of disabledServerTools) { + expect(existsSync(`.opencode/tools/${toolName}.ts`)).toBeFalse(); + } + expect(existsSync(".opencode/tools/server_api_shared.ts")).toBeFalse(); + expect(existsSync(".opencode/tools/zoom_to_map.ts")).toBeFalse(); + expect(existsSync(".opencode/tools/frontend_action.ts")).toBeFalse(); + expect(existsSync(".opencode/plugins/frontend-action-call-id.ts")).toBeFalse(); + }); +}); diff --git a/tests/opencode/scadaToolsDescription.test.ts b/tests/opencode/scadaToolsDescription.test.ts deleted file mode 100644 index b8eb912..0000000 --- a/tests/opencode/scadaToolsDescription.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { describe, expect, it } from "bun:test"; -import { readFileSync } from "node:fs"; - -describe("SCADA opencode tools", () => { - it("documents asset listing and reading query parameters", () => { - const listTool = readFileSync(".opencode/tools/list_scada_assets.ts", "utf8"); - const queryTool = readFileSync(".opencode/tools/query_scada_readings.ts", "utf8"); - - expect(listTool).toContain("无请求参数"); - expect(listTool).toContain("assets[].scada_id"); - expect(queryTool).toContain("asset_ids"); - expect(queryTool).toContain("observed_from"); - expect(queryTool).toContain("observed_to"); - expect(queryTool).toContain("granularity"); - expect(queryTool).toContain("metrics"); - expect(queryTool).toContain("半开区间"); - expect(queryTool).toContain("不要使用 GeoServer feature id/fid、external_id 或 site_external_id"); - }); -}); diff --git a/tests/routes/chatStream.test.ts b/tests/routes/chatStream.test.ts index de6d78c..63780e9 100644 --- a/tests/routes/chatStream.test.ts +++ b/tests/routes/chatStream.test.ts @@ -535,67 +535,4 @@ describe("streamPromptResponse", () => { }); }); - it("maps zoom_to_map tool calls to UIEnvelope SSE payloads", async () => { - const runtime = { - subscribeEvents: async () => - createEventStream([ - { - type: "message.part.updated", - properties: { - sessionID: "runtime-session-1", - part: { - id: "tool-part-map", - sessionID: "runtime-session-1", - messageID: "message-1", - type: "tool", - callID: "call-map", - tool: "zoom_to_map", - state: { - status: "running", - input: { - reason: "定位 SCADA 资产附近位置", - x: 121.47, - y: 31.23, - source_crs: "EPSG:4326", - }, - time: { start: Date.now() }, - }, - }, - time: Date.now(), - }, - }, - { - type: "session.idle", - properties: { - sessionID: "runtime-session-1", - }, - }, - ]), - prompt: async () => undefined, - messages: async () => [], - } as unknown as OpencodeRuntimeAdapter; - const events: Array<{ event: string; data: Record }> = []; - - await streamPromptResponse({ - runtime, - sessionId: "runtime-session-1", - clientSessionId: "client-session-1", - message: "chart", - write: (event, data) => events.push({ event, data }), - }); - - expect(events.find((item) => item.event === "tool_call")?.data).toMatchObject({ - session_id: "client-session-1", - tool: "zoom_to_map", - }); - expect(events.find((item) => item.event === "ui_envelope")?.data).toMatchObject({ - session_id: "client-session-1", - envelope: { - kind: "map_action", - schemaVersion: "agent-ui@1", - action: "zoom_to_map", - surface: "map_overlay", - }, - }); - }); }); diff --git a/tests/serverApi/gateway.test.ts b/tests/serverApi/gateway.test.ts deleted file mode 100644 index 0525990..0000000 --- a/tests/serverApi/gateway.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { ServerApiGateway } from "../../src/serverApi/gateway.js"; - -const context = { - actorKey: "user", - clientSessionId: "session", - projectId: "1c60fd8c-89fb-47ca-84f1-66ec10f5bf73", - projectKey: "project", - sessionId: "runtime", - traceId: "trace", -}; - -describe("ServerApiGateway project context", () => { - test("uses the metadata path and accepts project_id responses", async () => { - let requestedPath = ""; - const fetchImpl = (async (input: URL | RequestInfo) => { - requestedPath = new URL(input.toString()).pathname; - return Response.json([{ - project_id: context.projectId, - code: "lingang", - name: "SCADA 物联网监测", - description: null, - status: "active", - project_role: "owner", - gs_workspace: "lingang", - map_extent: { xmin: 120.0, ymin: 30.0, xmax: 122.0, ymax: 32.0 }, - }]); - }) as typeof fetch; - const gateway = new ServerApiGateway({} as never, fetchImpl); - - const result = await gateway.execute("get_project_context", {}, context); - - expect(requestedPath).toBe("/api/v1/projects"); - expect(result).toMatchObject({ - ok: true, - data: { project_id: context.projectId, status: "active", project_role: "owner" }, - }); - }); - - test("does not select the removed id response shape", async () => { - const fetchImpl = (async () => - Response.json([{ id: context.projectId, code: "lingang", name: "SCADA 物联网监测" }])) as unknown as typeof fetch; - const gateway = new ServerApiGateway({} as never, fetchImpl); - - const result = await gateway.execute("get_project_context", {}, context); - - expect(result).toMatchObject({ - ok: false, - error: { code: "NOT_FOUND" }, - }); - }); -}); diff --git a/tests/serverApi/schemas.test.ts b/tests/serverApi/schemas.test.ts deleted file mode 100644 index 63de63c..0000000 --- a/tests/serverApi/schemas.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, test } from "bun:test"; -import { domainToolSchemas } from "../../src/serverApi/schemas.js"; - -describe("domain tool validation", () => { - test("rejects unknown metrics and reversed time", () => { - const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T02:00:00Z", observed_to: "2026-07-14T01:00:00Z", granularity: "5m" }; - expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["ph_mean"] }).success).toBeFalse(); - expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_mean"] }).success).toBeFalse(); - }); - - test("accepts four current metrics and rejects removed metrics", () => { - const base = { asset_ids: ["940468c1-87cc-478f-b9f5-537ffbdb9025"], observed_from: "2026-07-14T01:00:00Z", observed_to: "2026-07-14T02:00:00Z", granularity: "5m" }; - expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["conductivity_mean", "level_mean", "flow_mean", "temperature_mean"] }).success).toBeTrue(); - expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["flow_total"] }).success).toBeFalse(); - expect(domainToolSchemas.query_scada_readings.safeParse({ ...base, metrics: ["velocity_mean"] }).success).toBeFalse(); - }); -}); diff --git a/tests/uiEnvelope/fromToolCall.test.ts b/tests/uiEnvelope/fromToolCall.test.ts index dd935bf..f0aff4c 100644 --- a/tests/uiEnvelope/fromToolCall.test.ts +++ b/tests/uiEnvelope/fromToolCall.test.ts @@ -3,23 +3,8 @@ import { describe, expect, it } from "bun:test"; import { toUiEnvelopeFromToolCall } from "../../src/uiEnvelope/fromToolCall.js"; describe("toUiEnvelopeFromToolCall", () => { - it("maps zoom_to_map calls to map action envelopes", () => { - const envelope = toUiEnvelopeFromToolCall({ - tool: "zoom_to_map", - reason: "定位 SCADA 资产附近位置", - params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, - }); - - expect(envelope).toMatchObject({ - kind: "map_action", - action: "zoom_to_map", - surface: "map_overlay", - params: { x: 121.47, y: 31.23, source_crs: "EPSG:4326" }, - fallbackText: "定位 SCADA 资产附近位置", - }); - }); - it("does not map removed visual tools", () => { + expect(toUiEnvelopeFromToolCall({ tool: "zoom_to_map", params: {} })).toBeNull(); expect(toUiEnvelopeFromToolCall({ tool: "show_chart", params: {} })).toBeNull(); expect(toUiEnvelopeFromToolCall({ tool: "view_scada", params: { asset_ids: ["A-1"] } })).toBeNull(); });