Skip to content

feat(lark): /invite 支持跨团队 bot + botmux bot 进群自动拉 owner - #693

Merged
deepcoldy merged 5 commits into
masterfrom
feat/invite-team-resolution
Aug 2, 2026
Merged

feat(lark): /invite 支持跨团队 bot + botmux bot 进群自动拉 owner#693
deepcoldy merged 5 commits into
masterfrom
feat/invite-team-resolution

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

改了什么

/invite 支持跨部署团队 bot(跟进 #680)。申晗 live 实测 @执行bot /invite @群外bot 时被婉拒「不是 botmux 机器人」——日志证实目标(如 botmux开发者(claude@sg1))解析不出 larkAppId:本机 bots-info.json 只覆盖本部署,sg1 等其它机器的 bot 不在其中。

  • 名字解析改为两级兜底:本机 bots-info.json → 「同团队」跨部署目录(新 services/team-bot-directory.ts):平台团队同步名册 platform-team-sync.json(本地文件读,sg1/cn2 全在里面,live 已核实)+ 联邦名册(本机托管 federations.json + spoke 用 syncToken 拉 hub /api/federation/roster——该端点本来就是给成员拉聚合名册的)
  • 目录整载一次、按名索引批量匹配(不会每个目标一次 hub HTTP);跨源同名多 app → 报歧义并带来源标注(cli_xxx(platform:t1));无任何团队源 → 不发 HTTP 直接 unresolved
  • 结果展示用团队目录的显示名,不再退化成裸 appId
  • im.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)。

影响面评估

  • 跨 CLI / 后端 / 会话类型:无关。daemon 侧 IM 事件与元命令层,不进 worker
  • 事件面:bot.added_v1 既有订阅事件,无新订阅;每 bot 每群仅一次 add 调用(「已在群」预读一次 members 列表)
  • 联邦/平台团队:只读(本地文件 + hub GET roster);不发任何 sync/delegate 写;不入会、不改信任闸
  • 安全:不涉及身份归一/授权;operator=owner 比对用已核实的 app-scoped open_id;hub HTTP 仅明文内网权威 hub(memberships 来源于既有平台生态);owner 拉人已严格尊重 autoInviteOwnerOnGroupAdd=false
  • 改动文件:services(新 team-bot-directory.ts + federation-store 增 listAllFederatedDeployments 导出 + groups-store 增 autoInviteOwnerOnGroupJoin + bot-registry 增 BotConfig 字段与 parse)、event-dispatcher.ts(+5 行)、invite-command.ts(解析两级兜底)、i18n zh/en(unresolved 文案)

测试验证

  • pnpm build ✅ 0 错误
  • 新测试 25 个隔离全绿:
    • test/team-bot-directory.test.ts 10 例(platform 源/托管联邦源/hub HTTP 源 + Bearer/去重跨源/单 hub 失败不拖垮/403 跳过/三源合并/无源短路)
    • test/group-join-owner.test.ts 9 例(默认拉 owner/已在群跳过/成员表挂了照样 add/显式 false 跳过/operator=owner 幂等/无 owner 跳过/code≠0/抛错/invalid_id_list 均 failed 不 throw)
    • test/invite-command.test.ts 33→39 例(新增 6:platform 解析且展示名不退化 / 托管联邦解析 / hub HTTP Bearer / 跨源同名歧义带 source / 无源不发 HTTP / 本机花名册优先不查团队目录)
  • mutation 思维自查:关掉 teams 兜底分支 → platform/hub 两例 FAIL;去掉「无任何团队源不发 HTTP」→ no-sources 用例 ASSERT(fetch not called) FAIL;去掉 autoInviteOwnerOnGroupAdd=false → skipped 用例 FAIL
  • 全套 11953 passed;2 个红均与本 PR 无关: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 钩子→ 不拉,属预期。

- /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
deepcoldy force-pushed the feat/invite-team-resolution branch from 031a71a to 8b0f06e Compare July 31, 2026 19:38
deepcoldy and others added 4 commits August 1, 2026 08:23
修 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 逻辑逐字节未变。
@deepcoldy
deepcoldy merged commit c9757aa into master Aug 2, 2026
6 checks passed
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

🚀 Released in v3.9.0

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.

1 participant