feat(agent): unify models and validate OpenCode providers

Generate chat, task and learning models from agent-models.json.
Use WaterNetwork fast/expert models with 272k context and validate
upstream names before readiness. Add an isolated model check command
with signal-safe cleanup and plugin, Skill and MCP management docs.

Verified in isolation: type checks, 26 focused tests and both model
catalog checks; no model inference requests.
This commit is contained in:
2026-10-09 11:28:37 +08:00
parent 75d8d526f9
commit d58bd19039
30 changed files with 1125 additions and 469 deletions
+268 -146
View File
@@ -1,178 +1,243 @@
# TJWater 供水 Agent 实验服务
# TJWater 供水 Agent
基于 OpenCode 的实验性供水 Agent 服务,用于编排 TJWater 供水管网前端交互。排水数据、排水分析流程和排水前端交互不在本仓库范围内。
这个服务把供水 WebGIS 中的对话、数据查询、地图操作和工况任务接到 OpenCode 2。浏览器负责展示和交互,Agent 编排工具,TJWater Server 校验身份、项目权限并保存业务状态。
本仓库与生产环境中的 `TJWaterAgent` 历史代码相互独立。浏览器身份和项目范围由 TJWater Server 验证,业务访问使用类型化工具。
本仓库仍是实验服务,与历史 `TJWaterAgent` 仓库独立。排水业务由排水 Agent 仓库维护。
## 当前边界
## 配置文件在哪里
- 生产请求必须携带 `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 沙箱中执行本地文件计算。
| 要修改什么 | 文件或位置 | 说明 |
| --- | --- | --- |
| 模型、provider、API 地址、默认模型 | [agent-models.json](agent-models.json) | 模型配置的唯一入口 |
| API key、服务地址、端口等环境变量 | 仓库根目录 `.local.env` | 本地私有文件,已被 Git 忽略;字段参考 [.env.example](.env.example) |
| 工具权限、外部 MCP 连接 | [opencode.json](opencode.json) | 不再单独配置模型和 provider |
| Agent 的业务指令 | [.opencode/agents/instruction.md](.opencode/agents/instruction.md) | 指令文件不指定模型 |
| Agent、插件、Skill 和 MCP 的管理索引 | [.opencode/agent-plugin.json](.opencode/agent-plugin.json) | 描述用途和实际配置入口;管理方法见 [.opencode/README.md](.opencode/README.md) |
| 代码修改规则 | [AGENTS.md](AGENTS.md) | 面向维护代码的 Agent |
## 主要 API
服务自动读取根目录的 `.local.env`,已有系统环境变量优先。普通启动不会自动读取 `.env`;部署时应由容器或进程管理器注入环境变量。
```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/mcp
GET /api/v1/agent/chat/session/:session_id/stream
POST /api/v1/agent/chat/stream
POST /api/v1/agent/chat/abort
如果不想修改仓库自带的模型配置,可以复制为 `agent-models.local.json`,再在 `.local.env` 中设置:
```dotenv
AGENT_MODEL_CONFIG_PATH=./agent-models.local.json
```
工况 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 与租约状态的唯一权威。
该文件已被 Git 和镜像构建忽略。它会替换整份模型配置,不与 `agent-models.json` 合并;相对路径按启动目录解析。
聊天 UIMessage 流与工作台业务事件流相互独立;业务状态由 Server 的 `/api/v1/workbench/events` 推送,不能混入 token SSE。
## 本地启动
`POST /api/v1/agent/chat/stream` 通过 SSE 返回事件流。主要事件如下:
在本仓库目录执行命令。运行环境需要 Bun、OpenCode 2、Python 3 和 `libseccomp2`,Linux 内核需支持 Landlock ABI 4 或更高版本。服务启动时会实际探测沙箱,失败会停止启动。
| 事件 | 用途 |
| --- | --- |
| `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)。
## 本地开发
仓库使用 OpenCode `2.0.24`,安装并检查 CLI:
```bash
bun add --global --trust --exact @opencode/cli@2.0.24
opencode2 --version
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 访问。
```bash
test -e .local.env || cp .env.example .local.env
chmod 600 .local.env
```
独立管理 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` 覆盖这些隔离值;启动器不会输出内部密钥。
在 `.local.env` 中填写启用的 provider 所需密钥,以及 TJWater Server 的实际地址。下面只列字段,密钥占位值需要替换:
## CI/CD
```dotenv
HOST=127.0.0.1
PORT=8787
OPENCODE_MODE=embedded
TJWATER_SERVER_BASE_URL=http://127.0.0.1:8000
WATERNETWORK_API_KEY=replace-with-your-key
```
Gitea Actions 通过 `.gitea/workflows/package.yml` 复用
`OrgTJWater/ci-templates` 的容器发布流程。手动触发只构建镜像并在 Dev
服务器运行候选验证;从 `main` 创建并推送 `v*` tag 后,候选验证通过才会
提升为正式 `agent` 服务。
默认仅启用 WaterNetwork,填写 `WATERNETWORK_API_KEY` 即可。增加其他 provider 时,还需提供对应凭据,并将默认模型引用指向有效的已配置模型。
仓库需要配置 `REGISTRY_USERNAME`、`REGISTRY_PASSWORD` 和
`DEV_DEPLOY_SSH_KEY` 三个 Gitea Secrets。`.env`、持久化数据和日志不会写入镜像;它们由 Dev 服务器通过环境文件和卷挂载。
配置完成后检查模型,再启动服务:
## OpenCode 2 接入
```bash
bun run check:models
bun run dev
```
运行时使用 `@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
curl --fail http://127.0.0.1:8787/health
```
工具变化:`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 审批和禁用子代理。
正常响应中 `ok`、`ready`、`warmed_up` 均为 `true`,并包含 OpenCode 版本和沙箱探测结果。`bun run dev` 使用 watch 模式;无 watch 的启动命令是 `bun run start`。修改模型配置或密钥后需重启服务,不依赖 watch 自动加载配置文件。
官方依据:[迁移指南](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` 标签不可作为当前稳定版安装依据。
开发环境允许无凭据的本地对话,但访问 Server 业务数据仍需产品凭据。生产环境中的聊天接口要求 `Authorization: Bearer ...` 和 `X-Project-Id`,用户身份和项目权限由 Server 验证。项目 ID 应使用 Server 返回的项目 UUID;本地上下文头不能替代产品鉴权。
## 旧 Agent 的 P0/P1 能力迁移
## 配置模型和自定义 provider
- `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 接口确认。
[agent-models.json](agent-models.json) 同时控制聊天菜单和 OpenCode 的模型定义。当前仅包含地址为 `https://llm.waternetwork.cn/v1` 的 `waternetwork` provider,保留两个模型:`gpt-6-luna` 显示为“快速”,使用闪电图标;`gpt-6.1-sol` 显示为“专家”,使用星光图标。两者的上下文窗口均配置为 272,000 tokens。
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 继续生效。
| 字段 | 用途 | 当前配置 |
| --- | --- | --- |
| `default_model` | 普通聊天默认值 | `waternetwork/gpt-6-luna` |
| `task_model` | 后台工况任务首选模型 | `waternetwork/gpt-6-luna` |
| `learning_gate_model` | 判断是否需要学习复盘 | `waternetwork/gpt-6-luna` |
`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` 中。
后两项省略时沿用 `default_model`。三个引用都必须指向已定义、启用且声明支持文本输出和工具调用的模型;聊天默认模型还必须在菜单中可选。
client 模式的外部 OpenCode 需要使用 `serviceOpencodeConfig` 加载可信插件包 `.opencode/tjwater-plugin/` 和 Agent 指令,并设置与 Agent 相同的绝对 `TJWATER_AGENT_WORKSPACE_ROOT`(等于 `CONVERSATION_WORKSPACE_ROOT`)、内部桥地址和密钥。隔离联调启动器已完成这些设置。2.0.24 的显式插件配置要求目录包;冷 Location 的模型发现必须等插件初始化完成。
### 使用 WaterNetwork 的最小配置
下面是一份完整示例,三个用途都使用 `gpt-6.1-sol`。可以将它保存到自己的模型配置文件中:
P2 的大结果文件中转、供水瓶颈和服务区算法未迁入。
```json
{
"default_model": "waternetwork/gpt-6.1-sol",
"task_model": "waternetwork/gpt-6.1-sol",
"learning_gate_model": "waternetwork/gpt-6.1-sol",
"providers": {
"waternetwork": {
"name": "WaterNetwork",
"api_url": "https://llm.waternetwork.cn/v1",
"api_key": "{env:WATERNETWORK_API_KEY}",
"models": {
"gpt-6.1-sol": {
"name": "通用助手",
"description": "对话与任务处理",
"icon": "sparkle",
"limit": { "context": 272000 }
}
}
}
}
}
```
## 外部 MCP 客户端
新增其他 provider 时,在 `providers` 下增加同样结构的一项,并换成自己的地址、密钥变量名和模型名。provider 名只允许字母、数字、下划线和连字符。
供水 Agent 复用 OpenCode 2.0.24 的 MCP 客户端,支持远程 Streamable HTTP、管理员配置的本地 stdio、原生 OAuth、工具目录更新和取消调用。当前 MCP 正式协议为 [2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28),配置依据见 [OpenCode MCP 文档](https://opencode.ai/v2/docs/mcp-servers/)。本次接入不发布 TJWater MCP 服务,也不增加 MCP Apps 或 Tasks 的前端界面。
`api_url` 填 API 基础地址,例如以 `/v1` 结尾的地址,不填 `/chat/completions`;不允许在地址中嵌入账号、密码、查询参数或片段。默认使用 OpenAI compatible 运行包,真实密钥始终放在环境变量中。
仓库中的 `mcp.servers` 默认是 `{}`。管理员在服务的 `opencode.json` 或 `OPENCODE_CONFIG_CONTENT` 中配置连接;模型和浏览器不能提交 MCP 地址、进程命令或认证头。所有服务名必须使用 `mcp_` 前缀,其余字符限英文字母、数字、下划线和连字符。下面是配置片段,示例默认禁用;替换为实际地址/程序后再启用:
| 模型字段 | 怎么填写 |
| --- | --- |
| `models` 的键 | Agent 使用的模型名;完整引用为 `provider/模型名` |
| `model_id` | 上游实际接收的名称,省略时与键名相同;支持 `org/model` 这样的名称 |
| `name`、`description`、`icon` | 聊天菜单的名称、说明和图标;图标可用 `bolt` 或 `sparkle` |
| `disabled: true` | 禁用模型,不进入聊天菜单,也不参与运行时校验 |
| `selectable: false` | 从聊天菜单隐藏,仍可供后台或学习使用,仍需通过校验 |
| `capabilities` | 工具调用和输入、输出类型;默认声明支持工具调用和文本输入、输出 |
| `limit`、`compatibility` | provider 所需的上下文限制和兼容设置,按实际服务能力填写 |
DeepSeek provider 已停用,默认角色不再引用它。重新启用 Flash 时,模型名使用 `deepseek-flash`;其他自定义别名可通过 `model_id` 映射到上游名称。配置文件使用标准 JSON,移除停用条目或给模型设置 `disabled: true`,不使用注释禁用配置。
旧的 `OPENCODE_MODEL`、`OPERATIONAL_TASK_MODEL`、`OPENCODE_MODEL_OPTIONS` 环境变量已不控制模型选择。在 `opencode.json` 或指令文件中另写模型定义也不会覆盖统一配置。
### 保存后检查模型
```bash
bun run check:models
```
该命令启动独立的 OpenCode 2,核对所有未禁用模型的注册结果、启用状态、工具能力声明和实际请求名称,再由插件使用 OpenCode 生效的 provider 配置读取上游 `/models` 目录。手工注册了一个名字,也必须在上游目录中找到对应名称才能通过。
命令不发送模型推理请求,结束后清理自己启动的进程和临时目录。检查通过表示模型名称和运行时配置一致;模型的推理效果、额度和实际工具调用表现需在使用时验证。
使用 Ctrl+C 或发送 SIGTERM 取消检查时,命令会等待自己的 OpenCode 子进程退出,再清理临时目录。
Agent 启动时也执行同一检查,失败则不监听 HTTP。常见结果如下:
| 检查结果 | 排查方向 |
| --- | --- |
| `not_advertised` | 上游没有该名称,检查 `model_id` 和 provider 的模型目录 |
| `not_registered`、`not_enabled` | OpenCode 未加载或未启用该定义;client 模式需同步配置 |
| `wire_id_mismatch`、`no_tools` | 运行时的名称映射或工具能力声明与配置不一致 |
| `credentials_unavailable` | OpenCode 没有取得该 provider 的凭据 |
| `catalog_unavailable` | 检查网络、API 基础地址或鉴权;HTTP 401/403 应排查凭据和权限 |
| `catalog_invalid` | `/models` 返回的数据结构不符合预期 |
| `unsupported_catalog` | 当前检查器不支持该运行包的目录接口 |
检查器支持 OpenAI compatible、OpenAI、OpenRouter 和 xAI 运行包的 `/models` 目录,其他运行包会拒绝启动。报告不输出密钥、连接地址或上游原始错误内容。
## Agent 与 OpenCode 的运行方式
默认 `OPENCODE_MODE=embedded`,Agent 启动和关闭自己的 OpenCode 子进程,并自动管理连接密码和内部工具桥密钥。`OPENCODE_COMMAND` 可指定 CLI 的绝对路径,`OPENCODE_PORT=0` 让系统分配空闲端口。
`OPENCODE_MODE=client` 用于连接单独管理的 OpenCode 服务,需要配置 `OPENCODE_CLIENT_BASE_URL`、`OPENCODE_CLIENT_PASSWORD`,并在两个进程中设置相同的稳定 `AGENT_INTERNAL_TOKEN`。
外部 OpenCode 还需通过 `serviceOpencodeConfig` 加载同一份模型配置、可信插件包 `.opencode/tjwater-plugin/` 和业务指令。其 `TJWATER_AGENT_WORKSPACE_ROOT` 必须与 Agent 的 `CONVERSATION_WORKSPACE_ROOT` 指向同一个绝对目录,同时设置 `TJWATER_AGENT_INTERNAL_BASE_URL` 指向 Agent 内部工具桥。只启动一个普通 OpenCode 服务不足以完成接入;缺少业务工具、模型检查 RPC 或目录绑定会导致预热失败。
要保留已有服务进行隔离联调,可以使用仓库启动器:
```bash
bun run start:integration:isolated
```
默认端口为 Agent `18787`、OpenCode `14096`,数据保存在 `data/integration-18787/`,不复用默认会话数据。可通过 `INTEGRATION_AGENT_PORT`、`INTEGRATION_OPENCODE_PORT` 和 `INTEGRATION_DATA_DIR` 修改。启动器负责配对配置和停止自己创建的两个进程。
## `.agent-workspaces` 是什么
这是会话的文件工作区,默认位置由 `CONVERSATION_WORKSPACE_ROOT=./.agent-workspaces` 决定。每创建一个会话,就创建一个独立目录,即使该会话尚未调用 Shell,也可能已有空工作区:
```text
.agent-workspaces/
└── conversation-<uuid>/
├── files/ 生成文件、计算脚本、结果和较长的 Shell 输出
├── tmp/ 临时文件
└── .cache/ 运行缓存
```
OpenCode 会话保存与该目录的绑定,恢复时继续使用原目录。会话分支使用独立的空工作区;历史上绑定到仓库根目录的旧会话会在继续执行前迁入新工作区,不复制源码、凭据或其他会话文件。
Shell 默认在 `files/` 中运行,只能写入上述三个子目录,工作区根目录只读。目录隔离配合 Landlock 和 seccomp 限制文件访问、网络与进程行为;目录本身不等于 OS 沙箱。限制覆盖 Shell 及其子进程,OpenCode 服务和类型化业务工具仍由可信宿主进程运行。本地 stdio MCP 也不在这个沙箱中。
Shell 和文件修改需要用户审批,即使聊天设置了 `approval_mode=always` 也会请求确认。Shell 不能联网或访问源码、密钥、其他会话;业务查询、网页搜索和地址解析使用类型化工具。超时单位为秒,默认 120 秒、最大 1800 秒;stdout 默认限制 1 MiB、stderr 最多 256 KiB、单文件最多 128 MiB。较长输出以文件保存在 `files/`,工具返回预览和路径;取消、超时、超限和命令结束均清理该进程组,不保留后台进程。
工作区已被 Git 和镜像构建忽略,但其中的文件需要随会话保留。目前没有自动过期清理,删除聊天记录也不会同步删除工作区。启动探测、预热和创建失败产生的临时工作区会清理。
删除仍被会话引用的目录会丢失生成文件,并可能使会话后续文件操作失败。需要改变根目录时,应同步保留会话数据库及目录绑定,不能只改环境变量后丢弃原目录。服务目前没有工作区附件下载接口。
持久化部署需要分别保存:
| 数据 | 默认位置 | 用途 |
| --- | --- | --- |
| 会话文件 | `.agent-workspaces/` | 文件计算和生成结果 |
| Agent 数据 | `data/` | 会话元数据、对话记录、记忆、学习状态和结果引用 |
| Skill 文件 | `.opencode/skills/` | 工具可读取的 Skill,目录可由 `OPENCODE_SKILLS_ROOT_DIR` 指定 |
| OpenCode 数据 | OpenCode 的 XDG 数据目录 | 原生会话数据库;部署时保留对应数据卷 |
| 日志 | `logs/` | 运行审计和学习日志 |
仅持久化 `data/` 无法保留会话生成的文件。`result_ref` 有独立的过期清理,不代表工作区也会自动清理。
## 当前能做什么
| 场景 | 工具或协议 | 说明 |
| --- | --- | --- |
| 查询供水数据 | `query_project_data` | 有限数据源:元素属性、SCADA 设备与历史、元素历史、分析运行和分页结果 |
| 地图与图表交互 | `locate_features`、`zoom_to_map`、`show_chart`、`view_history` | 通过 `frontend-action@1` 等待浏览器实际回执 |
| SCADA 地图分析 | `render_scada_analysis`、`clear_scada_analysis` | 展示和清除受控叠加层 |
| 网页搜索与地址解析 | `web_search`、`geocode` | 调用 Server 的 Bocha、天地图接口,模型不能指定任意 URL 或鉴权头 |
| 工况任务 | 查询、计划、报告、行动建议和人工证据工具 | 通过 Server 的任务与运行身份访问业务 |
| 会话与学习 | `session_search`、`memory_manager`、`skill_manager` | 按会话、用户和项目范围管理记录、记忆和 Skill |
| 本地计算 | `shell` | 在会话 OS 沙箱中运行经过审批的命令 |
| 外部资料和工具 | MCP 客户端 | 由管理员配置,并按会话权限调用 |
历史查询的时间窗口最长 24 小时,SCADA 字段限 `monitored_value` 和 `cleaned_value`;物理指标和单位以设备元数据为准。历史查询由 Server 校验只读权限,清洗操作仍需清洗权限。`view_history` 当前最多展示 100 个非空样本,不代表完整高频历史;缺失资产和历史缺数会返回实际状态。
`web_search` 支持 freshness、summary、1 至 50 条结果以及域名筛选;`geocode` 返回 `center=[lon,lat]`、`source_crs=EPSG:4326`,可用于地图定位。服务密钥配置在 Server 的 `BOCHA_API_KEY` 和 `TIANDITU_GEOCODER_TOKEN` 中。
后台执行由 worker 提交 Server 已保存的 `task_id`、`run_id`、`execution_request_id`。Agent claim 成功后每 20 秒发送心跳,运行正常结束显式调用 `complete`,失败或中止调用 `fail`。Server 决定活动运行和租约状态;计划、报告或 `final_answer` 的发布不代表业务任务完成。执行计划规则见 [工况执行计划](docs/execution-plan.md)。
交互会话遇到 `credential_refresh_required` 后,前端更新凭据;令牌只通过鉴权头传递,工具请求最多重试一次,403 不触发续期。后台 worker 使用已有 OAuth provider 更新凭据,不等待浏览器;无法写回失败时仍由 Server 的租约恢复处理。
服务保留 `agent-ui@1` UIEnvelope 兼容接口,目前该注册表为空。当前 6 个浏览器动作使用独立的 `frontend-action@1` 注册表;前端需声明能力并回传动作结果。待办使用 `todoread`、`todowrite`,通过 `todo_update` 更新前端。协议见 [Agent UI 协议](docs/agent-ui-protocol.md)。
大结果文件中转、供水瓶颈和服务区算法仍未从旧仓库迁入。
## 配置外部 MCP
在 `opencode.json` 的 `mcp.servers` 中添加服务,或由部署环境提供 `OPENCODE_CONFIG_CONTENT`。默认未连接任何外部 MCP。连接地址、命令和凭据由管理员配置,不接受浏览器或模型临时提交。
下面给出远程 HTTP 和本地 stdio 两种配置片段,示例均禁用,填写真实连接信息后再启用:
```json
{
@@ -200,22 +265,79 @@ P2 的大结果文件中转、供水瓶颈和服务区算法未迁入。
}
```
远程默认 `protocol: "auto"`,探测新协议并兼容旧协议;本地默认 `legacy`,避免对旧 stdio 程序额外启动探测进程。只服务新协议时可显式设为 `"2026-07-28"`。`codemode` 沿用原生默认值 `true`,设为 `false` 可直接暴露工具;两种入口执行相同的会话策略。超时单位为毫秒,全局默认启动/目录各 10 秒、执行 60 秒,已有显式全局值和每个服务的覆盖值优先。
服务名必须以 `mcp_` 开头。远程默认使用 `auto`,本地默认使用 `legacy`;代码还支持显式协议值 `2026-07-28`。超时单位为毫秒,默认启动和目录查询各 10 秒、执行 60 秒,可按服务覆盖。`codemode` 默认开启,改为 `false` 时直接暴露工具,两种方式使用同一权限策略。
只有管理员核实为只读的工具才自动放行:在已有 `permissions` 数组的 `mcp_*` 审批规则后追加精确工具名的 `allow`,保留原有敏感路径拒绝规则。例如:
管理员核实为只读的工具,可以在 `permissions` 中添加精确的自动放行规则;名称以会话 MCP 状态接口返回的工具 ID 为准,后续匹配的拒绝规则仍生效:
```json
{ "action": "mcp_docs_search", "resource": "*", "effect": "allow" }
```
工具的有效名称为 `<server>_<tool>`,不支持的字符由 OpenCode 规范为 `_`,以状态接口实际返回的名称为准。宽泛的 `mcp_* allow`、服务自报的 `readOnlyHint`、历史“始终允许”都不构成只读白名单。后续匹配的拒绝规则继续生效。普通聊天和任务协作中,白名单外调用进入现有 `permission_request` 审批,即使 `approval_mode=always` 也不会自动批准;后台 `task_execution` 只能调用白名单工具,学习复盘 `review` 禁止 MCP 调用,不等待浏览器审批。原生通用 MCP 资源列举/读取入口也受限制:交互会话审批,后台和复盘会话禁止,不能绕过逐个工具的白名单。外部结果是检索资料,不能直接替代 Server 已保存的业务证据或改变调用权限。
宽泛的 `mcp_* allow`、服务自报的 `readOnlyHint` 和历史审批不构成只读白名单。普通聊天和任务协作中,白名单外调用需用户审批;后台任务只能调用白名单工具,学习复盘禁用 MCP。通用 MCP 资源读取也受会话策略约束,外部结果不能直接替代 Server 保存的业务证据。
`GET /api/v1/agent/chat/session/:session_id/mcp` 沿用聊天接口的 Bearer、`X-Project-Id` 与会话归属校验。它返回 `servers`,每项仅含 `name`、`type`、`status`、`configured_protocol`、`tools`。状态为 `connected/pending/disabled/failed/needs_auth`;`configured_protocol` 是配置模式,不是协商后的版本。响应不包含连接地址、命令、认证头、OAuth 凭据或原始错误;查询不会创建或迁移会话目录。插件通过内部令牌保护的 `/internal/mcp/session-policy` 获取主服务已有的会话策略,不能凭请求 body 伪造用户或项目。
需要 OAuth 的服务应省略 `oauth: false`,由管理员以同一服务账号和 XDG 数据目录执行 `opencode2 mcp auth mcp_docs`。TJWater 的 Bearer 和项目头不会自动转交外部 MCP。
需要 OAuth 的服务应省略 `oauth: false`。管理员用同一服务账号、配置、环境和 XDG 数据目录执行 `opencode2 mcp auth mcp_docs`,认证流程、PKCE 与令牌续期由 OpenCode 管理;本项目不向前端暴露 OAuth 凭据,也不把 TJWater 的 Bearer 或项目头自动转交给外部 MCP。原生客户端发送的 OpenCode session ID 仅供调用关联,不是授权凭据。
本地 stdio MCP 是管理员信任的宿主进程,不受会话 Shell 沙箱约束。插件收敛继承环境,只传入基础运行变量及明确配置的 MCP 环境;该进程仍有服务账号的宿主文件和网络权限。
本地 stdio 是管理员信任的宿主进程,**不在会话 Shell 的 Landlock/seccomp 沙箱中**。插件将继承环境中除基础运行变量以外的值置空,只添加该服务明确配置的环境变量,内部 Agent/OpenCode 凭据变量始终置空;它仍具有服务账号的宿主文件和网络权限,因此环境收敛不等于 OS 沙箱。
会话 MCP 状态接口返回服务名、类型、状态、配置协议和工具 ID,不返回连接地址、命令或凭据。状态可能为 `connected`、`pending`、`disabled`、`failed`、`needs_auth`;配置协议不代表实际协商版本,连接成功也不代表工具目录已加载。结构错误会阻止启动,可选服务连接失败或等待 OAuth 不影响核心服务就绪。
配置结构错误会使启动失败;可选服务的连接失败或 `needs_auth` 不使核心 Agent 的 `/health` 变为未就绪。client 模式须同步更新可信插件和服务配置,缺少新版状态 RPC 将不能通过 warmup。禁用或移除服务并重启运行时即可回退,无需数据迁移。
## 前端与任务接入
本次定向验证:`bun run check`、`bun test tests/runtime/mcpConfig.test.ts tests/opencode/mcpPolicy.test.ts tests/routes/mcpRoutes.test.ts tests/runtime/opencode.test.ts tests/frontendAction/callIdPlugin.test.ts`。可选的本地原生联调为 `bun run tests/integration/mcp-smoke.ts`:使用临时 XDG 目录和样例 HTTP/stdio 服务,结束后清理自己的进程与数据,不请求模型。
聊天 API 的前缀是 `/api/v1/agent/chat`。生产请求需携带产品 Bearer 和项目头;涉及会话的操作还校验会话归属。
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/health` | OpenCode 和沙箱就绪状态,无聊天前缀 |
| GET | `/models` | 聊天模型菜单与默认值,不含 provider 凭据 |
| POST | `/session` | 创建会话 |
| GET | `/sessions`、`/session/:session_id` | 列表和会话内容 |
| DELETE | `/session/:session_id` | 删除聊天记录,不同步清理工作区 |
| POST | `/stream` | 发起对话并接收 SSE |
| GET | `/session/:session_id/stream` | 订阅会话流 |
| POST | `/abort` | 中止运行 |
| POST | `/permission/:request_id/reply` | 回复工具审批 |
| POST | `/question/:request_id/reply`、`/question/:request_id/reject` | 回复或拒绝澄清问题 |
| POST | `/session/:session_id/credentials` | 刷新运行凭据 |
| GET | `/ui-registry`、`/frontend-action-registry` | 查询界面与浏览器动作协议 |
| POST | `/frontend-actions/:action_id/result` | 浏览器回传动作结果 |
| GET | `/session/:session_id/mcp` | 查询本会话 MCP 状态和实际工具 ID |
| POST | `/task-executions` | 提交后台任务执行 |
| POST | `/task/:task_id/collaboration/session` | 创建任务协作会话 |
SSE 包含 `token`、`progress`、`ui_envelope`、`frontend_action`、`permission_request`、`question_request`、`todo_update`、`credential_refresh_required`、`session_title`、`done` 和 `error`,并保留兼容用途的 `tool_call` 事件。聊天 token 流与 Server 的 `/api/v1/workbench/events` 业务事件流相互独立。
开发环境的本地上下文可通过 `AGENT_LOCAL_USER_ID`、`AGENT_LOCAL_PROJECT_ID`、`AGENT_LOCAL_NETWORK` 设置,对应请求头为 `X-Agent-Local-User`、`X-Agent-Local-Project`、`X-Agent-Local-Network`。`X-Trace-Id` 用于请求追踪。
## 部署与维护
[Dockerfile](Dockerfile) 安装匹配的 OpenCode 2 和沙箱依赖,并把统一模型配置复制到镜像。环境文件、运行数据和日志不进入镜像。容器仍依赖宿主内核支持 Landlock。
部署时通过环境文件或密钥管理器注入凭据,并将模型文件及运行数据按需要挂载。比如 Compose 服务可包含以下片段,需合并到自己的完整部署配置中:
```yaml
env_file:
- .local.env
volumes:
- ./agent-models.json:/app/agent-models.json:ro
- ./agent-workspaces:/app/.agent-workspaces
- ./data:/app/data
- ./logs:/app/logs
- ./skills:/app/.opencode/skills
- ./opencode-data:/root/.local/share/opencode
```
配置 `AGENT_MODEL_CONFIG_PATH` 时需把相应文件挂载到容器可读路径。`docker-compose.yml` 是被 Git 忽略的本地配置,使用前需核对当前模型入口、环境变量和数据卷;不要沿用旧模型环境变量。
常用维护命令:
| 命令 | 用途 |
| --- | --- |
| `bun run check` | 检查服务和 OpenCode 插件的 TypeScript 类型 |
| `bun run check:models` | 通过实际 OpenCode 和上游目录检查模型,不请求推理 |
| `bun run start:prod` | 类型检查通过后启动服务 |
| `bun test tests/**/*.test.ts` | 全部单元测试,日常修改优先运行相关测试 |
| `bun run tests/integration/mcp-smoke.ts` | 可选的本地 MCP 联调,使用自己的样例服务和临时数据,不请求推理 |
[CI 工作流](.gitea/workflows/package.yml) 在推送 `v*` 标签或手动触发时调用共享容器流水线。当前镜像名为 `gitea.waternetwork.cn/orgtjwater/next-tjwater-agent`,部署服务名为 `next-agent`;仓库需设置 `REGISTRY_USERNAME`、`REGISTRY_PASSWORD`、`DEV_DEPLOY_SSH_KEY`。具体候选验证和发布步骤由共享模板定义。
代码修改约束见 [AGENTS.md](AGENTS.md),前端协议见 [Agent UI 协议](docs/agent-ui-protocol.md),任务计划规则见 [工况执行计划](docs/execution-plan.md)。