TJWaterAgent 内部智能体服务

TJWaterAgent 是 TJWater 内部版智能体服务,负责连接前端聊天界面、OpenCode 运行时、MCP 工具和 TJWater 后端 API。它面向内部研发与部署,保留完整的 Agent 编排、会话上下文、工具调用和运行时调试能力。

主要能力

  • 提供 POST /api/v1/agent/sessions/{session_id}/runs SSE 聊天接口。
  • 以内嵌模式启动并预热 OpenCode 运行时。
  • 管理前端 session_id 与 OpenCode session 的映射。
  • 在服务端保存当前会话的用户 token、项目、network 和 trace 上下文。
  • 通过 .opencode/tools 和 MCP 工具驱动地图定位、图表、SCADA、历史数据和业务 API 调用。
  • 通过 data/ 保存运行时会话元数据、结果引用和本地状态。

目录结构

src/                  服务端 TypeScript 源码
src/routes/           HTTP 路由
src/chat/             聊天流和 SSE 事件适配
src/runtime/          OpenCode 运行时管理
src/session/          会话映射和运行上下文
src/mcp/              MCP 服务与工具桥接
cli/                  Agent 使用的 TypeScript 后端 API CLI
.opencode/agents/    Agent prompt 和模型行为配置
.opencode/tools/     OpenCode 自定义工具
.opencode/skills/    可复用分析工作流
node-tests/           Node CLI 测试
data/                 本地运行时数据,禁止提交
logs/                 本地日志,禁止提交

仓库跟踪 .opencode/skills/ 中经过评审的默认工作流基线;部署环境仍可通过持久化卷保留 skill_manager 在运行中沉淀的增量内容。默认基线不得包含真实客户数据、认证信息或本地执行产物。

本地开发

项目使用 Bun

bun install
bun run dev

