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
+4 -2
View File
@@ -175,6 +175,8 @@ QQ websocket 示例:
QQ 入站附件:`image/*` 附件会被下载(每条消息最多 3 张、单张超过 5MB 跳过、10 秒下载超时、缺 scheme 的 URL 自动补 `https:`)并转成 base64 随消息传给 Agent;agent 声明 `promptCapabilities.image` 时作为 ACP image content block 发送,否则降级为文本说明。视频/文件等非图片附件不下载,仅以 `[视频] <url>` / `[文件] <url>` 文本拼进消息,交给模型自由使用;纯图片消息使用占位文本「(发来一张图片)」。日志只记录附件类型/大小/数量,不记录 URL 全文或 base64。
QQ 出站图片:Worker/Assistant 报告的 workspace 内图片(png/jpg、≤10MB、最多 3 张)经 `POST /v2/{groups|users}/{id}/files` 上传(`file_type: 1`、`file_data` base64、`srv_send_msg: false`)后以 `msg_type: 7` + `media.file_info` 发送;群上传的 file_info 只能发群、私聊上传的只能发私聊,按目标分别上传。无主动消息权限(40034105),图片与文本一样只能走被动回复窗口,且与文本共用同一 `msg_id` + 递增 `msg_seq` 计数器;Worker 落定事件先发文案再发图,窗口过期时按现有规则记录并下次入站补发,补发时按路径重读文件,文件不存在则降级为文本说明。图片上传/发送失败不阻断文本,降级为文本说明 + 安全日志。其他平台 adapter 不支持图片时把图片降级为 `[图片] <文件名>` 文本行。
### ACP 生命周期
默认 `runtime.acp.promptTimeoutMs` 是 `14400000`(4 小时),适合长构建/部署任务。其他默认值:
@@ -192,9 +194,9 @@ QQ 入站附件:`image/*` 附件会被下载(每条消息最多 3 张、单
运行链路是三层:
- **Assistant**:每个 conversation(chat + user)一个无工具 ACP 会话,只与用户对话;同群不同用户的会话互相隔离。它把用户意图整理成 Proposal(title、goal、steps),并解释 Worker 的反馈。Assistant 每条回复必须以隐藏 `GORI_ASSISTANT_ACTION_V2` envelope 结尾(`reply` + `actions`),action 只有 `create_proposal`、`confirm`、`adjust_proposal`、`follow_up`、`finish`、`start_next`、`cancel`、`stop`;格式错误只修复一次。
- **Assistant**:每个 conversation(chat + user)一个无工具 ACP 会话,只与用户对话;同群不同用户的会话互相隔离。它把用户意图整理成 Proposal(title、goal、steps),并解释 Worker 的反馈。Assistant 每条回复必须以隐藏 `GORI_ASSISTANT_ACTION_V2` envelope 结尾(`reply` + `actions`),action 只有 `create_proposal`、`confirm`、`adjust_proposal`、`follow_up`、`finish`、`send_image`、`start_next`、`cancel`、`stop`;格式错误只修复一次。`send_image { path }` 把 Worker 报告过的 workspace 内图片发给用户,与 Worker 附件同样的路径/类型/大小校验。
- **Proposal**:一份工作单,owner 是 chat + 发起用户;只有发起人本人可以 confirm/adjust/follow_up/finish/stop/cancel/list 它,`start_next` 也只启动发起人自己的 queued Proposal。状态流为 `proposed → queued → working → pending → finished`。`proposed` 只有用户确认后才进入 `queued`;`pending` 就是「等用户决定」,不再区分 success/failure;只有 `finish` 把 pending 落定为 `finished(done)`,`cancel` 落定为 `finished(cancelled)`。
- **Worker**:同一时刻全实例只有一个,在 `bot.workspace` 用 `bot.permissions` policy 执行一个已确认 Proposal。每轮必须以隐藏 `GORI_WORKER_RESULT_V2` envelope 收尾:`PENDING`(`summary` 必填,可带 `question`、`workspaceDirty`),不区分成功/失败,只把结果交给用户。
- **Worker**:同一时刻全实例只有一个,在 `bot.workspace` 用 `bot.permissions` policy 执行一个已确认 Proposal。每轮必须以隐藏 `GORI_WORKER_RESULT_V2` envelope 收尾:`PENDING`(`summary` 必填,可带 `question`、`workspaceDirty`),不区分成功/失败,只把结果交给用户。Worker 给用户看的图片(png/jpg)必须保存在 workspace 内(建议 `.gori-outbox/`),并通过 `attachments: [{ path, mimeType? }]`(最多 3 个)上报;runtime 校验路径必须在 workspace 内、magic bytes 为 png/jpg、单张 ≤10MB,违规的丢弃并在事件文本里说明。
确认语义是刻意的: