Refactor runtime around isolated bot instances

This commit is contained in:
zenord
2026-08-16 23:45:09 +08:00
parent 914579a94c
commit f9fb4ef776
51 changed files with 2355 additions and 1459 deletions
+194 -286
View File
@@ -1,348 +1,256 @@
# gori-agent
通过 QQ 等 IM 调用 ACP Coding Agent。当前默认后端是 Kimi Code:
`gori-agent` 是一个 Node.js 20+ / TypeScript 网关:从一个 IM 平台接收消息,通过官方 Agent Client Protocol(ACP)驱动一个 Coding Agent。
```text
用户 → QQ → gori-agent Gateway → ACP → Kimi Code → 工具/脚本 → QQ
用户 → 单个平台 Adapter → Gateway → ACP Session Manager → 单个 ACP Agent
```
## 五分钟上手
## Config v3 实例模型
### 1. 准备 Kimi Code
Config v3 只支持以下边界:
确认 Kimi Code 已登录且 ACP 可用:
- 一份运行配置对应一个固定 Bot 身份。
- 一个 Bot 只有一个 workspace、persona、ACP agent、skill 列表和 permission policy。
- 一个实例只绑定一个平台;多平台使用多个实例。
- 不再有 `roles[]`、`defaultRole`、`backends[]`、动态 `/role` 切换或 Config v1/v2 迁移。
- `configVersion` 非 `3` 会明确失败,旧 state v1 也不会自动改写。
```bash
kimi --version
kimi doctor
kimi acp --help
```
ACP 使用 `~/.kimi-code/config.toml` 中的 `default_model`。模型切换应先在 Kimi Code 中完成,再重启 gori-agent;已有聊天要使用新模型时,在 QQ 中发送 `/new` 创建新 session。
### 2. 构建并迁移配置
```bash
cd /home/ubuntu/gori-space/gori-agent
npm install
npm run build
./gori-agent.sh setup --config ./config.json
```
建议向导选择:
一台机器上的实例统一位于:
```text
Assistant workspace: /home/ubuntu/gori-space/gori-agent
Include ops role with the existing gori-update skill: yes
Write config: yes
${GORI_AGENT_ROOT:-$HOME/.gori-agent}/instances/<bot-id>/
├── config.json
├── logs/gori-agent.log
└── state/
├── acp-sessions.json
└── gori-agent.pid
```
迁移会把旧的 `kimi -p` 改为 `kimi acp`,并完整保留已有 QQ appId、secret 和连接模式。
实例目录就是该进程的 `GORI_AGENT_HOME`。实例目录、`state/`、`logs/` 使用 `0700`,配置与状态文件使用 `0600`。
### 3. 检查配置
```bash
./gori-agent.sh discover-backends --config ./config.json
./gori-agent.sh doctor --config ./config.json
```
预期看到 Kimi ACP `ready`,并支持 session `load/resume`。
### 4. 前台启动并从 QQ 验收
```bash
./gori-agent.sh start --config ./config.json
```
启动日志应包含:
```text
QQ websocket connected
QQ websocket ready
```
在 QQ 群中 @bot 并发送:
```text
/status
/roles
/role assistant
只读回答当前工作目录,不要修改文件
```
运维角色先做只读测试:
```text
/role ops
/status
请只读取并概述 gori-update skill,不要 pull、构建或部署
```
常用聊天命令:
```text
/roles 列出角色
/role <id> 切换角色
/status 查看当前 role/workspace/session 状态
/cancel 取消正在执行的任务
/new 为当前角色创建新的 Agent session
```
同一个 `平台 + chat + role` 会持续使用同一个 Kimi session;Gateway 或 ACP 子进程重启后会恢复。不同 role 的 session 相互独立。
### 5. 后台运行
前台验收通过后:
```bash
export GORI_AGENT_HOME=/home/ubuntu/.gori-agent
./gori-agent.sh start-daemon --config ./config.json
./gori-agent.sh status --config ./config.json
./gori-agent.sh logs
./gori-agent.sh stop
```
运行状态和日志位于:
```text
/home/ubuntu/.gori-agent/state/
/home/ubuntu/.gori-agent/logs/gori-agent.log
```
`config.json` 包含 IM secret,不要提交,建议执行:
```bash
chmod 600 config.json
```
## 多机器与多 QQ Bot
仓库只维护一个无密钥模板:`config.example.json`。在每台机器、每个 Bot 实例中复制后单独修改:
```bash
cp config.example.json config.json
chmod 600 config.json
```
每个进程只能登录一个 QQ Bot。为每个实例配置唯一的 QQ `appId`、`clientSecret`、`botNames` 和 `server.port`,并使用不同的 `GORI_AGENT_HOME`,避免日志、PID 和 ACP session 状态相互覆盖:
```bash
GORI_AGENT_HOME=/home/USER/.gori-agent-ROLE_ID \
./gori-agent.sh start-daemon --config ./config.json
```
建议每个 Bot 的 `roles[]` 只保留它自己的一个 role,同时修改 `ROLE_ID`、`workspace`、`persona` 和 `policy`。需要 skill 时,再向顶层 `skills[]` 添加对应 SKILL.md,并在 role 的 `skills` 中引用。各机器的 `config.json` 不要提交到仓库。
## 配置角色
角色定义在 `config.json` 的 `roles[]` 中:
```json
{
"id": "ops",
"backend": "kimi",
"workspace": "/home/ubuntu/gori-space",
"persona": "你是 Gori 团队运维角色,严格遵循 gori-update skill。",
"skills": ["gori-update"],
"policy": {
"permissionMode": "allowlist",
"allowedTools": ["read", "grep", "glob", "bash"],
"allowedCommandPatterns": ["允许的命令正则"]
}
}
```
- `backend`:ACP 后端 ID,默认 `kimi`。
- `workspace`:该角色的工作目录,必须是绝对路径。
- `persona`:角色职责。
- `skills`:引用顶层 `skills[]` 中的 SKILL.md。
- `permissionMode`:`deny` 全拒绝;`allowlist` 按工具和命令规则放行;`auto` 全放行(高风险)。
修改 persona、workspace、skill 内容或 policy 后,角色 fingerprint 会变化;下一条消息会创建新的 native session,避免沿用旧角色上下文。
## 配置 QQ
`config.json` 的最小 QQ websocket 配置:
```json
{
"policy": {
"allowedUsers": [],
"allowedChats": [],
"requireMentionInGroup": true
},
"platforms": {
"qq": {
"enabled": true,
"connectionMode": "websocket",
"appId": "你的 AppID",
"clientSecret": "你的 ClientSecret",
"botSecret": "",
"verifySignature": true,
"botNames": ["你的机器人名称"],
"intents": 33554432,
"shard": [0, 1]
}
}
}
```
初次测试可让 `allowedUsers` 和 `allowedChats` 为空;正式运维 bot 应限制到指定用户或群。群聊 chat ID 的格式为 `group:<group_openid>`。
---
## Detailed reference
Multi-IM gateway that drives coding agents through the official Agent Client Protocol (ACP):
```text
IM Adapter -> Gateway -> Role/Session Manager -> ACP Client -> kimi acp
```
The MVP backend is Kimi Code ACP. Gateway code contains no Kimi CLI prompt fallback and no Pi/Codex private protocol. Future agents must enter through an ACP adapter such as `codex-acp` or a Pi ACP adapter.
## Requirements and quick start
Requires Node.js 20+ and an authenticated Kimi Code installation.
## 构建与安装
```bash
npm install
npm run build
./install.sh
gori-agent setup
gori-agent doctor --config ./config.json
gori-agent start --config ./config.json
```
`setup` probes ACP backends, creates an assistant role, optionally creates the `ops` role, preserves all existing platform settings, prints a migration summary, and only writes after confirmation. A v1 config is accepted at runtime only when its default agent is Kimi; other CLI agents require an explicit ACP backend.
安装器只安装一个 launcher;不会为每个 Bot 复制源码或 `dist/`。默认 launcher 位于 `$HOME/.gori-agent/bin/gori-agent`。
Convenience wrapper commands:
确认 ACP agent 可用:
```bash
./gori-agent.sh start
./gori-agent.sh start-daemon
./gori-agent.sh status
./gori-agent.sh logs
./gori-agent.sh stop
kimi --version
kimi doctor
kimi acp --help
gori-agent discover-backends
```
`stop` sends SIGTERM and waits up to 10 seconds for graceful shutdown. For production, use systemd with `Restart=always` and `KillMode=control-group` so an unexpected gateway crash also cleans up ACP children.
模型和 provider 由 ACP agent 自身配置,例如 Kimi Code 使用 `~/.kimi-code/config.toml`。gori-agent 不复制模型凭据。
## CLI
## 实例管理
```bash
gori-agent setup [--config path]
gori-agent discover-backends [--json]
gori-agent start [--config path]
gori-agent status [--config path]
gori-agent doctor [--config path]
gori-agent print feishu [--config path]
gori-agent instance init <bot-id>
gori-agent instance doctor <bot-id>
gori-agent instance start <bot-id>
gori-agent instance status <bot-id>
gori-agent instance logs <bot-id>
gori-agent instance restart <bot-id>
gori-agent instance stop <bot-id>
gori-agent instance list
```
`discover-agents` remains a deprecated alias for `discover-backends`. Discovery does not send a prompt. Kimi is ready when `kimi acp` is available; Codex and Pi report `needs-adapter` unless their ACP adapter executable exists.
- `init` 交互生成 Config v3,本身不启动 Bot;它会询问并校验 `gateway.server.host/port`,扫描统一实例目录中其他 Config v3 的声明端口,并在默认端口冲突时建议下一个未占用端口。已有配置通过 `setup` 重跑时可保留或修改 host/port,不会静默改端口。
- 平台 secret/token 使用不回显输入,空 generic webhook/weixin secret 会随机生成。
- `start` 会检查配置存在、目录名匹配 `bot.id`、目录/配置权限、模板占位符、workspace、agent executable、PID identity 和端口,并等待 `/health` 返回匹配的 Bot/platform identity;任一不满足即 fail closed。
- `status` 核对 PID 对应的完整 `start --config` 身份以及 `/health` 返回的 Bot/platform identity。
- 每个实例必须使用不同的 `gateway.server.port` 和平台凭据;setup 的声明冲突提示不替代 `start` 的实际端口检查。
- `stop` 发送 `SIGTERM` 并等待最多 10 秒,不会自动 `SIGKILL`。
## Roles, sessions, and commands
可用 `GORI_AGENT_ROOT` 改变统一根目录:
A conversation binding is keyed by `platform + chatId + role`. Each binding stores the ACP backend, native session ID, workspace, and role fingerprint. The selected role and bindings survive gateway restart.
```bash
GORI_AGENT_ROOT=/srv/gori-agent gori-agent instance list
```
- A new session receives a hidden bootstrap prompt containing persona, workspace, skill content, and policy. The binding is saved only after bootstrap succeeds.
- A role persona, workspace, policy, or skill-content change changes its fingerprint and causes a new native session.
- Idle workers are stopped and later cold-resumed with `session/resume` (or `session/load` when resume is unavailable).
- Failed prompts are never replayed automatically.
- `/new` cancels the current turn, unbinds the current chat/role, and creates a native session on the next prompt. It does not delete Kimi history.
- `/cancel` bypasses the per-chat lock and sends ACP `session/cancel`; an unresponsive worker is force-recycled after the grace period.
## `config.example.json` 的定位
Chat commands:
`config.example.json` 是仓库内唯一可提交、无密钥的配置说明,不是运行配置。它可被 Config v3 schema 解析,但保留 `BOT_ID`、`QQ_APP_ID` 等明显占位符。
- `/help`
- `/roles`
- `/role <id>`
- `/status`
- `/cancel`
- `/new`
- `/agents` and `/agent <id>` (deprecated aliases)
`start`、`status`、`doctor`、`print` 找不到本地配置时会失败,绝不会回退到 example。只有 `setup` 和 `instance init` 会读取 example 作为初始化种子。
Messages in one chat are serialized; different chats run concurrently.
底层调试入口仍可使用:
## Configuration v2
```bash
gori-agent setup --config /absolute/path/config.json
gori-agent start --config /absolute/path/config.json
gori-agent status --config /absolute/path/config.json
gori-agent doctor --config /absolute/path/config.json
gori-agent print feishu --config /absolute/path/config.json
```
See `config.example.json`. Important sections:
`writeConfigFile()` 使用同目录临时文件、`fsync` 和原子 rename,并强制最终文件为 `0600`。
- `backends[]`: ACP spawn command and arguments. The default is `/home/ubuntu/.kimi-code/bin/kimi acp`.
- `roles[]`: backend, absolute workspace, persona, skills, and permission policy.
- `skills[]`: readable skill files with a byte limit. Skill contents participate in the role fingerprint.
- `acp.stateFile`: defaults to `$GORI_AGENT_HOME/state/acp-sessions.json`.
- `acp.initializeTimeoutMs`, `promptTimeoutMs`, `cancelGraceMs`: request lifecycle limits.
- `acp.idleTimeoutMs`, `sweepIntervalMs`, `maxProcesses`: worker pool limits.
- `policy.allowedUsers`, `allowedChats`, `requireMentionInGroup`: inbound IM policy.
- `platforms`: Feishu, WeCom, QQ, generic webhook, and Weixin settings. Migration preserves this object, including credentials.
## Config v3
State writes use a serial promise queue, temporary file, fsync, and atomic rename. A single-instance lock protects the state file. Corrupt state is preserved and startup fails explicitly instead of overwriting it.
完整说明见 `config.example.json`。顶层只有:
## Permission policy
```text
configVersion 固定为 3
bot Bot、workspace、persona、agent、skills、permissions
gateway HTTP server、入站 policy、唯一 platform
runtime.acp ACP state、timeout 和 worker pool
```
ACP permission requests are decided by role policy, not by persona:
- `deny`: reject every permission request.
- `allowlist`: require both an allowed tool name and, for bash/terminal, a matching raw command pattern. If the ACP request lacks enough command detail, it is denied.
- `auto`: approve everything; `doctor` prints a high-risk warning.
The example `ops` role loads the existing read-only skill file at `/home/ubuntu/gori-space/gori-deploy/.kimi-code/skills/gori-update/SKILL.md`. Its allowlist covers selected `git`, build/deploy entry scripts, and read-only Docker status/log commands. `sudo`, force push, and arbitrary deletion are not allowed. Script-internal operations cannot be inspected by ACP once an explicitly allowed deployment script starts, so script paths must remain trusted.
Kimi 0.36.1 does not advertise a generic model config option during ACP initialize; this MVP uses the model configured as Kimi's default and `doctor` reports that limitation.
## Platform behavior
| Platform | Inbound | Outbound | Notes |
| --- | --- | --- | --- |
| Feishu/Lark | Implemented | Implemented | `im.message.receive_v1` and message reply API. |
| WeCom | 501 scaffold | Implemented | Inbound verification/encryption is not implemented. |
| Weixin | External webhook scaffold | Synchronous | Native personal WeChat is not included. |
| QQ | Implemented | Implemented | WebSocket gateway and HTTP callback modes. |
| Generic webhook | Implemented | Synchronous JSON | HMAC-SHA256 signed JSON. |
QQ WebSocket mode uses intent `33554432` for `GROUP_AT_MESSAGE_CREATE` and `C2C_MESSAGE_CREATE`. The adapter supports top-level and nested `author.user_openid` fields, preserves callback ACK `{ "op": 12 }` even if ACP work later fails, and logs receive/send routing. No external QQ message is sent by the automated tests.
Endpoints:
- `GET /health` — aggregate ACP counters only; native session IDs are not exposed.
- `GET /platforms`
- `POST /webhook/feishu`
- `POST /webhook/wecom`
- `POST /webhook/qq`
- `POST /webhook/generic`
- `POST /webhook/weixin`
Generic webhook payload:
### Bot 与 ACP agent
```json
{
"chat_id": "demo-chat",
"user_id": "demo-user",
"text": "/status",
"message_id": "optional",
"is_group": false,
"mentions_bot": true
"bot": {
"id": "my-bot",
"workspace": "/absolute/workspace",
"persona": "Describe responsibilities, boundaries, and confirmation points.",
"agent": {
"id": "kimi",
"command": "/home/USER/.kimi-code/bin/kimi",
"args": ["acp"],
"env": {}
},
"skills": [],
"permissions": {
"mode": "deny",
"allowedTools": [],
"allowedCommandPatterns": []
}
}
}
```
When `platforms.webhook.secret` is set, provide `X-Gori-Signature: sha256=<HMAC-SHA256 hex>` over the exact JSON body.
- `bot.id` 只允许小写字母、数字和连字符,最长 63 字符。
- `workspace` 与每个 skill `file` 必须是绝对路径。
- `bot.skills[]` 直接声明 `{ id, file, maxBytes }`;ID 必须唯一。
- agent 使用 `shell: false` 在 `bot.workspace` 启动。
- agent env 可能包含 secret,不能进入可提交模板、日志或 diff。
## Lifecycle and diagnostics
### Permission policy
Shutdown order is QQ WebSocket stop, active ACP cancellation, worker termination, state flush/unlock. SIGINT and SIGTERM share the same idempotent shutdown path. Health statistics include active workers, in-flight turns, worker crashes, persisted bindings, and locked chats.
ACP permission request 由 `bot.permissions` 决策,persona 不是安全边界:
`doctor` checks role/workspace/skill validity, ACP initialize and restore capabilities, state directory access, permission warnings, configured platforms, and QQ requirements. It performs no prompt and no external QQ test.
- `deny`:拒绝所有 permission request,默认值。
- `allowlist`:`toolCall.name` 必须与 `allowedTools` 精确匹配;bash/terminal 还必须由至少一个 `allowedCommandPatterns` 正则完整覆盖整条 raw command。名称或 command 缺失时拒绝。
- `auto`:自动允许,风险等价于 yolo;`doctor` 会告警。
## Development
`allowedTools` 只控制 ACP 审批,不会注册或创造工具。skills、agent 与 permission policy 是三个独立概念。
### 单平台配置
`gateway.platform` 是按 `type` 区分的 union,只能选择一个:
- `qq`:WebSocket 或 webhook。
- `feishu`:`/webhook/feishu`。
- `wecom`:`/webhook/wecom`;入站仍是 501 scaffold。
- `webhook`:`/webhook/generic`,同步 JSON + HMAC-SHA256。
- `weixin`:`/webhook/weixin`,外部 bridge scaffold。
对象存在即启用,不再使用 `enabled`。Server 只构造所选 adapter,也只挂载需要的 webhook route。QQ WebSocket 仅在 `type=qq` 且 `connectionMode=websocket` 时启动,此模式不挂载 `/webhook/qq`。
QQ websocket 示例:
```json
{
"type": "qq",
"connectionMode": "websocket",
"appId": "QQ_APP_ID",
"clientSecret": "QQ_CLIENT_SECRET",
"botSecret": "",
"verifySignature": true,
"botNames": ["QQ_BOT_NAME"],
"intents": 33554432,
"shard": [0, 1]
}
```
正式 Bot 应通过 `gateway.policy.allowedUsers` / `allowedChats` 限制入口。QQ 群 chat ID 为 `group:<group_openid>`。
### ACP 生命周期
默认 `runtime.acp.promptTimeoutMs` 是 `7200000`(2 小时),适合长构建/部署任务。其他默认值:
- initialize:10 秒
- cancel grace:5 秒
- idle worker:30 分钟
- sweep:60 秒
- max processes:8
`stateFile` 为空时使用当前实例的 `$GORI_AGENT_HOME/state/acp-sessions.json`。
## Session 与 state v2
每个绑定由当前实例固定的 `platform + chatId` 唯一确定。state v2 header 保存 `botId` 与 platform type;打开 state 时身份不匹配会拒绝启动,避免错误复用另一个实例目录。
Binding 保存:
- chat key
- agent ID
- native ACP session ID
- workspace
- Bot fingerprint
- 创建与更新时间
Bot fingerprint 包含 Bot ID、workspace、persona、agent、permissions、skill 路径/内容 hash 和 bootstrap schema version。任一语义变化后,下一条消息创建新 native session,不恢复旧身份上下文。
首次 prompt 会创建 native session,发送隐藏 bootstrap,bootstrap 成功后才保存 binding。worker 空闲回收后优先 `session/resume`,否则回退 `session/load`。失败 prompt 不会自动重放,因为工具操作可能已有副作用。
旧 state v1 会被原样保留并明确拒绝;使用新的实例目录开始 state v2,不要手工改写运行中的 state。
## IM 命令
```text
/help
/status
/cancel
/new
```
- `/status` 显示固定 Bot、agent、workspace 和 session persisted/running 状态。
- `/cancel` 绕过 per-chat lock,发送 ACP cancel。
- `/new` 清除当前 chat binding;下一条普通消息创建新 native session。
- `/roles`、`/role`、`/agents`、`/agent` 会返回固定 retired 提示,不会转发给 ACP。
同一 chat 串行执行,不同 chat 可并发。
## HTTP 端点
- `GET /health`:`configVersion`、`botId`、platform 和 aggregate ACP counters,不暴露凭据/native session ID。
- `GET /platforms`:当前单一 Bot/platform 概览。
- `POST /webhook/<selected-platform>`:只存在所选平台路由;generic 使用 `/webhook/generic`。
## Doctor 与安全纪律
`instance doctor` / `doctor --config` 检查:
- Config v3 与 example placeholder。
- 配置权限 `0600`。
- Bot ID、workspace、skills 与 permissions。
- ACP initialize 与 session restore capability。
- state 目录可读写。
- 单平台必需字段,但不打印 credential 值。
自动测试不会启动真实 Bot、发送平台消息、pull、部署或访问远程机器。真实 `config.json`、备份、state 和日志不得提交。
## 开发验收
```bash
npm run typecheck
npm test
npm run build
git diff --check
bash -n install.sh gori-agent.sh bin/gori-agent
```
Tests use Node `node:test` through `tsx`. The fake ACP subprocess covers initialize/new/resume/prompt/cancel, persistence, idle cold resume, Gateway commands, and QQ normalization/ACK behavior.
测试使用 Node `node:test` + `tsx` 和本地 fake ACP 子进程,覆盖 Config v3、permission、bootstrap、timeout/cancel、冷恢复、fingerprint mismatch、state v2 identity、原子配置/state 写入、Gateway retired commands、QQ normalization 和单平台 route。
Legacy `src/agents/*` source remains only for compatibility/reference and is not imported by Gateway or Server. There is no runtime `CliAgent` fallback.
`src/agents/*` 与 `src/core/session-store.ts` 仅为 legacy compatibility/reference,不在当前运行链路。运行时没有单轮 CLI fallback。