# TJWater 供水 Agent 实验服务 基于 OpenCode 的实验性供水 Agent 服务,用于编排 TJWater 供水管网前端交互。排水数据、排水分析流程和排水前端交互不在本仓库范围内。 本仓库与生产环境中的 `TJWaterAgent` 历史代码相互独立。浏览器身份和项目范围由 TJWater Server 验证,业务访问使用类型化工具。 ## 当前边界 - 生产请求必须携带 `Authorization: Bearer ...` 和 `X-Project-Id`,Server 验证身份及项目权限。开发环境允许无凭据的本地对话,但不能访问 Server 业务工具。 - 不包含 `cli/`、`tjwater_cli` OpenCode 工具或自由形式的 CLI 命令桥接。 - Agent 的结构化界面输出使用 `agent-ui@1` UIEnvelope 事件;受控浏览器操作使用 `frontend-action@1` 前端动作。 - Server 访问通过有限类型化工具;`web_search` 和 `geocode` 使用现有 Server 搜索、地理编码接口。Shell 仅在经过审批的会话 OS 沙箱中执行本地文件计算。 ## 主要 API ```text GET /health GET /api/v1/agent/chat/models GET /api/v1/agent/chat/ui-registry GET /api/v1/agent/chat/frontend-action-registry POST /api/v1/agent/chat/frontend-actions/:action_id/result POST /api/v1/agent/chat/session POST /api/v1/agent/chat/task-executions GET /api/v1/agent/chat/sessions GET /api/v1/agent/chat/session/:session_id GET /api/v1/agent/chat/session/:session_id/stream POST /api/v1/agent/chat/stream POST /api/v1/agent/chat/abort ``` 工况 worker 通过 `POST /api/v1/agent/chat/task-executions` 提交 Server 已持久化的 `task_id`、`run_id` 和 `execution_request_id`。三者缺一即拒绝;Agent 不从进程内 Map 推断当前执行代次。Agent claim 成功后每 20 秒对该 run 心跳,正常流结束显式调用 `complete`,失败或中止显式调用 `fail`。计划、报告和完成命令均携带同一 run 身份,Server 是活动 run 与租约状态的唯一权威。 聊天 UIMessage 流与工作台业务事件流相互独立;业务状态由 Server 的 `/api/v1/workbench/events` 推送,不能混入 token SSE。 `POST /api/v1/agent/chat/stream` 通过 SSE 返回事件流。主要事件如下: | 事件 | 用途 | | --- | --- | | `progress` | Agent 规划和工具执行进度 | | `token` | 助手回复的增量文本 | | `ui_envelope` | 供可信前端渲染的结构化界面描述 | | `frontend_action` | 请求可信前端执行受控动作 | | `tool_call` | 用于兼容和调试的旧版工具调用事件 | | `permission_request` | 运行时权限请求 | | `question_request` | 运行时向用户发起的澄清请求 | | `todo_update` | Agent 待办事项更新 | | `session_title` | 会话标题更新 | | `done` | 本次运行完成 | | `error` | 本次运行失败或被中止 | ## 本地上下文 服务通过 `src/context/localContext.ts` 提供本地上下文,默认值如下: ```text AGENT_LOCAL_USER_ID=local-user AGENT_LOCAL_PROJECT_ID=local-project AGENT_LOCAL_NETWORK=local-network ``` 供水业务数据只来自用户输入、经过校验的事件上下文或已启用的受控工具。`web_search`、`geocode` 和 `query_project_data` 已开放,通过当前会话的可信凭据访问 Server;不接受模型指定的 URL、鉴权头或项目范围。`get_project_context`、`list_scada_assets` 和 `query_scada_readings` 仍未开放。 实验期间可以通过以下请求头覆盖本地上下文: ```text X-Agent-Local-User X-Agent-Local-Project X-Agent-Local-Network X-Trace-Id ``` ## UIEnvelope 与前端动作 UIEnvelope 注册表通过以下接口提供: ```text GET /api/v1/agent/chat/ui-registry ``` 该接口用于兼容 `agent-ui@1` 协议。目前没有注册图表语法、界面组件或 UIEnvelope 动作。 前端动作使用独立的注册表: ```text GET /api/v1/agent/chat/frontend-action-registry ``` 当前提供 `render_scada_analysis` 和 `clear_scada_analysis` 两个地图叠加层动作。浏览器需要在流式请求中声明 `frontend-action@1` 能力,执行动作后再通过以下接口回传结果: ```text POST /api/v1/agent/chat/frontend-actions/:action_id/result ``` 协议详情参见 [Agent UI 协议](docs/agent-ui-protocol.md)。 ## 本地开发 ```bash bun install bun run dev bun run check bun test tests/**/*.test.ts ``` `bun run check` 会检查服务代码和 `.opencode` 工具的类型。 工况任务工具通过本机内部桥访问 Agent,再由 Agent 使用当前请求的短期产品凭据调用 TJWater Server。`embedded` 模式会把启动时生成的内部密钥传给子进程;`client` 模式必须在 Agent 与外部 OpenCode 服务中配置相同的 `AGENT_INTERNAL_TOKEN` 后分别重启。缺少共享密钥时 Agent 会拒绝启动,工具桥也不会退化为仅凭 session ID 访问。 独立管理 Agent 与 OpenCode 时必须显式配置稳定的 `AGENT_INTERNAL_TOKEN`;如果 `bun --watch` 使用自动生成的密钥重启 Agent,仍在运行的 OpenCode 子进程会保留旧密钥,内部工具桥将拒绝请求。需要保留现有开发服务做隔离联调时,可运行 `bun run start:integration:isolated`。它默认使用 Agent `18787`、OpenCode `14096`,从当前仓库目录加载 `opencode.json`、`.opencode/` 与现有 provider 环境,并把 Agent Session、memory、result ref、Skill 及 OpenCode XDG 运行数据写入 `data/integration-18787/`,不会复用默认运行实例的数据目录。可用 `INTEGRATION_AGENT_PORT`、`INTEGRATION_OPENCODE_PORT`、`INTEGRATION_DATA_DIR` 覆盖这些隔离值;启动器不会输出内部密钥。 ## CI/CD Gitea Actions 通过 `.gitea/workflows/package.yml` 复用 `OrgTJWater/ci-templates` 的容器发布流程。手动触发只构建镜像并在 Dev 服务器运行候选验证;从 `main` 创建并推送 `v*` tag 后,候选验证通过才会 提升为正式 `agent` 服务。 仓库需要配置 `REGISTRY_USERNAME`、`REGISTRY_PASSWORD` 和 `DEV_DEPLOY_SSH_KEY` 三个 Gitea Secrets。`.env`、持久化数据和日志不会写入镜像;它们由 Dev 服务器通过环境文件和卷挂载。 ## OpenCode 2 接入 运行时使用 `@opencode/client@2.0.24`,私有进程默认启动 `opencode2 serve`。 Ubuntu 安装:`bun add -g --trust @opencode/cli@2.0.24`,然后检查 `opencode2 --version`。 `OPENCODE_COMMAND` 可指定绝对路径。Client 模式除共享 `AGENT_INTERNAL_TOKEN` 外,还需 用 `OPENCODE_CLIENT_PASSWORD` 提供外部 V2 Server 的密码;嵌入模式自动捕获密码,不写日志。 业务工具由 `.opencode/plugins/tjwater-tools.ts` 的 V2 插件显式注册,直接提供给模型, 保留业务输入验证及前端动作的原生 call ID。启动前检查版本、模型、工具完整性;缺失即失败。 `@opencode-ai/sdk` 仅为现有产品 SSE 契约提供开发期类型,不参与 V2 请求或进程启动。 工具变化:`bash` → `shell`,`task` → `subagent`,`apply_patch` → `patch`。 V2 内置目录不含 `todo`、`todoread`、`todowrite`;本项目插件恢复会话内持久化的 `todoread`、`todowrite`,并将更新继续转换为 `todo_update`。问答改用 Session Form API, 权限回复使用 session ID;文本、推理、工具与错误事件在运行时转换为现有前端 SSE。 已移除 V2 忽略的 `server` 和 `experimental.continue_loop_on_deny` 配置,权限转换为 有序 `permissions` 规则,保留敏感路径静态拒绝、shell/edit 审批和禁用子代理。 官方依据:[迁移指南](https://opencode.ai/v2/docs/migrate-v1/)、 [插件](https://opencode.ai/v2/docs/build/plugins/)、[工具](https://opencode.ai/v2/docs/tools/)、 [客户端](https://opencode.ai/v2/docs/build/client/)。旧 `dev.opencode.ai` beta 文档及 `@opencode-ai/*@next` 标签不可作为当前稳定版安装依据。 ## 旧 Agent 的 P0/P1 能力迁移 - `memory_manager` 和 `skill_manager` 经内部令牌保护的 HTTP 桥接调用主服务,共用主服务的会话上下文和存储。身份与项目范围不由模型传入;新增记忆仍须先读取同 scope。 - 交互会话收到 `credential_refresh_required` 后,前端续期并向 `POST /api/v1/agent/chat/session/:session_id/credentials` 提交 `request_id`;令牌只在认证 header 中传递,不写入会话文件。工具请求最多重试一次,403 不触发续期。续期超时或新凭据再次被拒绝时停止本次执行。 - 后台 worker 使用已有 OAuth provider,依据 Server 的 claimed/running 运行快照向上述接口更新凭据,body 为 `{}`。后台运行不等待浏览器。若凭据已失效而无法写回失败,Server 的租约恢复仍负责终结失效运行。 - 普通会话通过 `query_project_data` 读取有限数据源:供水元素属性、SCADA 设备与历史、元素历史、分析运行及分页结果。历史窗口最长24小时;历史查询接口按 scada.view 只读权限校验,数据清洗仍需 scada.clean;SCADA 字段只接受 `monitored_value/cleaned_value`,物理指标和单位以设备元数据为准。任务会话仍使用任务专属查询工具。 - `locate_features`、`zoom_to_map`、`show_chart`、`view_history` 使用 `frontend-action@1`,等待实际浏览器回执。地图缺失资产返回 missing_ids;历史缺数不生成假曲线。图表仅接收有限、对齐的数值序列。历史展示当前最多100个非空样本,不代表完整高频历史。 - 业务阶段以 progress 快照保存,保留并行操作和错误;任务中的分支、等待和证据仍由 execution-plan@2 表达。`final_answer` 用于最终答复呈现,在运行成功结束后发布;不强制结束模型循环,也不代表工单或业务任务完成。任务运行仍由 Server 的 complete/fail 接口确认。 Shell 已迁移为 Landlock + seccomp 内核沙箱,替换 OpenCode 2 自带的 `shell` 工具;其他宿主 Shell 创建入口关闭。Code Mode 嵌套调用也不能恢复原执行器。此边界覆盖 Shell 与子进程,OpenCode 服务和类型化工具仍在可信服务进程运行。参考 [OpenCode V2 插件接口](https://opencode.ai/v2/docs/build/plugins/) 和 [权限文档](https://opencode.ai/v2/docs/permissions/)。 - Ubuntu/Linux 必须支持 Landlock ABI >= 4,并安装 Python 3、`libseccomp2`。当前 WSL Ubuntu 实测 ABI 7。启动时实际执行一次限制网络的探测;失败则不监听 HTTP,不回退到宿主 Shell。`/health` 的 `sandbox` 显示探测结果。Docker 已加入所需库和执行脚本,宿主内核也必须支持 Landlock。 - `CONVERSATION_WORKSPACE_ROOT` 默认 `./.agent-workspaces`,与受保护的 `data/`、`logs/` 分开。每个会话使用唯一 `conversation-*` 目录,Native Session Location 保存绑定;恢复后继续使用原目录,旧的 checkout 会话在继续执行前迁入新目录。新分支使用独立空目录,不复制源码、凭据或其他会话文件。 - 容器部署时须将 `/app/.agent-workspaces` 挂载到持久卷,并保留 OpenCode 的会话数据库卷。仅挂载 `/app/data` 不能保留新会话文件,例如 Compose 增加 `./agent-workspaces:/app/.agent-workspaces`。此次迁移未修改本地忽略的 Compose 配置或远端部署。 - 根目录只读,可写文件放在 `files/`、`tmp/`、`.cache/`,避免模型生成的插件或配置被 OpenCode 自动加载。原生文件工具也检查规范路径、符号链接和会话范围,保留 `.env`、`data`、`logs` 静态拒绝与编辑审批。 - root 容器中的 Shell 降为 UID/GID 10001;普通 Ubuntu 用户运行时保留本身 UID,由 Landlock/seccomp 提供限制。环境变量只保留明确的基础运行白名单,模型、用户和内部服务凭据不进入沙箱。系统程序、Python 环境和 Skill 目录只读,网络 socket、进程检查/发信号及脱离进程组被拒绝。 - Shell 的 timeout 单位是秒,默认 120,最大 1800。stdout 默认限 1 MiB(`SANDBOX_MAX_OUTPUT_BYTES`),stderr 最多 256 KiB,单文件写入最多 128 MiB。较长 stdout/stderr 保存在本会话 files/,响应返回 3000 字节预览和 stdout_file/stderr_file 路径,避免落到无法读取的 OpenCode 全局截断文件。超时、取消、输出超限及命令结束均清理该进程组,不提供持久后台进程。会话文件保留在配置根目录,当前未增加自动文件保留期限或附件下载接口。 - Shell 和文件修改即使在 `approval_mode=always` 也会请求用户确认;常规业务工具沿用自动批准。配置中的 deny 继续生效。 `web_search` 对接 `POST /api/v1/web-searches`(Bocha),支持 query、五种 freshness、summary、1–50 条结果及域名 include/exclude。`geocode` 对接 `POST /api/v1/geocoding-requests`(天地图),校验成功状态和坐标范围,并返回 `center=[lon,lat]`、`source_crs=EPSG:4326`。坐标可直接用于 `zoom_to_map`。两个工具使用现有凭据续期机制,403 不触发续期;服务密钥只配置在 Server 的 `BOCHA_API_KEY` 和 `TIANDITU_GEOCODER_TOKEN` 中。 client 模式的外部 OpenCode 需要使用 `serviceOpencodeConfig` 加载可信插件包 `.opencode/tjwater-plugin/` 和 Agent 指令,并设置与 Agent 相同的绝对 `TJWATER_AGENT_WORKSPACE_ROOT`(等于 `CONVERSATION_WORKSPACE_ROOT`)、内部桥地址和密钥。隔离联调启动器已完成这些设置。2.0.24 的显式插件配置要求目录包;冷 Location 的模型发现必须等插件初始化完成。 P2 的大结果文件中转、供水瓶颈和服务区算法未迁入。