bun install 会通过 postinstall 安装 .opencode 子目录依赖。bun run dev 以 watch 模式启动 src/server.ts,修改 src/**.opencode/**opencode.json.local.env 后会自动重启。

cli/tjwater-cli 是当前唯一的 TJWater 业务 CLI 入口,由 Bun 直接执行 cli/tjwater-cli.tscli/src/ 源码,并随 Agent 镜像一起交付,不需要 Python 或 PyInstaller 构建步骤。

常用命令

bun run check
bun run contract:generate
bun run test:api
bun run test:cli
bun run start
bun run start:prod
docker build -t tjwater-agent:local .
  • bun run check:检查主项目和 .opencode 的 TypeScript 类型。
  • bun run contract:generate:生成 contracts/agent-v1.openapi.json
  • bun run test:api:验证公开 REST 契约和聊天路由。
  • bun run test:cli:运行 node-tests/cli/*.node.mjs
  • bun run start:直接启动服务。
  • bun run start:prod:先类型检查,再启动服务。

运行模式

当前运行时使用 OpenCode 稳定版 1.x CLI,并通过稳定版 SDK 的 @opencode-ai/sdk/v2 HTTP 客户端访问运行时;这与 opencode2@opencode-ai/client 的 2.0 beta 运行时不同。Embedded 模式由服务进程拉起本机 OpenCode:

OPENCODE_MODE=embedded
TJWATER_API_BASE_URL=http://127.0.0.1:8000

当前仅支持 Embedded 模式,不支持连接外部 OpenCode server。

生产镜像会从固定的 OpenCode v1.18.13 源码提交构建 CLI,并应用仓库内的 patches/opencode-1.18.13-message-phase.patch。该补丁只透传 OpenAI Responses 输出项已有的 commentary / final_answer phase,不改变模型行为:过程文本继续写入 可折叠的 Agent 过程卡,final_answer 到达后立即按增量写入正式回答。未提供 phase 的 DeepSeek 模型启用 OpenCode 1.18.13 内置的 JSON Schema 最终回答工具 StructuredOutput:模型必须先完成全部分析和工具调用,再把完整回答写入 answer; 该工具成功后 OpenCode 会直接结束运行循环,不再进入下一轮模型或工具调用。Agent 将 answer 映射为正式文本推送;若模型未按协议调用该工具,仍保留会话 idle 后提取最终 文本的兼容兜底。 本地直接运行 bun --watch src/server.ts 时,PATH 中也需要放置应用了同一补丁的 opencode CLI,才能启用 phase 驱动的正式文本流式输出。

认证续期与学习工具

后端工具调用遇到即将过期的 access token 或首次 401 时,Agent 会通过当前 SSE 流发送 credential_refresh_required。前端使用服务端保存的 Keycloak refresh token 强制换取新 access token,再调用 POST /api/v1/agent/sessions/{session_id}/credential-refreshes 唤醒原工具调用。等待上限为 30 秒,同一会话的并发请求合并为一次续期,原调用最多重试一次;403 不触发续期。

memory_managerskill_manager 在 OpenCode 侧只保留内部 HTTP 桥,读取会话上下文和持久化数据的逻辑统一在 Agent 主进程中执行。长期记忆、自动学习和显式工具写入因此共享同一组 MemoryStoreSkillStore 和运行时会话上下文。

本地可使用 .local.env 保存开发配置;系统环境变量优先级更高。

服务会在 HTTP 端口开始监听前完成 OpenCode 健康检查、临时会话创建和工具目录加载。GET /health 返回 ready: truewarmed_up: true 时,表示冷启动预热已经完成。开发环境会输出各预热阶段的耗时。

opencode.json 已启用 experimental.continue_loop_on_deny。用户拒绝权限请求后,OpenCode V1 会把拒绝结果交还给 Agent,让其尝试无需该权限的替代方案,而不是直接结束本轮执行。

前端提供三种整体权限模式:“请求批准”只执行 OpenCode 明确允许的白名单,Shell 和写操作逐次交给用户确认;“自动批准”额外自动放行低风险业务工具、skill、沙箱 Shell,以及真实路径位于当前 conversation workspace 内且通过 realpath/symlink 校验的 read/edit/glob/grep;“始终允许”自动放行当前对话中所有未被 OpenCode 明确禁止的权限请求。自动放行统一使用单次批准,切换整体模式后立即恢复对应策略,不会写入持久授权。

单次权限请求支持“允许一次”“保存授权”和“拒绝”。“保存授权”使用 OpenCode 的 always 回复,仅保存 OpenCode 为本次请求建议的权限范围,并只在当前 OpenCode 会话内生效。任意外部目录默认仍由静态配置禁止;.env、普通 data/logs/ 和其他对话目录保持禁止。真实聊天会话使用 data/conversation-workspaces/<随机目录>/ 作为独立工作目录。普通 rm <文件>rmdir 和非强制递归删除可在沙箱内执行,rm -rf/rm -fr 及等价的递归强制删除形式会在执行前拒绝。

OpenCode 的内置 Bash 由同名自定义工具覆盖。命令经内部鉴权路由进入独立子进程,切换到专用非 root UID 后应用 Landlock 文件规则和 seccomp 网络规则:当前 conversation workspace 可读写,系统/Python/skills 只读,其他应用文件、其他对话和全局 tool-output 不可见;IPv4/IPv6 TCP 与 UDP socket 均被拒绝。启动时会探测 Landlock ABI(要求 ≥4)和 libseccomp,失败时 Agent 直接启动失败,不会回退到未沙箱化 Shell。Shell 环境不包含模型 key、内部 token 或用户 access tokenHOMETMPDIR 和 Python 缓存均位于当前对话目录。

store_render_ref 只会从当前对话绑定的工作目录导入包装格式 JSON;工作区根目录固定为项目内的 ./data/conversation-workspaces,以确保 OpenCode 能继续发现项目配置和工具。文件必须包含 metadatalocation.file_pathdata,且真实路径不能越出当前对话目录;单文件默认上限为 128 MiB,成功导入后只删除这一份源包装文件。升级前已经存在的会话没有独立工作目录,需要新建对话后才能使用该导入能力。

CLI 桥接层对 stdout 设置独立的 128 MiB 硬上限(MAX_CLI_OUTPUT_BYTES);stderr 最多保留 256 KiBMAX_CLI_STDERR_BYTES),超出后截断但不会终止 CLI。MAX_INLINE_RESULT_BYTES(默认 12000 字节)仅决定内联还是落盘:较大结果写入当前对话的 tool-data/ 并返回 data_file.file_path,不会因为超过 12000 字节杀掉 CLI;分析脚本需要文件输入时可由 tjwater_cli(store_result=true) 强制落盘小结果。会话暂存数据不自动删除。OpenCode 自身为其他工具生成的全局 tool-output 仍按 RESULT_REF_TTL_HOURS(默认 7 天)清理,但沙箱命令不能访问该目录。

配置与安全

不要提交 .env.local.envdata/logs/、会话记录、模型输出、访问令牌或 node_modules/。部署凭据、镜像仓库账号和 webhook 地址应放在 Gitea secrets 或部署环境变量中。

发布

Gitea 包工作流位于 .gitea/workflows/package.yml。发布前至少运行:

bun run check

如修改 CLI 或工具调用逻辑,同时运行:

bun run test:cli
S
Description
No description provided
Readme
38 MiB
Languages
TypeScript 91.5%
Python 4.2%
JavaScript 3.3%
Dockerfile 0.7%
Shell 0.3%