Skip to content

feat(lark): 支持可信 OnCall 拉群后自动开放当前群对话权限 - #1034

Open
litongde1996 wants to merge 1 commit into
deepcoldy:masterfrom
litongde1996:feat/lark-auto-oncall-chat-access
Open

feat(lark): 支持可信 OnCall 拉群后自动开放当前群对话权限#1034
litongde1996 wants to merge 1 commit into
deepcoldy:masterfrom
litongde1996:feat/lark-auto-oncall-chat-access

Conversation

@litongde1996

Copy link
Copy Markdown
Contributor

改动说明

  • 扩展 BotConfig,新增可信拉群 operator 与自动群聊授权配置,并在解析时 trim、去空、去重。
  • 新增自动授权 Store,使用 rmwBotEntry() 原子持久化 autoOncallChats,同时热更新内存配置。
  • 接入 im.chat.member.bot.added_v1:仅可信 operator 拉群时为当前 bot × 当前 chat 开放对话。
  • 接入 im.chat.member.bot.deleted_v1:bot 退群时仅清理自动来源授权,保留人工 allowedChatGroups
  • 更新 canTalk 白名单判断,并补充 Store 与事件分发测试。

安全边界

  • operator 必须按当前应用视角的 open_id 精确匹配。
  • 授权严格限定在当前 bot × 当前 chat,其他 bot 和其他群不会继承。
  • 自动授权只开放 canTalk,不开放 canOperate/restart/grant/cd 等管理能力仍受原权限控制。

验证

  • pnpm vitest run --project unit test/auto-oncall-store.test.ts test/event-dispatcher.test.ts
    • 2 个测试文件、183 条测试全部通过。
  • pnpm build 通过。
  • 已确认构建产物包含 autoOncallChats / auto-oncall 逻辑。

本 PR 不包含真实 ByteOncall open_id

@deepcoldy

Copy link
Copy Markdown
Owner

感谢投入!这个方向(可信 OnCall 拉群 → 自动开放本群对话,且只给 talk 不给 operate)是合理的,落盘走 rmwBotEntry 原子写、add/remove 幂等、bot.deleted 只清自动来源保留人工 allowedChatGroups 这几点都做得挺干净。下面几条是我实测出来的问题,想请你看一下:

1)(建议合前修)hasConfiguredAllowlist 加入 autoOncallChats 会把「未配白名单」的 bot 整体翻成限制态

hasConfiguredAllowlist 是「本 bot 有没有白名单」的总闸,canTalkcanOperate 都用它。实测(直接调用 canTalk/canOperate):

  • 授权前,一个没配 allowedUsers/allowedChatGroups/globalGrants 的 bot 是 open 模式:任意群 canTalk=true, canOperate=truereason='open'
  • 写入一条 autoOncallChats: ['oc_oncall'] 之后:canTalk('oc_unrelated')=falsecanOperate('oc_unrelated')=false并且 canOperate('oc_oncall') 也是 false

也就是说:一次自动授权会让这个 bot 在所有其它群失去对话能力,且包括 owner 在内没有人能再跑 /restart/cd 等操作(因为 operate 只认 allowedUsers,而它是空的)。原有的 allowedChatGroups 不会有这个问题,因为它只能由 owner 手工//grant 配出来,配置时 setup 与 services/allowed-chat-groups.ts 都会强制/告警「必须有 owner」;而 autoOncallChats 是事件自动写入的,绕过了这层保护。

顺带两处同一判据的第二实现没跟上,建议一起对齐:

  • src/im/lark/card-handler.ts 里手写的 hasAllowlist fallback(目前只认 allowedChatGroups/globalGrants/p2pOpen
  • src/services/allowed-chat-groups.ts 的「配了群授权却没 owner」启动告警(目前只看 allowedChatGroups)——恰好是上面这个最危险场景,现在不会告警

2)(建议合前修)owner 没有办法撤销自动授权

/grant/revoke 的整群撤销走的是 removeAllowedChatGroup,只删 allowedChatGroups。实测:allowedChatGroups 已为空、autoOncallChats 里还有该 chat 时,非授权用户的 canTalk 仍然是 true。目前唯一的撤销途径是把 bot 踢出群,而踢出后又只有可信 operator 能重新拉进来。建议让 /revoke 同时清 autoOncallChats(或在 dashboard 暴露该字段)。

3) im.chat.member.bot.deleted_v1 与 master 上已有的 handler 重复

你写这个 PR 时的 base(c984c3854)确实还没有这个事件,是 master 后来加的:master 现在用它做 invalidateChatStats(群成员数缓存失效)。我做了一次 trial merge,解掉 import 冲突后,同一个对象字面量里 'im.chat.member.bot.deleted_v1' 出现了 2 次 —— 好消息是 tsc 会报 TS1117(我单独验证过),所以不会静默丢失、rebase 时你一定会撞到;处理时请把两件事合并进同一个 handler,而不是二选一。

4) 与既有机制的重叠(不阻塞,想听你的想法)

oncallChats / defaultOncall 已经实现了「整群成员都能对话」,services/grant-store.ts 里的 addAllowedChatGroup/removeAllowedChatGroup 与新的 auto-oncall-store.ts 也几乎逐行同构(只换了字段名)。这样群级 talk 授权就有了第 4 套并行实现,判据分散在 4 个地方。是否可以考虑复用 allowedChatGroups + 一个「来源标记」,或者复用 oncall 那条路?如果有必须独立字段的原因,也麻烦在 PR 描述里说明一下,方便后续维护。

5) 小项

  • 落后 master 约 1324 个 commit,需要 rebase。merge-tree 只报 2 个冲突文件,但实际有 4 个冲突区(其中一处约 180 行),rebase 时留意一下。
  • master 已在 chore(build): 包管理器从 pnpm 换成 bun #1033 把包管理器换成 bun(packageManager: bun@1.4.0),PR 描述和 test/auto-oncall-store.test.ts 头部注释里的 pnpm ... 可以更新成 bun
  • autoOncallOperatorOpenIds / autoOncallChats 目前在 dashboard 和 README 里完全不可见,owner 无从感知这个授权存在,建议至少补一段文档。

关于测试:我用反向变异验了一下现有覆盖——把 isAutoOncallOperator 焊成 return true 会红(✅ operator 判据有覆盖),把 hasAllowedChatGroupautoOncallChats 分支删掉也会红(✅);但删掉 hasConfiguredAllowlist 里新增的那行 autoOncallChats,183 条测试全绿 —— 也就是第 1 条缺陷所在的那行目前零覆盖。另外 test/event-dispatcher.test.tsvi.mock 掉了 auto-oncall-store,那 4 条新用例实际断言的是 mock 的行为,建议补一条不 mock store 的用例把「open 模式 bot 收到自动授权后,owner 仍能 operate」钉住。

基线我这边是干净的:干净 HOME 下 test/auto-oncall-store.test.ts + test/event-dispatcher.test.ts 183/183 通过。

以上是自动评审的初步意见,可能有误判,也不代表最终结论 —— 最终以维护者审阅为准。如果哪条你觉得判断不对,欢迎直接反驳。

@deepcoldy

Copy link
Copy Markdown
Owner

补充几条(交叉复审后的增量,同样都是实测复现),以及对上一条评论里 的一个更好的修法:

① 的推荐修法:把 autoOncallChatshasConfiguredAllowlist 里拿掉(一行)

我上一条只报了问题没给方案,这里补上。你把它加进总闸的动机应该是怕 fail-open,但这一行其实是不需要的,而且正是它造成了那个副作用:

  • 受限 bot(已配 owner):总闸本来就因 allowedUsers 非空而为 true,加不加这行行为完全一样
  • open 模式 bot:拿掉后回到 PR 前语义(本来就是全开放),不会因为一次自动授权就把整个 bot 的权限模式翻面

我把这行删掉后跑了四象限验证:

场景 结果
open bot:授权群 / 其它群 talk+operate 全部 true(= PR 前语义,无副作用)
受限 bot:陌生人在授权群 talk true功能仍然有效
受限 bot:陌生人在其它群 talk false ✅ 边界不变
受限 bot:陌生人 operate false ✅ 不授 operate
受限 bot:owner operate true ✅ 不再被锁死

即原则是「自动事件不应翻转全局安全姿态」。这一行改动就能修掉 ①,且不影响这个特性本身要达成的效果。

②a 新增:/revoke 会给出「假成功」反馈(比「撤不掉」更容易误导)

当一个群同时在 allowedChatGroupsautoOncallChats 里时,owner 跑 /revokeremoveAllowedChatGroup 确实删掉了,于是回复 cmd.revoke.chat_done(撤销成功)—— 但 talk 仍然经 autoOncallChats 放行。实测:撤销前 canTalk=true,收到「撤销成功」之后 canTalk 仍然是 true。owner 拿到的是明确的成功信号,却什么都没撤掉,这比「没有撤销入口」更危险。

②b 新增:/revoke @某人 对自动授权群里的人也无效

自动授权群里的 talk 来自「群成员身份」而不是 chatGrants,所以针对个人的撤销在这类群里完全没有效果 —— 想撤掉单个人,目前也只能踢 bot 或手改 bots.json

②c 新增(小,但方向偏不安全侧):removeAutoOncallChat 磁盘写失败时内存侧不回收

rmwBotEntry 返回 !ok(例如 bot_not_in_config)时函数提前 return,内存里的 bot.config.autoOncallChats 不做清理 → 授权继续生效到重启为止。

这里做个校准,避免让你多背责任:同样的形状在 master 的 grant-store.ts 里也存在if (!r.ok) return r; 后不动内存),所以不是本 PR 引入的模式,我不把它当阻断项。不过 remove 语义下这个方向是偏 fail-open 的(删除失败 = 授权保留 = 陌生人继续能说话),而 grant-store 那几处是 owner 手动触发、失败会当场看到文案;你这条是事件驱动、没有人在看回执,所以同样的形状风险更高一点。如果顺手,建议在 !r.ok 分支也清一次内存(或至少 logger.warn 到能被发现)。

另外上一条评论里 ③(bot.deleted_v1 重复注册)我们又用更忠实的形态复核了一次 —— 把你的 handler 体放进 master 版的同一个 handlers 对象字面量、用仓库真实 tsconfig 跑 tsc --noEmit,确认报 error TS1117;而 buildtsc && ...、CI 每个 PR 都跑 build,所以它一定会在 CI 挂掉、不会静默丢失 master 的 invalidateChatStats。这条不升级严重性,只是提醒你 rebase 时要把两件事合进同一个 handler,而不是二选一。

关于 ① 的严重性,我们也复核了触发面:dashboard onboarding(明确「绝不产出空 allowedUsers 的可启动 bot」)、交互式 setup(assertOwnerWhenChatGroups)、CLI add(promptRequiredOwner)三条受支持的新建路径都强制 owner,所以唯一触发途径是手改 bots.json 配了 autoOncallOperatorOpenIds 却不配 owner。因此我们没有把它提到最高档 —— 但因为修法只有一行、且能顺带修掉「owner 被锁死」这个很难排查的症状,仍然建议合前修。

以上依然是自动评审的初步意见,可能有误判,最终以维护者审阅为准。

@litongde1996
litongde1996 force-pushed the feat/lark-auto-oncall-chat-access branch from 6c6a687 to f84be97 Compare August 28, 2026 03:33
@deepcoldy

Copy link
Copy Markdown
Owner

谢谢更新,f84be9785 我们逐条实测复验了一遍 —— 上一轮提的三条都修好了,代码层面没有阻断意见。下面是复验记录,以及一条非阻断的补测试建议。

三条修复的实测结果

① 总闸 —— 已修,且报警器也补上了

autoOncallChatshasConfiguredAllowlist 摘掉之后,四象限重跑:

场景 上一版 现在
无 owner bot:其它群 canTalk ❌ false ✅ true
无 owner bot:canOperate ❌ false true
有 owner bot:陌生人在授权群 canTalk ✅ true ✅ true(功能没丢
有 owner bot:陌生人 canOperate ✅ false ✅ false(边界没松)
有 owner bot:owner canOperate ✅ true ✅ true

我们上一轮特别担心「缺陷修了但报警器还是坏的」(那行原本零覆盖)——这次没有发生:把那行加回去(等于把缺陷复原),测试立刻红一条,红的是 does not turn an open bot into allowlist mode after opening a chat automatically,走的是 bot.added 之后另一个群的真 canTalk/canOperate

/revoke —— 已修,而且比我们建议的更彻底

我们原本只建议在调用方多清一次,你改的是 grant-store.removeAllowedChatGroup 本体,在同一次 RMW 里把手动与自动两份一起清。真跑 store:撤销后磁盘和内存两份都为 undefinedcanTalk 真的变 false,「假成功」不复存在。反向变异(退回只清手动来源)→ 2 条红。

原子性我们也走了一遍 rmwBotEntry 的形状:两份在同一个 mutate 闭包里改、一次 rename 落盘,内存只在 r.ok 之后统一动;唯一残留窗口是「rename 成功后、内存更新前进程崩溃」,而那种情况重启读盘即 fail-closed,方向是安全的。

③ 事件处理器 —— 已合并,不是二选一

现在只注册一次,invalidateChatStats(同步执行,不进队列)与 removeAutoOncallChat(ack-safe 异步)都在。全仓 tsc --noEmit 干净,TS1117 消失。另外 rebase 也做了(之前落后约 1324 个提交、4 处冲突,现在 MERGEABLE)。三个测试文件 339/339 通过。

一条非阻断建议:给「不 mock」的那份测试补一条用例

我们做了一个分文件的检查,结果值得告诉你:

只跑这个文件 把 ① 的缺陷复原后
test/auto-oncall-store.test.ts(不 mock store) 全绿,抓不到
test/event-dispatcher.test.tsvi.mock 了 store) 1 failed,抓到

也就是说,① 这条缺陷的报警器完全长在 mock 那一侧。我们顺手把 mock 与真 store 的内存侧写抽出来跑了同一串操作序列(重复 add、删不存在的、删到空、删空后再 add),7/7 逐步一致,所以当下是可靠的、不影响这次的结论。

但它的可靠性依赖「这份手抄的 mock 与真实现保持同步」,而这件事没有任何机制维护 —— 以后真 store 改了内存侧语义(比如去重、排序、或改成不删空字段),mock 不会跟着变,测试照绿,报警器就悄悄失效了。

建议:在 test/auto-oncall-store.test.ts(不 mock 的那份)里补一条用例,直接钉住「open 模式 bot 自动授权后,总闸不得翻转 / owner 仍能 operate」。这样这条判据就有了不依赖 mock 同步纪律的锚点,和 event-dispatcher 侧那条形成双保险。这条不阻断合入,你觉得合适再加。

另外要更正我们上一轮的一条意见

上一轮我们说「还有两处权限判断没跟上(card-handler 手写的 hasAllowlistcheckAllowedChatGroupsConfig 启动告警)」—— 这条是我们说错了,你不改它们才是对的。

因为 ① 的正确修法是把这份名单从总闸里拿掉,而那两处正是同一个总闸的另外两个实现。如果「对齐」了,等于把 ① 的缺陷在卡片路径上重新引入一遍(无 owner 的 bot 自动授权后,所有卡片按钮会全部失效)。修复方向反转之后,我们基于旧方向提的对齐建议就不成立了,抱歉带来困扰。

还有一个待维护者定的方向问题(不是代码问题)

我们注意到 autoOncallOperatorOpenIds / autoOncallChats 目前在 dashboard、setup 交互、CLI 里都没有入口,唯一配置方式是手动编辑 bots.json。对照 Oncall 模式在 dashboard 上是有勾选项的(「默认进入 oncall 模式 / 所有未绑定的群下次开话题自动绑定」)。

而且 defaultOncall 本身已经实现了「自动绑定 + 落盘 + 整群成员可对话」(还额外绑定工作目录),这个 PR 相比它真正新增的能力是**「只有可信名单里的人拉群才算」**这道门 —— 这道门的价值我们认为是实的,「谁都能把 bot 拉进群就全群可用」和「只有 OnCall 系统拉的才自动开」安全含金量确实不同。

所以有个方向问题想请你和维护者一起定:这道门是否可以做成 defaultOncall 下的一个可选字段,而不是新增一套并行的群级授权?如果做成选项,dashboard 上也能顺势加个输入框,管理员就能看到自己的 bot 自动开放了哪些群。当然,如果你当初避开 Oncall 模式是有具体原因(比如它带的 workingDir 绑定不合适),那正是我们不知道的信息,麻烦在 PR 描述里说明一下,我们不再纠结这点。

以上仍是自动评审的意见,最终以维护者审阅为准。代码质量上我们这边已经没有阻断项了。

@deepcoldy

Copy link
Copy Markdown
Owner

维护者已就上一条里的方向问题做了决定:采纳方向 B —— 请把这个能力收敛到现有 defaultOncall(Oncall 模式)下,而不是新增一套并行的群级授权机制。

先说清楚:代码质量上我们已经没有阻断项了,上一轮三条都修得很干净。这次是产品/架构方向上的决定,所以还要请你再改一轮,辛苦。

为什么是 B

defaultOncall 已经实现了「自动绑定 + 落盘 + 整群成员可对话」(还额外绑定工作目录),你这个 PR 相比它真正新增的能力,是**「只有可信名单里的人拉群才算」**这道门。这道门有价值,我们认,但它更像 Oncall 模式的一个约束条件,而不是需要另起一套的能力。收敛过去的好处:

  • 代码量大幅减少(授权判断、落盘、撤销全部复用现成的)
  • 权限判断不再多散一处(现在 evaluateTalk / card-handler 手写 fallback / 启动校验分散在几个地方,多一套就多一处要同步)
  • dashboard 上能顺势加输入框(defaultOncall 已经有「默认进入 oncall 模式」的勾选项,管理员能看到状态;而 autoOncallOperatorOpenIds / autoOncallChats 目前在 dashboard、setup、CLI 里都没有入口,只能手改 bots.json —— 管理员没有任何地方能看到自己的 bot 自动开放了哪些群)

但有一个坎,麻烦你先评估再动手

我们读了代码,B 有一个时机错配要解决,先说出来免得你踩:

  • 「谁拉的」这个信息只在 im.chat.member.bot.added_v1 事件里有data.operator_id.open_id),别处拿不到
  • defaultOncall 的自动绑定是在「首次被 @」时触发的ensureDefaultOncallBoundevent-dispatcher.ts 的消息处理里被调用),入参只有 larkAppId / chatId / chatType没有 operator

所以不能简单地在 ensureDefaultOncallBound 里加一道 operator 判断——那时候已经拿不到是谁拉的了。可行的方向大致两种,供你参考(也欢迎你提第三种):

  1. bot.added 时把「是可信人拉的」这个事实记下来ensureDefaultOncallBound 绑定前查这个标记。相当于把你现在的 autoOncallChats 降级成一个内部标记,对外收敛成 defaultOncall 的一个约束。
  2. bot.added 时就直接调用现有的 autoBindOncallFromDefault(那里能拿到 operator),把绑定时机从「首次被 @」提前到「进群那一刻」。这条更简洁,但要注意它会改变现有 defaultOncall 的行为时序,得确认不影响既有用户。

另外提醒一个约束:defaultOncall.enabled 目前强制依赖 workingDirbot-registry.tsenabled: enabled && !!workingDir,没配目录则 enabled 恒为 false),而且它绑定时写的是带 workingDironcallChats。如果你的场景不希望顺带绑定工作目录,这一点会有摩擦——这恰恰可能是你当初避开 Oncall 另起一套的原因。如果是这样,请直接说,这是我们不知道的信息,方向可以再议,不要硬套。

还有一条建议一起做掉(原本非阻断)

test/auto-oncall-store.test.ts(不 mock store 的那份)建议补一条用例,直接钉住「open 模式 bot 自动授权后,总闸不得翻转 / owner 仍能 operate」。

原因是我们做了个分文件检查:把上一轮 ① 的缺陷复原后,只跑不 mock 的那份测试是全绿的(抓不到),只有 event-dispatcher.test.tsvi.mock 了 store)能抓到。也就是这条判据的报警器完全长在 mock 那一侧。我们对账过 mock 与真 store 的内存侧写当下逐步一致(7/7,含「删到空要删字段」的边界),所以不影响这次结论;但它依赖「手抄的 mock 与真实现保持同步」,而这没有任何机制维护 —— 以后真实现改了语义、mock 没跟着改,测试会照绿而报警器悄悄失效。

既然要再改一轮,顺手把这条补上比较划算。

小结

  • 代码没问题,是方向调整,辛苦再改一轮
  • 动手前先评估上面那个时机错配和 workingDir 约束;如果发现 B 在你的场景里成本明显更高,或者当初避开 Oncall 是有具体原因的,请直接说出来,我们再和维护者一起议,不要为了收敛硬套
  • 改完 @ 一下,我们再跑一轮复验

以上仍是自动评审的意见,方向决定来自维护者;具体实现方案以你和维护者商定为准。

@deepcoldy

Copy link
Copy Markdown
Owner

更正上一条里我的一处不准确表述,免得你按错的描述去找代码:

我写「路径② 在 autoBindOncallFromDefault 那里能拿到 operator」—— 这句不对。 那个函数的签名里没有 operator:

export async function autoBindOncallFromDefault(
  larkAppId: string,
  chatId: string,
  workingDir: string,
): Promise<>

operator 是在调用点的作用域里才有的(bot.added 处理器内的 operatorOpenId 局部变量)。所以路径② 的准确表述是:把绑定动作从「首次被 @」提前到 bot.added,用调用点上下文里的 operator 做闸,而不是"那个函数能拿到 operator"。改动面仍然会碰到既有 defaultOncall 的时序,请照旧确认存量影响。

另外补两条我们核实过的细节,对你评估方案有用:

1)如果你想走第三种方案「可信拉群 → 只开 talk、不绑 workingDir」,摩擦比表面更大。 现在 oncallChats 的解析是这样的:

.filter((c: any) => c && typeof c.chatId === 'string' && typeof c.workingDir === 'string')

也就是条目必须带 workingDir 才会被解析进来。要支持空 workingDir,等于动存量 schema,dashboard 侧那些 !input.workingDir 的校验也得跟着松。

所以如果你因为「不想绑工作目录」而认为 B 不合适、倾向保留独立实现或提第三种方案,这个理由是站得住的,我们不会视为回避 —— 请直接把它写在 PR 描述或回复里,我们会带着这条重新和维护者确认方向。不必为了迁就 B 去改存量 schema。

2)行号更正ensureDefaultOncallBoundsrc/services/oncall-store.ts:384defaultOncall 那条 enabled: enabled && !!workingDirsrc/bot-registry.ts:2893(均以当前 master 为准)。

结论不变:方向 B 是维护者的决定,但先评估、评估后觉得不合适可以反馈,比硬套更重要。

@deepcoldy

Copy link
Copy Markdown
Owner

再更正一次我上一条的「行号更正」—— 抱歉,上一条给你的行号是按 origin/master 数的,而你的分支上有一处不是那个数,按我给的数字去找会对不上。

准确的定位(用代码片段 + 基线标注,避免再错):

1)defaultOncallworkingDir 硬依赖 —— 同一句在两个基线上行号不同:

defaultOncall = { enabled: enabled && !!workingDir, workingDir, since };
  • 你的分支(PR head f84be9785):src/bot-registry.ts:2899以这个为准
  • 当前 origin/masterbe25b692e):src/bot-registry.ts:2893

master 在你的 rebase 基点之后又进了约 6 行改动,所以同一句漂移了。

2)ensureDefaultOncallBound —— 这一处两个基线恰好一致,都是 src/services/oncall-store.ts:384

export async function ensureDefaultOncallBound(
  larkAppId: string,
  chatId: string,
  chatType: 'group' | 'p2p',
): Promise<OncallChat | undefined>

3)oncallChats 解析要求带 workingDir(第三方案的摩擦点)——这处我上一条只贴了片段没给行号,仍以片段为准:

.filter((c: any) => c && typeof c.chatId === 'string' && typeof c.workingDir === 'string')

以后我们引用位置会统一用「代码片段 +(必要时)文件:行 @ commit」的形式,不再给裸行号 —— 这个仓库你的分支和 master 之间行号漂移较多,裸行号很容易把人带偏。给你添麻烦了。

其余结论都不变:方向 B 是维护者的决定,但请先评估那两个坎(operator 时机错配、workingDir 硬依赖);如果评估下来 B 在你的场景成本明显更高,或你当初避开 Oncall 就是因为不想绑工作目录,直接说出来即可,我们会带着这条重新和维护者确认方向,不需要为了收敛去动存量 schema。

@deepcoldy

Copy link
Copy Markdown
Owner

这个 PR 下面已经积了 6 条评论,其中最后三条是我在自我更正,读起来确实乱。这条把当前仍然有效的事项汇总成一份,作为唯一口径,前面几条如有冲突以本条为准(不含新内容,只做收敛)。

已完成,无需再动

v2(f84be9785)里上一轮的三条问题都已修好,我们逐条实测复验过:

  • ✅ 总闸不再被 autoOncallChats 污染(管理员能 operate 了,功能未丢)
  • /revoke 在同一次原子写里清掉手动与自动两份来源(「假成功」已消除)
  • bot.deleted_v1 合并成单个 handler(invalidateChatStatsremoveAutoOncallChat 都在),tsc 干净
  • ✅ 已 rebase 到 master,三个测试文件 339/339 通过

代码层面我们没有阻断意见。

待你处理(按优先级)

1. 先评估方向 B 的可行性,再决定动不动手(维护者决定的方向,但评估结论可以反馈

把这个能力收敛到现有 defaultOncall 之下,而不是新增一套并行的群级授权。动手前请先看这两个坎:

  • operator 时机错配:「谁拉的」只在 im.chat.member.bot.added_v1 里有(data.operator_id.open_id);而 defaultOncall 的自动绑定发生在首次被 @ 时,ensureDefaultOncallBound(larkAppId, chatId, chatType) 拿不到 operator。所以不能简单地在那个函数里加判断。
  • workingDir 硬依赖defaultOncall.enabled 要求配了 workingDir 才为真,绑定写的是带 workingDironcallChats 条目;而 oncallChats 的解析本身也要求条目带 workingDir
    .filter((c: any) => c && typeof c.chatId === 'string' && typeof c.workingDir === 'string')

⚠️ 如果你的场景是「可信拉群 → 只开 talk、不想绑工作目录」,那么 B 需要动存量 schema,成本明显偏高 —— 这个理由完全站得住,请直接说出来,我们会带着它重新和维护者确认方向。不要为了迁就 B 去改存量 schema。 你也可以提第三种方案。

2. 补一条测试(建议,非阻断)

test/auto-oncall-store.test.ts mock store 的那份)加一条用例,直接钉住「open 模式 bot 自动授权后总闸不翻转 / owner 仍能 operate」。原因:目前这条判据的报警器只长在 test/event-dispatcher.test.ts 的 mock 那一侧,不 mock 的那份对它零覆盖 —— 当下可靠,但依赖手抄 mock 与真实现保持同步,而这没有机制保障。

我方已作废的意见(不用管)

  • ❌ 「card-handler 的手写 hasAllowlist 和启动告警也要跟上」—— 撤回,你不改它们才是对的(跟上反而会把已修的问题在卡片路径上复现)
  • ❌ 上一条给的 bot-registry.ts:2893 —— 那是按 origin/master 数的;你的分支上是 2899。往后我们只给代码片段,不给裸行号。

定位信息(以你的分支 f84be9785 为准)

// src/bot-registry.ts:2899
defaultOncall = { enabled: enabled && !!workingDir, workingDir, since };

// src/services/oncall-store.ts:384(此处 master 与你的分支一致)
export async function ensureDefaultOncallBound(
  larkAppId: string, chatId: string, chatType: 'group' | 'p2p',
): Promise<OncallChat | undefined>

评论刷得有点多,抱歉。有任何一条你觉得判断不对,直接反驳即可。

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