Files
next-tjwater-agent/README.md
T
jiang d58bd19039 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.
2026-10-09 11:28:37 +08:00

344 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TJWater 供水 Agent
这个服务把供水 WebGIS 中的对话、数据查询、地图操作和工况任务接到 OpenCode 2。浏览器负责展示和交互,Agent 编排工具,TJWater Server 校验身份、项目权限并保存业务状态。
本仓库仍是实验服务,与历史 `TJWaterAgent` 仓库独立。排水业务由排水 Agent 仓库维护。
## 配置文件在哪里
| 要修改什么 | 文件或位置 | 说明 |
| --- | --- | --- |
| 模型、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 |
服务自动读取根目录的 `.local.env`,已有系统环境变量优先。普通启动不会自动读取 `.env`;部署时应由容器或进程管理器注入环境变量。
如果不想修改仓库自带的模型配置,可以复制为 `agent-models.local.json`,再在 `.local.env` 中设置:
```dotenv
AGENT_MODEL_CONFIG_PATH=./agent-models.local.json
```
该文件已被 Git 和镜像构建忽略。它会替换整份模型配置,不与 `agent-models.json` 合并;相对路径按启动目录解析。
## 本地启动
在本仓库目录执行命令。运行环境需要 Bun、OpenCode 2、Python 3 和 `libseccomp2`,Linux 内核需支持 Landlock ABI 4 或更高版本。服务启动时会实际探测沙箱,失败会停止启动。
仓库使用 OpenCode `2.0.24`,安装并检查 CLI:
```bash
bun add --global --trust --exact @opencode/cli@2.0.24
opencode2 --version
bun install
```
首次配置时创建本地环境文件,已有文件则直接编辑:
```bash
test -e .local.env || cp .env.example .local.env
chmod 600 .local.env
```
在 `.local.env` 中填写启用的 provider 所需密钥,以及 TJWater Server 的实际地址。下面只列字段,密钥占位值需要替换:
```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
```
默认仅启用 WaterNetwork,填写 `WATERNETWORK_API_KEY` 即可。增加其他 provider 时,还需提供对应凭据,并将默认模型引用指向有效的已配置模型。
配置完成后检查模型,再启动服务:
```bash
bun run check:models
bun run dev
```
另开一个终端查看启动结果,地址和端口按实际配置替换:
```bash
curl --fail http://127.0.0.1:8787/health
```
正常响应中 `ok`、`ready`、`warmed_up` 均为 `true`,并包含 OpenCode 版本和沙箱探测结果。`bun run dev` 使用 watch 模式;无 watch 的启动命令是 `bun run start`。修改模型配置或密钥后需重启服务,不依赖 watch 自动加载配置文件。
开发环境允许无凭据的本地对话,但访问 Server 业务数据仍需产品凭据。生产环境中的聊天接口要求 `Authorization: Bearer ...` 和 `X-Project-Id`,用户身份和项目权限由 Server 验证。项目 ID 应使用 Server 返回的项目 UUID;本地上下文头不能替代产品鉴权。
## 配置模型和自定义 provider
[agent-models.json](agent-models.json) 同时控制聊天菜单和 OpenCode 的模型定义。当前仅包含地址为 `https://llm.waternetwork.cn/v1` 的 `waternetwork` provider,保留两个模型:`gpt-6-luna` 显示为“快速”,使用闪电图标;`gpt-6.1-sol` 显示为“专家”,使用星光图标。两者的上下文窗口均配置为 272,000 tokens。
三个默认模型分别用于:
| 字段 | 用途 | 当前配置 |
| --- | --- | --- |
| `default_model` | 普通聊天默认值 | `waternetwork/gpt-6-luna` |
| `task_model` | 后台工况任务首选模型 | `waternetwork/gpt-6-luna` |
| `learning_gate_model` | 判断是否需要学习复盘 | `waternetwork/gpt-6-luna` |
后两项省略时沿用 `default_model`。三个引用都必须指向已定义、启用且声明支持文本输出和工具调用的模型;聊天默认模型还必须在菜单中可选。
### 使用 WaterNetwork 的最小配置
下面是一份完整示例,三个用途都使用 `gpt-6.1-sol`。可以将它保存到自己的模型配置文件中:
```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 }
}
}
}
}
}
```
新增其他 provider 时,在 `providers` 下增加同样结构的一项,并换成自己的地址、密钥变量名和模型名。provider 名只允许字母、数字、下划线和连字符。
`api_url` 填 API 基础地址,例如以 `/v1` 结尾的地址,不填 `/chat/completions`;不允许在地址中嵌入账号、密码、查询参数或片段。默认使用 OpenAI compatible 运行包,真实密钥始终放在环境变量中。
| 模型字段 | 怎么填写 |
| --- | --- |
| `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
{
"mcp": {
"timeout": { "startup": 10000, "catalog": 10000, "execution": 60000 },
"servers": {
"mcp_docs": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"protocol": "auto",
"oauth": false,
"headers": { "Authorization": "Bearer {env:MCP_DOCS_TOKEN}" },
"disabled": true
},
"mcp_company": {
"type": "local",
"command": ["node", "/opt/company-mcp/server.js"],
"cwd": "/opt/company-mcp",
"environment": { "MCP_API_KEY": "{env:COMPANY_MCP_API_KEY}" },
"protocol": "legacy",
"disabled": true
}
}
}
}
```
服务名必须以 `mcp_` 开头。远程默认使用 `auto`,本地默认使用 `legacy`;代码还支持显式协议值 `2026-07-28`。超时单位为毫秒,默认启动和目录查询各 10 秒、执行 60 秒,可按服务覆盖。`codemode` 默认开启,改为 `false` 时直接暴露工具,两种方式使用同一权限策略。
管理员核实为只读的工具,可以在 `permissions` 中添加精确的自动放行规则;名称以会话 MCP 状态接口返回的工具 ID 为准,后续匹配的拒绝规则仍生效:
```json
{ "action": "mcp_docs_search", "resource": "*", "effect": "allow" }
```
宽泛的 `mcp_* allow`、服务自报的 `readOnlyHint` 和历史审批不构成只读白名单。普通聊天和任务协作中,白名单外调用需用户审批;后台任务只能调用白名单工具,学习复盘禁用 MCP。通用 MCP 资源读取也受会话策略约束,外部结果不能直接替代 Server 保存的业务证据。
需要 OAuth 的服务应省略 `oauth: false`,由管理员以同一服务账号和 XDG 数据目录执行 `opencode2 mcp auth mcp_docs`。TJWater 的 Bearer 和项目头不会自动转交外部 MCP。
本地 stdio MCP 是管理员信任的宿主进程,不受会话 Shell 沙箱约束。插件收敛继承环境,只传入基础运行变量及明确配置的 MCP 环境;该进程仍有服务账号的宿主文件和网络权限。
会话 MCP 状态接口返回服务名、类型、状态、配置协议和工具 ID,不返回连接地址、命令或凭据。状态可能为 `connected`、`pending`、`disabled`、`failed`、`needs_auth`;配置协议不代表实际协商版本,连接成功也不代表工具目录已加载。结构错误会阻止启动,可选服务连接失败或等待 OAuth 不影响核心服务就绪。
## 前端与任务接入
聊天 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)。