feat: send workspace images to QQ chats

Worker results may report image attachments stored inside the workspace;
the runtime validates them (containment, png/jpg magic, size) and
delivers them with the pending event. Assistants gain a send_image
action so users can ask for an image later. QQ uploads via /files with
base64 file_data and sends msg_type 7 rich media, sharing the same
msg_id/msg_seq counter as text replies; non-image adapters flatten
images to text.
This commit is contained in:
zenord
2026-08-19 14:39:18 +08:00
parent 7aa610294c
commit 6b506b8c55
17 changed files with 659 additions and 62 deletions
+5 -1
View File
@@ -97,6 +97,8 @@ gori-agent list
- proposals.json(version 2)保存 Proposal:title/goal/steps、owner chat、发起用户 requesterUserId、状态流(proposed/queued/working/pending/finished)、pending(summary/question?/workspaceDirty?/receivedAt)、finished(finishKind done|cancelled、finishNote?)、worker session 与进程组 PGID/token;同样的 lock 与原子写入纪律。打开 v1 文件时先写 `proposals.json.v1-<timestamp>.bak`(0600)备份再按固定映射迁移。
- `src/core/workspace-scope.ts`
- canonical workspace + 跨实例 Config v3 扫描;相同或父子 workspace 在 doctor/start/runner fail closed。lease 已退役。
- `src/core/workspace-images.ts`
- 出站图片校验与读取:路径必须 resolved 在 canonical workspace 内、png/jpg magic bytes、单张 ≤10MB;发送时按路径重读。
- `src/core/gateway.ts`
- 入站 allowlist、群 mention、命令和回复;不维护普通消息队列或 per-chat task lock,也没有固定时间处理中提醒。
- 每条异步入站使用独立串行 ReplyStream,`replySequence` 从 1 动态递增;发送失败只写安全日志且不阻止任务或后续发送。
@@ -207,7 +209,7 @@ Kimi Code agent `args` 必须严格为 `["acp"]`,不能添加可能绕过 assi
Proposal 状态流:`proposed → queued → working → pending → finished`。`proposed` 必须用户确认才进入 `queued`;`pending` 就是「等用户决定」,不区分 success/failure;只有 `finish` 把 pending 落定为 `finished(done)`,`cancel`(proposed/queued/pending)落定为 `finished(cancelled)`;没有 working、owner 自己没有 pending、全局没有 workspaceDirty pending 时,最早确认的 queued Proposal 才可通过 confirm 或 start_next 开始。
Assistant 每条回复以隐藏 `GORI_ASSISTANT_ACTION_V2` envelope 结尾(`reply` + `actions`),action 仅 `create_proposal`、`confirm`、`adjust_proposal`(仅 proposed/queued)、`follow_up`(pending → working,优先 resume 原 worker native session,失败则带 Proposal 上下文新起 session,用户图片附件随 prompt 给 Worker)、`finish`、`start_next`、`cancel`、`stop`,格式错误只修复一次。Worker 每轮以隐藏 `GORI_WORKER_RESULT_V2` envelope 收尾:仅 `PENDING`(`summary` 必填,可选 `question`、`workspaceDirty`),同样只修复一次。
Assistant 每条回复以隐藏 `GORI_ASSISTANT_ACTION_V2` envelope 结尾(`reply` + `actions`),action 仅 `create_proposal`、`confirm`、`adjust_proposal`(仅 proposed/queued)、`follow_up`(pending → working,优先 resume 原 worker native session,失败则带 Proposal 上下文新起 session,用户图片附件随 prompt 给 Worker)、`finish`、`send_image`(把 Worker 报告过的 workspace 内图片发给用户)、`start_next`、`cancel`、`stop`,格式错误只修复一次。Worker 每轮以隐藏 `GORI_WORKER_RESULT_V2` envelope 收尾:仅 `PENDING`(`summary` 必填,可选 `question`、`workspaceDirty`、`attachments`),同样只修复一次。
确认语义:
@@ -224,6 +226,8 @@ state v3(`acp-sessions.json`)header 保存 `botId` 和 platform,只存 ass
命令 `/help`、`/status`、`/list`、`/confirm`、`/finish`、`/stop`、`/cancel` 旁路 Assistant;全部 owner-only(chat + user):`/confirm` 对 proposed,`/finish` 对 pending,`/stop` 仅对 working(→pending),`/cancel` 对 proposed/queued/pending;`/list` 面板按 pending → working → queued → proposed → 最近 finished 排列。QQ 入站图片附件(image/*,单张 ≤5MB、每条最多 3 张、10 秒下载超时)下载为 base64 经 `IncomingMessage.attachments` 透传;agent 声明 `promptCapabilities.image` 时作为 ACP image content block 发给 Assistant/Worker,否则降级为文本说明;视频/文件附件不下载,仅以 `[视频] <url>` / `[文件] <url>` 文本拼接。
出站图片(QQ):Worker 在 `GORI_WORKER_RESULT_V2` 的 `attachments`(最多 3 个 `{path, mimeType?}`)上报 workspace 内 png/jpg(建议 `.gori-outbox/`),Assistant 也可用 `send_image { path }` 主动发图;runtime 校验 resolved realpath 必须在 canonical workspace 内、png/jpg magic bytes、单张 ≤10MB,违规丢弃并在事件文本说明。发送走 `POST /v2/{groups|users}/{id}/files` 上传(`file_type: 1` + base64 `file_data` + `srv_send_msg: false`)后 `msg_type: 7` + `media.file_info` 发送;群/私聊上传的 file_info 不通用,按目标分别上传。图片与文本共用 Gateway 的 `msg_id` + 递增 `msg_seq` 计数器;落定事件先文案后图;被动窗口过期时照旧记录并下次入站补发,补发按路径重读文件、文件缺失降级为文本说明;上传/发送失败不阻断文本,降级文本说明 + 安全日志。其他平台 adapter 无 `supportsImages` 标记时图片降级为 `[图片] <文件名>` 文本。
## 6. 单平台装配
Server 只构造 `gateway.platform.type` 对应 adapter,只挂载对应 webhook: