feat(lark): /invite 支持跨团队 bot + botmux bot 进群自动拉 owner - #693
Merged
Conversation
- /invite 名字解析两级兜底:本机 bots-info.json 未命中时,查「同团队」目录 (services/team-bot-directory.ts)——平台团队同步名册(platform-team-sync.json) + 联邦名册(本机托管 federations.json + spoke 用 syncToken 现拉 hub 的 /api/federation/roster)。跨源同名多 app 报歧义并标注来源;无任何团队源 时不发 HTTP。修复拉另一台机器部署的 bot(如 claude@sg1)恒 unresolved - 结果展示用团队目录的显示名,不退化成裸 appId - bot 进群自动拉 owner:im.chat.member.bot.added_v1 事件上(handleBotAdded 之前,让 autoStart 的 D7 成员闸能吃到新 owner),用自己的 app scope 把 自己 owner 拉进群;已在群/无权限/无 owner 静默容错,不依赖任何开工开关。 新增 bots.json 开关 autoInviteOwnerOnGroupAdd(默认 ON,显式 false 关, 告警类 bot 可免打扰) Co-Authored-By: Riff
deepcoldy
force-pushed
the
feat/invite-team-resolution
branch
from
July 31, 2026 19:38
031a71a to
8b0f06e
Compare
修 codex 复审抓出的两个 blocker(PR #693 follow-up): P1(同名拉错 bot + 连带拉错 owner):原逻辑对无 app_id 的名字目标,只要 本机 bots-info.json 同名恰好 1 个候选就直接定案、根本不查团队目录——但 「本机唯一」≠「全局唯一」:跨部署「同团队」可能存在同名不同 app 的 bot (用户 @ 的正是远端那个,而飞书 mention 没带 app_id)。旧逻辑会静默把它 拉成本机的那个;本 PR 又默认「进群自动拉 owner」,错拉还会连带把错 bot 的 owner 拉进敏感群。改为把本机花名册 + 团队目录同名候选合并、按 larkAppId 去重后统一判定:唯一→拉;多个不同 app(跨本机↔团队/跨源)→报歧义带来源; 两边都无→unresolved。整目录仍每命令只装载一次;无团队源时不发 hub HTTP。 P2(团队目录漏 no-transport 过滤,破 #668 不变量):team-bot-directory 把 federation 两源(本地 FederatedBot + hub roster,均带 larkTransportEnabled) 所有 bot 纳入名字解析,未排除 larkTransportEnabled===false 的 core-only/ apiOnly bot。这类 bot 没有飞书传输身份、不能作群成员(#668),既有拉群路径 dashboard.ts 正是 filter(larkTransportEnabled===false) 剔除后才 invite。不加 过滤会让 /invite @CoreOnly 尝试拉一个进不了群的 app、留下收不到事件的死 bot。 两个 federation 入口都跳过显式 false,保留 true/undefined(旧版兼容按可传输); 平台名册无该字段、成员一律按可传输。 顺带:resolvedNameById 声明移到引用它的 nameOf 闭包之前(可读性)。 测试: - 新增 5 例(team-bot-directory:local/hub 两源各 1 例排除 transport===false 保留 true/undefined;invite-command:P1 跨本机↔团队同名不同 app→歧义、 同 app 两源→去重不误歧义、local hit 有团队源仍查目录、无团队源不发 HTTP) - 改 1 例:本机同名多候选歧义提示改为带来源标注(cli_x(local)) - 4 处 mutation 均见红(P2 local/hub 过滤、P1 短路各自精确变红) - pnpm build ✅;隔离 3 文件 63/63;相关联邦/groups 套 99/99 影响面:纯 daemon 侧 IM 事件/元命令层,不进 worker;team-bot-directory 仅被 invite-command 引用(blast radius 收敛);不改身份归一/授权,只收窄名字发现面。 Co-Authored-By: Riff <noreply@riff.dev>
codex delta 复审指出 P2 只闭合了一半:fetchRemoteHubBotDirectory 消费的 /api/federation/roster 有两类 bot——(1) hub 托管的 remote deployment bot, buildFederatedRoster 会透传 larkTransportEnabled(上轮已能过滤);(2) **hub 自己的 local bot**,buildFederatedRoster 的 local 映射根本没写这个字段, buildTeamRoster/LiveBot 也不带 apiOnly。于是 hub 本机的 core-only bot 到 spoke 看来是 undefined→被当 legacy-normal 保留,P2 原问题(把进不了群的 app 拉进群/留死 bot)仍可发生。 生产端补齐(与 federation-spoke-api 的 spoke 侧 advert 同源): - LiveBot + TeamRosterBot 增可选 larkTransportEnabled - buildTeamRoster 从传入的 liveBots 把 transport 带进 local roster(经 transportByAppId 映射;仅在 liveBots 提供时已知,否则 undefined=legacy normal) - buildFederatedRoster 的 hub-local 映射透传 larkTransportEnabled(与 remote-dep 映射对齐) - dashboard liveBots() 从 loadBotConfigs() 的 apiOnly 填 transport(config 不可读→undefined,与既有 isNoTransportBot 的 fail-open 语义一致) team-bot-directory 的过滤上轮已加,现在数据能真正到达它。 测试(codex 明确要求「端到端而非手工伪造 hub JSON」): - team-bot-directory.test.ts 新增 E2E:真 buildFederatedRoster(hub-local apiOnly bot)→ 真 fetchRemoteHubBotDirectory,断言 core-only 被剔除 - api-only-federation-capability.test.ts 新增 hub-local 三类(true/false/absent) 经 liveBots 进 roster 的断言(补上原注释所说「local 类未在聚合层覆盖」的洞) - 2 处 mutation 均见红:去掉 buildFederatedRoster local 透传 / 断开 buildTeamRoster 的 transportByAppId 线 → E2E + roster 测试精确变红 影响面:动了 buildTeamRoster/buildFederatedRoster/liveBots 三个公共读函数, 其消费者(dashboard 团队/联邦 UI、spoke advert、federated-group)全套回归: team-roster 40 + federation-spoke-api + federation-api + api-only 等 132+40 全绿。 纯新增可选字段,旧调用(不传 liveBots)行为不变。 Co-Authored-By: Riff <noreply@riff.dev>
…elta P2 收尾) codex delta 复审:上轮生产端主链路(liveBots→buildTeamRoster→buildFederatedRoster hub-local→spoke)已接通,但 dashboard liveBots() 的 config 读取失败分支是 fail-OPEN(apiOnlyIds=undefined→全体 larkTransportEnabled=undefined→远端消费端 按 legacy-normal 保留→core-only bot 又可能被 /invite 拉进群)。而对同一能力, 既有 federation-spoke-api.ts:177-193 明确 fail-CLOSED(config 不可读→全体 false, 因为无法确认"有 transport")。这条缝是 federated 出站(远端消费端无本地 config 可复核),必须与 spoke advert 同款 fail-closed,不能沿用仅面向本地 UI 的 isNoTransportBot fail-open 习惯(后者 fail-open 只因它旁边就有一次本地 config 兜底)。 修: - 抽纯 helper resolveLiveBotTransport(bots, apiOnlyIds | null)(team-roster.ts, 它已 own LiveBot):apiOnlyIds=null(config 读失败)→每个 bot transport=false (fail-closed);健康路径逐 bot !has(id)。 - dashboard liveBots() catch 分支从 undefined 改 null,走 helper。 测试(codex 指出原 E2E 从"已带 false 的 liveBot"起,绕过了 apiOnly→transport 转换,删掉 dashboard 那段仍会绿): - team-roster.test.ts 新增 resolveLiveBotTransport 4 例:健康 per-bot(apiOnly= false-transport/normal=true)、无 apiOnly 全 true、**FAIL-CLOSED: null→全 false (含 normal bot 也 false,不 fail-open)**、spread 保留其它字段 - mutation:helper 的 null 分支从 false 改 true(fail-open)→ FAIL-CLOSED 用例精确变红 影响面:helper 纯函数;liveBots() 仅 catch 分支语义变化(健康路径逐 bot 值不变); 全 8 套联邦/团队/api-only/dashboard-adjacent 回归 176 全绿。 Co-Authored-By: Riff <noreply@riff.dev>
解 groups-store.ts 冲突:两侧各在 addUsersByUnionId 之后 append 独立函数 (PR 侧 autoInviteOwnerOnGroupJoin + master #617 侧 listChatMemberDisplays), 取并集各带自身闭合括号,import 合并 listChatBotMembers。codex 审过的 autoInviteOwnerOnGroupJoin 逻辑逐字节未变。
|
🚀 Released in v3.9.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
改了什么
① /invite 支持跨部署团队 bot(跟进 #680)。申晗 live 实测
@执行bot /invite @群外bot时被婉拒「不是 botmux 机器人」——日志证实目标(如botmux开发者(claude@sg1))解析不出 larkAppId:本机 bots-info.json 只覆盖本部署,sg1 等其它机器的 bot 不在其中。services/team-bot-directory.ts):平台团队同步名册platform-team-sync.json(本地文件读,sg1/cn2 全在里面,live 已核实)+ 联邦名册(本机托管 federations.json + spoke 用 syncToken 拉 hub/api/federation/roster——该端点本来就是给成员拉聚合名册的)cli_xxx(platform:t1));无任何团队源 → 不发 HTTP 直接 unresolvedim.receive_id事件不在飞书控制台订阅清单里 → 收不到,只能 @② botmux bot 进群自动把 owner 拉进群(申晗语义:让那个机器人自己拉)。
im.chat.member.bot.added_v1事件钩子(handleBotAdded之前,autoStart 的 D7「群内需有 allowedUser」闸刚好能吃到新 owner):本 bot 用自己 app scope 把自己的 owner(resolvedAllowedUsers 首个 ou_ 用户)拉进群。已在群(成员预算表命中 / operator=owner)→ 幂等跳过;无 owner / API 拒绝 / 抛错 → 静默容错只记日志,不打扰群。跨部署零数据依赖(sg1 的 bot 被 cn1 拉进群后,sg1 自己的 daemon 拉 owner)。新 bots.json 开关autoInviteOwnerOnGroupAdd:默认 ON,显式 false 关(告警/oncall 类 bot 被平台批量拉事件群不打扰 owner)。影响面评估
测试验证
pnpm build✅ 0 错误test/team-bot-directory.test.ts10 例(platform 源/托管联邦源/hub HTTP 源 + Bearer/去重跨源/单 hub 失败不拖垮/403 跳过/三源合并/无源短路)test/group-join-owner.test.ts9 例(默认拉 owner/已在群跳过/成员表挂了照样 add/显式 false 跳过/operator=owner 幂等/无 owner 跳过/code≠0/抛错/invalid_id_list 均 failed 不 throw)test/invite-command.test.ts33→39 例(新增 6:platform 解析且展示名不退化 / 托管联邦解析 / hub HTTP Bearer / 跨源同名歧义带 source / 无源不发 HTTP / 本机花名册优先不查团队目录)group-join-shared-routing(beforeAll 10s import 超时,干净基线 stash 复跑同款挂,环境 flake)、card-handler-grant-partial(隔离+干净基线均挂,既有 master test debt)Live 验证(合入后需全 fleet 新机部署才生效)
sg1 目标:cn1 拉
botmux开发者(claude@sg1)应进群;sg1 的 daemon 部署含本 PR 后,进群应自动拉申晗。老版本无 owner 钩子→ 不拉,属预期。