Skip to content

feat(oncall): 支持群角色优先私聊回复与失败回群 - #1353

Open
MarcWebber wants to merge 4 commits into
deepcoldy:masterfrom
MarcWebber:codex/group-private-replies
Open

feat(oncall): 支持群角色优先私聊回复与失败回群#1353
MarcWebber wants to merge 4 commits into
deepcoldy:masterfrom
MarcWebber:codex/group-private-replies

Conversation

@MarcWebber

@MarcWebber MarcWebber commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

话题群用于 oncall 时,优先将答案私聊发送给本轮提问人,减少群内打扰;私聊无法投递时,答案自动回到原群话题。配置位于“群 × 机器人角色”,默认关闭,同一机器人在不同群独立生效。

行为与边界

  • 开启后沿用原群话题会话,按已有逐轮发送人记录投递。继续在原话题追问可保留上下文。
  • 私聊发送失败或找不到本轮收件人时,复用原有群回复路径公开发送答案。不会借用上一轮用户作为收件人;没有对应提问轮次的系统回执,以及已识别为机器人发起的轮次,保留群内回复。
  • 可选“群内提示文案”仅在私聊成功后发送,留空不提示。提示发送失败只记录日志,不重发私聊,也不将已送达的答案公开回群。
  • 开启时关闭公开流式卡和思考/工具输出气泡,/card show/cot show 均不能绕过。失败降级沿用原回复路径,不恢复公开过程气泡。
  • 普通私聊、chat-scope 群会话及既有 talk 权限保持原行为。此开关用于减少打扰,不保证答案私密;网络响应丢失时可能出现私聊与群内都收到答案的情况。

实现与影响

复用角色 metadata、角色 API、逐轮发送人与机器人身份记录,以及已有 Lark 发送接口。覆盖 botmux send 正文、附件、语音和 daemon 回复;不增加私聊续接映射或用户白名单。发送失败后继续走原发送链路及其校验,共用 Lark 传输限制保留。

改动位于公共 CLI/daemon 层,未修改 CLI 适配器或 PTY/Tmux 后端。不同 CLI 共用此投递逻辑;自动化验证覆盖群话题降级、普通私聊/chat-scope 不受影响和既有桥接路径。第三方 CLI/API 自行发送消息不受此开关控制。

已 rebase 到 master a2a02ee。总计 18 个文件,+484 / −9,含测试和 1 张约 43 KB 的真实配置截图。

验证

bun run build 通过;以下 14 个测试文件、798 个用例通过

bun run test test/private-reply.test.ts test/session-reply-thread-anchor.test.ts test/role-resolver.test.ts test/cli-send-dispatch.test.ts test/cli-send-hook-context.test.ts test/cli-send-slash.test.ts test/reply-target-fallback.test.ts test/event-dispatcher.test.ts test/session-group-birth-anchor.test.ts test/bridge-fallback-gate.test.ts test/bridge-final-output-retry.test.ts test/cot-message.test.ts test/cli-send-private-reply.test.ts test/scheduler-silent-execute.test.ts

新增真实 CLI 入口集成测试,拦截底层 HTTP,覆盖私聊成功、失败回原话题、旧轮次收件人缺失,以及拒绝 --into / --chat-id 改写目的地。daemon 回归覆盖无轮次/命令回执、发送失败降级;思考气泡回归覆盖强制显示也不公开输出,同时通过 master 新增的静默定时任务气泡及既有静默任务回归。

最新构建已在本地部署并重启,目标机器人在线,群角色开关与提示文案回读正常;更新后的界面已实页检查并截图。此前真人飞书验收已覆盖私聊成功、原话题追问、权限控制及默认关闭;失败回群分支由自动化模拟验证,尚未新增真人失败场景验收。

配置界面

入口:角色管理 → 按群组 → 选择机器人 → 优先私聊回复提问人。下图为手动开启后的实际页面,默认关闭。

群角色的优先私聊开关、失败回群说明与自定义提示

@deepcoldy

Copy link
Copy Markdown
Owner

@MarcWebber 你好,这是自动评审的初步意见(最终以维护者审阅为准)。整体设计很扎实:收件人严格取 replyTargets[turnId].senderOpenId、私聊失败不回退公开发群、语音/附件/桥接兜底(sessionReply)都覆盖到了,测试也全绿。但有一个阻断问题:

🔴 F1(建议修改):私聊模式下 CoT 思考气泡仍然公开发到群话题,与功能目的直接冲突。

  • 流式卡已在 streamingCardDisabled/streamingCardDisabledFor/resume-streaming-card 三处关掉,但 CoT 气泡走的是另一条独立通道:cotEnabled()src/im/lark/cot-message.ts:238)只查 apiOnly / cotForced / thinkingCard(默认 ON)/ noCotChats,没有任何私聊模式判断;worker-pool.ts:12364thinking_update 处理也未加闸。
  • apiCreatereceive_id: ds.chatId + 话题锚点(reply_in_thread)发 message_cot,所以气泡公开出现在原群话题里。而且工具调用命令行/输出(thinkingCardToolResult 默认 ON)会一并公开——oncall 场景下群成员能看到完整工作过程(命令、路径、文件内容片段),"减少群内公开回复"的核心目的被绕过。
  • 我在 PR 代码上做了一个探针测试:私聊模式开启时 cotEnabled(ds) 仍返回 true(探针红);在 cotEnabledapiOnly 判断后加一行 if (privateReplyEnabled(ds.session)) return false;(import 路径 ../../core/private-reply.js)后探针变绿。建议把这行闸放在 cotForced 判断之前,与流式卡的处理方式一致(streamingCardDisabled 里私聊闸也在 ds.streamingCardForced 之前,/card show 无法绕过),并补一条"私聊模式 CoT 不发群"的测试。

🟡 非阻断建议:

  • N1:daemon.ts 的 sessionReply 闸用 turnId ?? ds.session.quoteTargetId 兜底,无 turnId 的会话级公开通知(如 /t 建话题提示、配额卡、grant 限制提示、v3 workflow 通知、/card 命令回执)也会被劫持成发给"最近一个提问人"的私聊,群话题里看不到这些公开回执,且可能发错给上一轮的人。可考虑只在确实存在 per-turn 发送人记录时才私聊(cli.ts 的 legacy 槽规则就是"显式指向该 turn 才采信"),或把这些公开通知调用点显式排除。
  • N2:bot 发送人拦截只覆盖 legacy 单槽(quoteTargetId === turnId && quoteTargetSenderIsBot);per-turn 的 replyTargets[turnId] 条目不携带 isBot,若该轮发送方是机器人(bot-to-bot 派发),答案会私聊给 bot open_id。建议在 turn 条目里带 isBot 一并判定。
  • N3:turn 被裁剪(>32 轮)或重启后 replyTargets 丢失时,私聊按设计 fail-closed(不发群、3 次重试后仅日志),答案会静默丢失;可考虑在群里留一条"本轮私聊投递失败"的公开提示,避免提问人干等。

验证情况:基线已是最新 origin/master(43f228a0),bun run build 通过;PR 所列 11 个测试文件 686 用例本地复跑全绿。

@deepcoldy

Copy link
Copy Markdown
Owner

复审意见:建议修改(1 阻断 + 5 非阻断),暂不合并

独立复核了本 PR 的改动与现有评审评论所列问题,结论一致:F1 阻断成立,需作者修复后复验。

🔴 F1(阻断):私聊模式下 CoT 思考气泡仍公开发群话题 — 确认成立

cotEnabled()src/im/lark/cot-message.ts:238)只查 apiOnly / cotForced / thinkingCard(默认 ON)/ noCotChats,无私聊闸;handleCotThinkingUpdateworker-pool.ts:12364command-handler.ts:1502 调用,apiCreatereceive_id: ds.chatId + 话题锚点发 message_cot,气泡公开落在原群话题。工具调用命令行/输出(thinkingCardToolResult 默认 ON)一并公开,「减少群内公开回复」的核心目的被绕过。

我写了独立探针(不依赖现有评论):构造群话题会话 + writeRoleReplyPrivately(true) 后,privateReplyEnabled=truecotEnabled=true(红);在 cotEnabledapiOnly 判断后、cotForced 之前加 if (privateReplyEnabled(ds.session)) return false;(import ../../core/private-reply.js)后探针转绿。闸放在 cotForced 之前与流式卡处理一致(streamingCardDisabled 里私聊闸也在 streamingCardForced 之前,/card show 无法绕过)。变异后已完全还原,工作树 clean。建议作者修复并补一条「私聊模式 CoT 不发群」的回归测试。

🟡 非阻断

  • N1 确认daemon.ts sessionReply 闸用 turnId ?? ds.session.quoteTargetId 兜底。无 turnId 的公开通知(/t 提示、配额卡、grant 限制、workflow 通知、/card 回执、各斜杠命令响应)在 quoteTargetId 命中 replyTargets 条目时被劫持成发给「最近提问人」的私聊,群内看不到回执;未命中时 throw 经 deliverFinalOutput 重试后静默丢失。建议只在显式带 turnId 的答案路径私聊,公开通知调用点排除。
  • N2 确认ReplyTargetEntry 顶层无 isBot,bot 守卫仅 quoteTargetId === turnId && quoteTargetSenderIsBot,只对最新一轮生效;延迟答复的 bot-to-bot 轮次答案会私聊给 bot open_id。建议 turn 条目带 isBot 一并判定。
  • N3 确认:turn 裁剪(pruneReplyTargets,>32 轮)或 legacy 会话无 replyTargets 时,sendPrivateReply throw → deliverFinalOutput 重试耗尽后仅日志,答案静默丢失,提问人干等。建议群里留一条「本轮私聊投递失败」的公开提示。
  • N4(补充):cli.ts cmdSend 的私聊路径(--into/--chat-id exit 2 闸 + 两处 sendPrivateReply 拦截)无集成测试覆盖,仅靠 sendPrivateReply 单测间接覆盖;daemon sessionReply 路径已有集成测试(session-reply-thread-anchor.test.ts),cmdSend 路径建议补一条。
  • N5(补充):docs commit 新增 docs/assets/private-reply-settings.png,但仓库内无任何文档/README 引用该图,属孤儿资产;建议在对应文档页引用或移除。

验证

  • bun run build 通过。
  • 本地复跑 16 个相关测试文件 356 用例全绿:private-replysession-reply-thread-anchorrole-resolvercot-messagecot-subjectreply-target-fallbackcli-send-dispatchcli-send-slashcli-send-hook-contextcli-send-image-modestreaming-card-pinningdashboard-roles-batchdashboard-streaming-card-pin-togglepin-streaming-card-recovery-wiringstreaming-card-buttons-wiringtrigger-session-reply-mode

影响面

  • 跨会话类型:p2p 与 chat-scope 群会话不受影响(privateReplyEnabled 返回 false);thread/topic 群会话按设计生效。项目协作模式走 thread scope,开启后同样适用——属显式 per-group 开关,管理员知情。
  • 跨 CLI:改动在 daemon/cli 层拦截,未触碰任何 adapters/cli/ 适配器,所有 CLI 等价生效。
  • 跨后端:未改动 PtyBackend/TmuxBackend,无影响。
  • 主要风险是「逐出口打补丁」漏掉出口(F1 即一例);F1 修复后出口覆盖完整(sessionReply、cmdSend 两处、流式卡三处、CoT)。

球作者修 F1 → 复验,目前未合码。

@MarcWebber
MarcWebber force-pushed the codex/group-private-replies branch from d62db4d to a0f42ac Compare September 10, 2026 07:31
@MarcWebber MarcWebber changed the title feat(oncall): 支持群角色私聊回复与自定义提示 feat(oncall): 支持群角色优先私聊回复与失败回群 Sep 10, 2026
@MarcWebber
MarcWebber force-pushed the codex/group-private-replies branch from a0f42ac to 2459c07 Compare September 11, 2026 04:34
@MarcWebber MarcWebber closed this Sep 12, 2026
@deepcoldy deepcoldy reopened this Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants