fix(setup): 权限数据范围自动收敛为「与应用的可用范围一致」,不再以「全部」提审 - #1061
Merged
Conversation
建 bot / 权限自愈只调了 `scope/update`(把权限点加进清单),但其中一部分权限还各带 一份「权限可访问的数据范围」要填 —— 那是**另一个接口** `privilege/update`,botmux 全仓从未引用过。于是每次自动发版都带着「未配置」提交审核,而这些权限都是「需审核」 档,租户审批规则明写「非必要不申请全员数据,如申请全员范围请提供充分的理由说明, 视情况加签至 CEO-2」。 改动: - 新增纯函数 `extractOpenPlatformPrivileges` / `canFillPrivilegeWithAppAvailability` / `buildPrivilegeAppAvailabilityContent` / `selectPrivilegesNeedingAppAvailability` / `buildPrivilegeUpdatePayload`。 - `automateOpenPlatformSetup` 在 `scope/update` 之后、`app_version/create` 之前补 一步:读 `privilege/all`,对「isRequired 且当前为空」且 schema 可安全填的条目写成 `availability_of_app`(即「与应用的可用范围一致」)。顺序是硬要求——写在发版之后 这一版仍带「未配置」提审,等于没修。 - 结果新增 `privilegeRangeCount` / `privilegeRangeWarning`。0 有两种成因(本来没有 待填的 / 写失败),必须靠 warning 区分,不能照 count 报「已配齐」。 判据与格式都对齐 console 而非猜的:`schemaType===SelectionExpression(1) && organizationType===InternalOrganization(1)`(console 的 `em()`),只取 `isRequired` (console gate `jC()` 同档);在此之上额外要求每个字段都是选人控件且支持 `in` ——`availability_of_app` 是成员范围语义,混了「工作地点」这类字段(DLP 执行日志那条 就是)会整条跳过,留给人手配。已有 content 的一律不覆盖。 非致命,与 scope 注册同档:数据范围只影响后续审批快慢,不影响 bot 收发消息,读写 失败都只记 warning 继续发版。 影响面:仅 feishu 的开放平台自动化链路(建 bot / 权限自愈 / VC listener 保存 / dashboard 建 bot 共用 `automateOpenPlatformSetup`)。新增的是一次读 + 条件性一次写, `scope/update` 与发版行为逐字不变;从权限台账重建 ACK 的那条路径显式报 0 + warning (没读写过,不谎报已配)。 验证: - `tsc --noEmit` 0 错;`bun run build` 绿。 - 相关 4 个测试文件 196/197,唯一失败是 `collectBotmuxRedirectUrls` 的环境基线用例 (未改动的 master 上同断言同样失败,已 stash 对照确认)。 - content 格式**逐字节**对齐线上由人在 console 手工配好的应用(6 个已配应用里 5 个 完全相同,第 6 个仅 description 是飞书改名前的旧文案 ⟹ description 纯展示)。 - 增量合并语义为实测所得:只传 1 条并真实改动它,同应用另一条已配好的数据范围逐字节 不变(先用 `content:''` 试探时服务端直接忽略、判不出方向,换成写真实不同值才钉死)。 - 反向变异 6 组全红:删整段接线(+3)、搬到发版之后(+2,含顺序断言)、去掉选人字段 守卫(+5)、允许覆盖已配置项(+1)、filter value 不套 JSON 字符串(+3)、改坏 DM 抬头文案(+1,用于确认放宽 region 窗口后断言仍有牙)。
live 建 bot 实测发现上一个 commit **完全空转**:`privilegeRangeCount` 恒为 0。
根因:「一键创建智能体」模板(`manifest/upsert_by_template`)建出来的应用,这两条
数据范围**出生就带 `{"mode":"all"}`**(console 上显示选中「全部」)——正是审批规则里
要补充理由、视情况加签至 CEO-2 的那一档。而上一版的守卫是「有 content 就算配过、
不覆盖」(本意是别覆盖人手精心配的范围),模板塞的这个默认值刚好满足「有 content」,
于是被当成用户的选择跳过。
对照实验坐实是模板本身写的:加 `--no-open-platform-auto` 跳过整个后半段自动配置,
建出来的应用照样是 `mode:"all"`。
改动:
- 守卫从「content 非空」改为「**已收敛到 all 以外**」(新增 `isPrivilegeRangeNarrowed`):
`mode:'all'` 视为待收窄;`mode:'part'` 等具体范围仍绝不覆盖;content 存在但不是合法
JSON 时保守视为已配置(覆盖一个读不懂的值风险更大)。
- **`createOpenPlatformAppWithClient` 也要做一次**。它建完模板紧接着就发第一个版本
上架,而 `automateOpenPlatformSetup` 发的是下一个版本——只改后者救不回第一版,那一版
仍带「全部」进审批。抽出共享的 `narrowRequiredPrivilegeRanges` 两处都调;创建路径里
非致命(此时应用已建成未发版,为只影响审批快慢的一步把整条创建链路判死,会把用户丢进
手动读 Secret 的恢复路径)。
验证(三个真实 bot 的对照,同一租户):
| 形态 | 数据范围 | 最新版本 |
|---|---|---|
| 改前(`!content` 守卫) | `all` / 待发布 | Init |
| 改前(`--no-open-platform-auto`,纯模板) | `all` / 待发布 | Init |
| **改后** | **`part` / `availability_of_app` / 已生效** | **Audited(已通过)** |
- `tsc --noEmit` 0 错;`bun run build` 绿。
- 相关 4 个测试文件 200/201,唯一失败是 `collectBotmuxRedirectUrls` 的环境基线用例
(未改动的 master 上同断言同样失败)。
- 新增测试的 fixture 是**线上抓下来的原文**(含 `mode:"all"` 那两条)。⚠️ 第一版 fixture
把 `\n` 写成了真换行 → 那串不是合法 JSON → 走进「读不懂 → 保守视为已配置」分支,测试
会假绿;已改为 JSON 转义序列并加注释说明。
- 反向变异 3 组全红:退回 `!content` 守卫(+3,即本次修的 bug 本体)、把 `mode:'all'`
也当已收敛(+4)、建应用路径不收窄(+4)。
CI `build` 红(本地漏跑了这个文件):`test/setup-pickers.test.ts` 里
`createOpenPlatformAppWithClient` 的四个用例都用**按调用顺序返回**的 stub,
并且按下标断言 body。上一个 commit 在 `event/switch` 与 `app_version/create`
之间插了 `privilege/all`,把后续每个响应和下标都错位了一格。
- 四处 stub 补上 `privilege/all` 的响应(`{privileges:[],scopeBiz:[]}` = 无待收窄项)。
其中两个用例原本"侥幸还绿"(断言是 `some()` 形式),但响应序列同样已错位,一并修正。
- 顺序断言里加上 `/developers/v1/privilege/all/<appId>`,位置即证明收窄发生在发版之前。
- 版本 payload 的断言从写死下标 `client.calls[4]` 改为**按路径查找**:这条链路中间
插过步骤,写死下标会让任何后续插入都变成一堆看不出所以然的失败。
反向变异确认改成按路径后仍有牙:把 `visibleSuggest.members` 的创建者去掉 → 变红。
验证:`tsc --noEmit` 0 错;`setup-pickers` / `open-platform-{description,avatar,rename}` /
`setup-open-platform-automation` / `scope-optional-autofix` / `dashboard-bot-onboarding*`
共 235/236,唯一失败仍是 `collectBotmuxRedirectUrls` 的环境基线用例。
自查时对着 console 前端自己的「是否配置好」谓词 `XC()` 逐条比对,发现我的判据比它宽:
```js
// console XC()
mode !== undefined && mode !== '' && mode !== 'null'
&& (mode === 'all' || (Array.isArray(filters) && filters.length > 0))
```
即 `mode:'part'` 但 `filters` 为空,在 console 眼里**不算配置好**(UI 显示「暂未配置
筛选条件」)。这是又一个「看着配过、其实是空的」中间态——我上一个 commit 刚因为
`mode:"all"` 被当成"配过了"而全盘空转,放过这个就是重犯同类错误。
- `isPrivilegeRangeNarrowed` 增加:非 all 的 mode 必须真的带 filters 才算收敛。
- 顺带补齐两个此前会误判为「已收敛」的形态:`mode` 整个缺失、`mode:'null'`
(console 的「无」)。
- 相应修正一处测试 fixture:原来用 `{"mode":"part","filters":[]}` 表示「人手配过的
具体范围」,按新判据它恰好是未收敛,改为带一条真实 filter。
反向变异:去掉 filters 非空检查 → 2 例变红。
验证:`tsc --noEmit` 0 错;相关 7 个测试文件 236/237,唯一失败仍是
`collectBotmuxRedirectUrls` 环境基线用例。
Owner
Author
|
复审自查补充一条(head // console XC()
mode !== undefined && mode !== '' && mode !== 'null'
&& (mode === 'all' || (Array.isArray(filters) && filters.length > 0))即 已补齐三个此前会被误判为「已收敛」从而跳过的形态: |
Owner
Author
|
CI 全 7 项绿(head 此前一次
(另有一次 补充 live 验证:
|
|
🚀 Released in v3.18.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.
问题
建 bot / 权限自愈只调了
scope/update(把权限点加进应用清单),但其中一部分权限还各带一份「权限可访问的数据范围」要填 —— 那是另一个接口privilege/update,botmux 全仓从未引用过。这两项权限的 scope level 都是「需审核」,而租户审批规则(
config/audit_rule)明写:受影响的权限点只有两个(只有它们的 schema 是 SelectionExpression + 内部组织):
vc/meeting.meetingid会议号查询会议信息vc:meeting.meetingevent:readtask/manage_task_and_tasklist_members创建、更新任务或清单时可指定的人员范围task:task:write、task:tasklist:write而且新建的 bot 不是「未配置」,是真的选了「全部」:「一键创建智能体」模板(
manifest/upsert_by_template)建出来的应用,这两条出生就带{"mode":"all"}。加--no-open-platform-auto跳过整个后半段自动配置,建出来照样是mode:"all"—— 坐实是模板本身写的。(另一个相关但不需要动的概念:
contact_range「通讯录权限范围」是第三个接口,实测线上 50 个 app 全部已是equalToVisible=true。)改动
在
scope/update之后、app_version/create之前补一步:读privilege/all,把「必须配、但还没收敛」的条目写成availability_of_app(即「按条件筛选」+「与应用的可用范围一致」)。顺序是硬要求:写在发版之后,这一版仍带「全部」提审,等于没修。因此两条路径都要做,各自发的是不同的版本:
createOpenPlatformAppWithClient—— 模板建完立刻发第一版automateOpenPlatformSetup—— 权限自愈 / 补配时发下一版只做前者,存量 bot 永不收窄;只做后者,新建 bot 的第一版仍带「全部」。抽了共享的
narrowRequiredPrivilegeRanges两处都调。判据与格式都对齐 console 而非猜的(从 console 前端 bundle 读出调用点与 payload 构造):
schemaType===SelectionExpression(1) && organizationType===InternalOrganization(1)(console 的em()),只取isRequired(console gatejC()同档)。在此之上额外要求每个字段都是选人控件且支持in——availability_of_app是成员范围语义,混了「工作地点」这类字段的(DLP 执行日志那条就是)整条跳过,留给人手配。「已收敛」的判据是「mode 不是 all」而不是「content 非空」:后者会把模板默认值当成用户的选择而跳过(见下方「过程中的一个自咬」)。
mode:'part'等具体范围绝不覆盖;content 存在但不是合法 JSON 时保守视为已配置。非致命,与 scope 注册同档:数据范围只影响后续审批快慢,不影响 bot 收发消息,读写失败都只记 warning 继续发版。结果新增
privilegeRangeCount/privilegeRangeWarning,0 有两种成因(本来没有待收窄的 / 写失败),必须靠 warning 区分。影响面
仅 feishu 的开放平台自动化链路。
automateOpenPlatformSetup被建 bot / 权限自愈 / VC listener 保存 / dashboard 建 bot 共用,createOpenPlatformAppWithClient被 CLI 与 dashboard 建 bot 共用 —— 两处都新增「一次读 + 条件性一次写」,scope/update、事件订阅与发版行为逐字不变。从权限台账重建 ACK 的那条路径显式报0 + warning(没读写过,不谎报已配)。写入按
(bizId, resource)增量合并(实测所得,见下),不会动同应用里其它已配好的条目。验证
三个真实 bot 的 live 对照(同一租户,同一天)
!content守卫)all/ 待发布--no-open-platform-auto,纯模板)all/ 待发布part/availability_of_app/ 已生效改后那个 bot
privilegeRangeCount: 2,两条都落地,版本审核通过而非停在「审核中」。格式正确性:content 与线上由人在 console 手工点过「与应用的可用范围一致」的应用逐字节相同 —— 6 个已配应用里 5 个完全一致,第 6 个仅
description是飞书改名前的旧文案(⟹ description 纯展示、不参与语义)。增量合并语义为实测所得:只传 1 条并真实改动它,同应用另一条已配好的数据范围逐字节不变。⚠️ 先用
content:''试探时服务端直接忽略(code=0 但值没变),「合并」与「整体 no-op」两个假设预测同一观测、判不出方向;换成写一个真实不同的值才钉死。自动化测试:
tsc --noEmit0 错;bun run build绿;相关 4 个测试文件 200/201,唯一失败是collectBotmuxRedirectUrls的环境基线用例(已 stash 到干净 master 对照确认:同断言在未改动的 master 上同样失败)。新增测试的 fixture 是线上抓下来的原文。反向变异 9 组全红(每组只报删了哪一部分):
app_version/create之后filter value不套 JSON 字符串!content守卫(= 本次修的 bug 本体)mode:'all'也当已收敛过程中的一个自咬(值得记一笔)
第一个 commit 自动化测试全绿、live 建 bot 也「成功」,但
privilegeRangeCount: 0—— 改动完全空转。原因就是上面说的:守卫写成「有 content 就算配过」,而模板塞的mode:"all"刚好满足。纯函数测试全绿掩盖了它,因为测试 fixture 里「未配置」用的是空 content,而生产里根本不存在这个形态。另外新增测试的第一版 fixture 把
\n写成了真换行 → 那串不是合法 JSON → 走进「读不懂 → 保守视为已配置」的分支,测试会假绿;已改为 JSON 转义序列并加注释。遗留(未包含在本 PR)
存量 49 个 app 里 48 个
vc/ 44 个task仍是「未配置」或「全部」。它们下次跑权限自愈发版时会被本 PR 自动收窄,但如果想立刻批量补一次,需要单独跑一个脚本(纯增量写、不动已配好的)。要不要做、什么时候做,等确认。🤖 Generated with Claude Code