dsh-im-ops/docs/方案/Issue-95-九渠道私聊群聊白名单与命令权限方案.md
2026-09-01 10:46:41 +08:00

621 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Issue #95:九渠道私聊、群聊白名单与命令权限方案
日期:2026-09-01。代码基线:v4.3.0 / `009f1ab`。状态:已实施并通过自动化、真实飞书群聊验收及用户手工私聊验收。
需求来源:[Issue #95](https://github.com/xmanrui/dsh-im/issues/95)。Issue 原始诉求包含群聊、私聊和命令白名单;本文按后续讨论收敛为九个 IM 渠道统一的发送者访问控制,以及“可以执行全部现有命令 / 不可以执行命令”两档权限。
## 1. 最终决定
九个 IM 渠道统一增加机器人级访问策略:
- 私聊和群聊的权限范围完全独立,但在同一个设置页中编辑。
- 两个场景都可以选择“允许所有用户”或“仅白名单用户”。
- 白名单按发送者限制,不保存、不选择、不限制群 ID;机器人能进入或接收哪些群,继续由渠道自身决定。
- 每个用户只有“可以执行命令”和“不可以执行命令”两档,不做命令名、命令组或角色分级。
- 同一用户可以在私聊中有命令权限、在群聊中没有,反之亦然。
- 原 owner、扫码接入者及现有明确授权身份始终保留原始访问权和全部命令权限,不受白名单模式、默认命令权限或用户行覆盖;访问设置只约束其他用户。
- 访问策略按 `botId` 保存,不跨机器人、渠道或 Host 共享。
实现复用现有三处机制:
1. 复用 `BotWorkspaceStore`,把访问策略和工作区、Agent Preset、上下文增强、投递目标一起按 `botId` 管理,不增加数据库或独立配置文件。
2. 复用现有“更多机器人设置”页面和页签骨架,在现有“投递设置”后增加第二个页签“访问设置”。
3. 复用现有六条入站处理路径:四个文字桥接渠道共用 `TextHarnessBridge`,另外五个渠道只接入薄的共享权限判断,不复制九套业务逻辑。
Telegram 和 WhatsApp 的旧访问设置自动迁移,迁移后旧入口不再显示。飞书当前没有用户可配置的白名单入口;“群聊响应方式”与白名单无关,继续保留。
AI Office Connector 不在本方案范围内,不修改其配置、协议、任务领取或执行链路。
## 2. 范围
### 2.1 必须实现
覆盖以下九个渠道:
1. 微信
2. 飞书
3. 钉钉
4. 企业微信
5. QQ
6. Slack
7. Telegram
8. Discord
9. WhatsApp
每个机器人必须具备:
- 一份私聊访问策略;
- 一份群聊成员访问策略;
- 两个场景各自独立的命令权限;
- 自动初始化或自动迁移;
- 设置保存后对后续入站事件立即生效,无需重连机器人;
- 删除机器人时随现有机器人级数据一起清理。
### 2.2 明确不做
- 不配置允许使用机器人的群、频道、Topic 或 Thread 列表。
- 不增加群 ID 白名单或群会话范围限制。
- 不按具体命令、命令组、管理员角色或组织部门授权。
- 不建立跨渠道统一用户账号,也不推断两个渠道 ID 是否属于同一个人。
- 不接入平台通讯录,不做用户搜索、昵称同步或自动补全。
- 不保存被动观察到的全量成员目录或访问审计数据库。
- 不限制主动投递、连接检查或机器人出站消息。
- 不改变 Session 的群聊、Topic、Thread 隔离方式。
- 不处理 AI Office。
## 3. 设置页体验
### 3.1 入口和页签
继续使用机器人卡片右上角的“更多机器人设置”入口。顶层只保留两个页签,顺序固定为:
| 页签 | 内容 |
| --- | --- |
| 投递设置 | 保持现状,仍作为默认页签,避免改变已有入口体验 |
| 访问设置 | 作为紧跟“投递设置”的第二个页签,统一编辑私聊、群聊的白名单和命令权限 |
“访问设置”页内按顺序竖向放置“私聊”和“群聊”两个区域,不再增加子页签、弹窗或第三个顶层页签。两个区域复用同一个场景编辑组件,页底只提供一个“保存访问设置”按钮,一次原子保存私聊和群聊策略。
微信当前没有群聊入站能力。其“访问设置”页仍显示群聊区域以保持九渠道页面结构一致,但该区域仅显示“当前渠道不支持群聊”说明并禁用控件;私聊区域仍可正常保存。其余渠道正常编辑群成员策略。
### 3.2 单个场景的控件
每个场景先提供访问模式:`允许所有用户` / `仅白名单用户`,再根据当前模式显示对应控件:
- `允许所有用户`:默认命令权限,以及独立的“命令权限例外”名单;
- `仅白名单用户`:独立的“白名单用户”名单,每行设置用户标识和命令权限;
- 两种名单都使用紧凑的新增按钮、删除按钮和逐行命令权限,但数据互不复用;
- 场景区域内不放单独保存按钮,统一使用页底的“保存访问设置”。
在“仅白名单用户”模式下,列表决定谁可以发送普通消息,每一行同时决定该用户能否执行命令。未添加白名单用户时,除渠道现有的特权身份外没有普通用户可以访问;保存前必须给出明确警告,但允许用户确认保存。
在“允许所有用户”模式下,所有发送者都可以发送普通消息;默认命令权限作用于未单独列出的用户,“命令权限例外”只覆盖指定用户的默认命令权限。
“白名单用户”和“命令权限例外”是两种业务场景的数据,必须分别保存。切换访问模式只切换当前生效规则,不转换、不清空另一份名单;切回原模式时恢复该模式上次保存的配置。
访问设置中的群聊区域只编辑群消息发送者,不提供群 ID 控件;不额外堆叠解释性旁白。
页面同时明确提示:原所有者/扫码接入者始终可以访问并执行命令,本页设置只约束其他用户。为保持实现简单,本期不把所有者做成特殊的只读用户行,也不允许页面修改其原始权限。
### 3.3 旧入口
- 删除 Telegram 机器人卡片中的旧“兼容模式 / 安全模式(私聊白名单)”访问设置。
- 删除 WhatsApp 机器人卡片中的旧“仅自己 / 指定联系人 / 开放响应模式”访问设置。
- 保留飞书机器人卡片中的“群聊响应方式”及其群消息权限授权流程。
- 其他机器人卡片、工作区、Agent Preset、上下文增强、连接检查和移除入口保持不变。
旧入口只从界面移除;旧数据在自动迁移成功前不能丢弃。
## 4. 最小数据模型
访问策略沿用现有 `direct` / `group` 场景划分,并在每个场景内分开保存 `open` 与 `allowlist` 配置:
```json
{
"direct": {
"mode": "allowlist",
"open": {
"defaultCanExecuteCommands": false,
"commandPermissionOverrides": [
{
"id": "open-mode-exception-id",
"canExecuteCommands": true
}
]
},
"allowlist": {
"users": [
{
"id": "direct-allowlisted-user-id",
"canExecuteCommands": true
}
]
}
},
"group": {
"mode": "open",
"open": {
"defaultCanExecuteCommands": false,
"commandPermissionOverrides": [
{
"id": "group-command-exception-id",
"canExecuteCommands": true
}
]
},
"allowlist": {
"users": [
{
"id": "group-allowlisted-member-id",
"canExecuteCommands": false
}
]
}
}
}
```
两套模式配置只复用相同的用户行形状,不复用同一个数组。仍不增加角色、规则表达式、命令数组或继承层级。
### 4.1 判断规则
| 模式 | 普通消息 | 命令 |
| --- | --- | --- |
| `open` | 所有可识别发送者均允许 | 命中 `open.commandPermissionOverrides` 时使用该行的 `canExecuteCommands`,否则使用 `open.defaultCanExecuteCommands` |
| `allowlist` | 只有命中 `allowlist.users` 的发送者允许 | 使用命中白名单行的 `canExecuteCommands`;未命中者连普通消息都不允许 |
两种模式只读取各自的数据。`allowlist` 模式不读取开放模式的默认命令权限或例外名单,`open` 模式也不读取白名单;切换模式不会让一类用户自动变成另一类用户。
在上表之前先判断渠道现有的特权身份。命中原 owner/扫码接入者时直接允许普通消息和命令,不读取场景模式或用户行。特权身份继续来自渠道既有、可验证的接入配置,不复制进访问策略,也不新增管理员角色:
- 微信:具体的 `ownerUserId`;
- 飞书:具体的 `ownerOpenIds`,通配符 `*` 不是人类身份,不作为特权 ID;
- 钉钉:现有 `approvedSenders[].staffId`;
- QQ:具体的 `ownerUserOpenid`,通配符 `*` 不作为特权 ID;
- WhatsApp:当前绑定账号 `accountJid`,复用现有 JID 等价比较;
- 企业微信、Slack、Telegram、Discord 当前没有可验证的人类 owner/扫码者字段,不臆造特权身份。Telegram 旧 `allowedUsers` 只是可编辑白名单,不升级为永久 owner。
### 4.2 校验和归一化
- 配置必须同时包含 `direct` 和 `group`,整份原子保存。
- `mode` 只能是 `open` 或 `allowlist`。
- `open.defaultCanExecuteCommands` 和两类用户行的 `canExecuteCommands` 必须是布尔值。
- 用户标识转成字符串、去除首尾空白、拒绝空值和控制字符,最大 256 个字符。
- 同一场景的同一份名单内不允许出现重复用户标识;同一 ID 可以分别存在于两种模式的名单中,权限互不继承。
- 除 WhatsApp 外均按渠道提供的稳定发送者标识精确匹配。
- WhatsApp 复用现有号码/JID 归一化和 `areJidsSameUser()`,兼容号码 JID、LID 和备用发送者 JID,不另写身份算法。
- 入站事件无法得到可靠发送者标识时拒绝处理,不根据昵称猜测身份。
### 4.3 每个渠道使用的发送者标识
| 渠道 | 私聊 | 群聊成员 |
| --- | --- | --- |
| 微信 | `from_user_id` | 当前不支持群聊入站 |
| 飞书 | `sender.sender_id.open_id` | `sender.sender_id.open_id` |
| 钉钉 | `senderStaffId`,缺失时沿用现有 `senderId` | 同左 |
| 企业微信 | `from.userid` | `from.userid` |
| QQ | `user_openid` 对应的现有 `senderId` | `member_openid` 对应的现有 `senderId` |
| Slack | `event.user` | `event.user` |
| Telegram | `message.from.id` 的字符串形式 | 同左 |
| Discord | `message.author.id` | 同左 |
| WhatsApp | 当前发送者号码/JID | 群参与者号码/JID及其备用标识 |
标识是渠道内、机器人内、场景内的不透明字符串。尤其 QQ 的私聊 `user_openid` 和群成员 `member_openid` 不能互相推导,所以同一页中的两个区域必须分别配置。
## 5. 入站处理顺序
访问策略只增加一道共享门禁,不接管渠道原生路由:
```text
平台事件基本校验与机器人回声过滤
↓
渠道现有触发规则(@、回复、Thread、群消息权限等)
↓
原 owner/扫码接入者命中则直接保留访问与全部命令权限
↓(其他用户)
按 direct/group + senderId 检查访问策略
↓
若现有命令解析器识别为命令,再检查命令权限
↓
现有去重、批量输入、问题/审批、Session、Harness 和回复流程
```
实现时访问判断必须早于以下副作用:
- 下载图片或文件;
- 创建、切换或清除 Session;
- 执行本地命令或 Harness 命令;
- 回答问题或审批;
- 触发模型请求;
- 执行飞书卡片中的命令等价操作。
被白名单拒绝的消息沿用现有安全行为:不调用 Harness、不创建 Session、不回复。可以复用现有日志记录渠道、`botId`、场景和拒绝原因,但不新增审计库、计数器,也不输出完整发送者标识。
发送普通消息有权限、但执行命令无权限时,由当前渠道现有文字发送方法回复“你可以发送普通消息,但没有执行命令的权限。”;该消息在本地消费,不能作为普通 Prompt 转发给模型。
权限设置对保存后的新入站事件生效,不取消已经开始的 Harness Turn,也不追溯改变已经进入队列的事件。
## 6. 命令权限边界
“命令”指当前代码已经识别并在本地执行的 dsh-im 命令,包括:
- `/help`、`/status`、`/version`、`/new`、`/stop`、`/steer` 等控制命令;
- Workspace、Session、模型、推理等级、Agent Preset、历史和压缩命令;
- `/batch`、`/send`、`/cancel` 等批量输入控制命令;
- 飞书 `/repair`、`/watch` 等现有渠道命令;
- 飞书卡片中与新建 Session、切换 Workspace/Session/模型/Preset 等价的操作。
实现不能简单地把所有 `/` 开头文本都判成命令。应复用现有命令解析器,并为目前“解析和执行混在一起”的 Workspace、Compact 等分支补充无副作用的识别函数。未被现有解析器识别的 `/foo` 继续按现有普通消息行为处理。
以下不是命令,只检查普通访问权限:
- 普通文字、图片和文件消息;
- 对 Harness 补充问题的回答;
- 对 Harness 审批的批准或拒绝;
- 渠道原生引用、提及和线程回复本身;
- 主动投递和连接测试。
飞书 `/repair` 等现有平台授权流程仍保留平台侧和所有者身份校验。新的命令布尔值不能绕过这些已有安全条件,也不新增第二种管理员角色。
## 7. 九渠道行为基线与初始化
新策略取得唯一处理权前,必须把当前可验证的入站行为转换为等价初始值。所有当前能够执行命令的用户在初始化后均为 `canExecuteCommands: true`;开放范围的 `open.defaultCanExecuteCommands` 也为 `true`,避免升级后功能突然减少。
初始化名单用于保持升级前其他用户的行为;owner/扫码接入者不复制到可编辑、可公开的访问策略行,而是始终由上一节的 Host 特权判断保留原始权限。
| 渠道 | 当前行为基线 | 私聊初始值 | 群聊初始值 | 继续保留的渠道行为 |
| --- | --- | --- | --- | --- |
| 微信 | 仅绑定账号所有者可以发送消息 | `allowlist` 空名单;owner 由 Host 特权放行 | `allowlist` 空名单、默认不允许命令;当前仅作完整策略占位,Runtime 不读取 | 微信扫码身份、消息协议和回复机制 |
| 飞书 | 没有用户可见白名单设置;底层 `ownerOpenIds` 当前承担有效发送者边界 | 有 `*` 则 `open`,否则为 `allowlist` 空名单,owner 由 Host 特权放行 | 同私聊 | “仅 @ 响应 / 响应所有群消息”、群消息权限和 Topic Session |
| 钉钉 | 私聊直接处理,群聊被 @ 时处理 | `open` | `open` | Stream、`isInAtList`、AI Card、现有授权信息 |
| 企业微信 | 处理平台投递的私聊和群聊消息 | `open` | `open` | 企业微信自身可见范围、WebSocket 和流式回复 |
| QQ | 扫码机器人私聊仅绑定者;手动绑定的 `*` 为开放;群聊接受任意被 @ 的成员 | `*` 为 `open`,否则为 `allowlist` 空名单,owner 由 Host 特权放行 | `open` | `GROUP_AT_MESSAGE_CREATE`、Markdown 回复和群成员作用域 ID |
| Slack | 私聊直接响应,频道仅处理 `app_mention` | `open` | `open` | Socket Mode、提及、线程和流式消息 |
| Telegram | `compatible` 为私聊开放、群聊提及/回复;`private-allowlist` 为白名单私聊且拒绝群聊 | 按旧模式迁移 | 按旧模式迁移 | 提及、回复、Topic、Rich Message 和命令菜单 |
| Discord | 私信直接响应;服务器频道首次 @ 后进入机器人管理的 Thread | `open` | `open` | Gateway、@、Thread 创建与后续 Thread 路由 |
| WhatsApp | `self-only`、`private-allowlist`、`open` 三种模式 | 按旧模式迁移 | 按旧模式迁移 | 自聊、提及、回复、已读和输入状态 |
飞书在产品上没有旧白名单入口,因此不称为“迁移飞书白名单”。但当前代码确实用 `ownerOpenIds` 过滤普通消息和卡片操作者;如果直接把新版策略设为开放,会扩大现有访问范围。这里仅用它生成一次等价初始策略。初始化后:
- 新访问策略负责普通用户的消息和命令访问,原 owner 先走共享特权放行;
- `ownerOpenIds` 继续保留给接入所有者、修复校验和连接测试等原有用途;
- “群聊响应方式”继续独立生效;
- `ownerOpenIds` 不再作为第二套普通用户白名单叠加,只用于共享特权放行及既有修复校验,避免新增用户仍被隐藏门禁拒绝。
## 8. Telegram 和 WhatsApp 自动迁移
本节只处理 Telegram、WhatsApp 已存在的渠道访问配置。开发期曾使用但从未发布的统一结构 `{ mode, defaultCanExecuteCommands, users }` 不兼容、不迁移,也不在 Store 加载时回写;配置必须直接使用第 4 节定义的最终拆分结构。
### 8.1 Telegram
| 旧配置 | 新私聊设置 | 新群聊设置 |
| --- | --- | --- |
| `compatible` | `open`,默认可执行命令;旧 `allowedUsers` 保存在独立的 `allowlist.users`,当前不生效 | `open`,默认可执行命令 |
| `private-allowlist` | `allowlist`,迁移全部 `allowedUsers` 到 `allowlist.users`,每人可执行命令 | `allowlist`,空名单,即继续拒绝所有群成员 |
旧名单只属于私聊,不能自动复制为群成员名单。
### 8.2 WhatsApp
| 旧配置 | 新私聊设置 | 新群聊设置 |
| --- | --- | --- |
| `self-only` | `allowlist`,空名单;当前绑定账号由 Host 特权放行 | `allowlist`,空名单 |
| `private-allowlist` | `allowlist`,仅迁移全部 `allowedNumbers` 到 `allowlist.users`,允许命令;当前绑定账号由 Host 特权放行 | `allowlist`,空名单 |
| `open` | `open`,默认允许命令;旧号码保存在独立的 `allowlist.users`,当前不生效 | `open`,默认允许命令 |
旧 `allowedNumbers` 只用于私聊,不能复制为群成员名单。绑定账号身份仅保留在 Host,使用现有账号 JID 和身份匹配函数做特权判断,不写入可公开的访问策略。
### 8.3 迁移时机和原子性
1. Host 加载渠道 `config.json` 和 `workspaces.json`。
2. 若 `accessPolicies[botId]` 已存在,直接使用,绝不再次根据旧字段覆盖。
3. 若不存在,按第 7、8 节生成完整策略。
4. 先使用 `BotWorkspaceStore` 现有临时文件加 `rename` 方式原子写入包含新策略段的 `workspaces.json`。
5. 写入成功后再启动该机器人 Runtime,新策略成为唯一普通消息访问来源。
迁移必须幂等。空白名单也是有效配置,不能因为数组为空而被误判为“尚未迁移”。
为避免跨两个文件做脆弱事务,本期不强制删除 Telegram/WhatsApp `config.json` 中的旧字段;它们作为不可见、只读的回退数据保留,但 Runtime、状态接口和页面都不再使用。后续如需清理可单独实施,不能在本期双写一个无法表达私聊/群聊拆分及命令权限的旧模型。
迁移写入失败时,不得默认开放。受影响渠道不完成启动并显示可诊断错误,其他渠道继续运行;旧 `config.json` 未被修改,可在修复文件权限或磁盘问题后重试迁移。
## 9. 持久化与共享实现
### 9.1 复用 `BotWorkspaceStore`
继续使用 `workspaces.json` 版本 2,增加一个可选的顶层 `accessPolicies` 段:
```json
{
"version": 2,
"workspaces": {},
"agentPresets": {},
"contextEnhancement": {},
"deliveryTargets": {},
"accessPolicies": {
"bot_id": {
"direct": {},
"group": {}
}
}
}
```
这是向后兼容的增量字段,处理方式与现有可选的 `contextEnhancement` 类似,不为一个独立的可选段或内部 schema 调整引入新文档版本。首次为仍是 v1 的文档写入访问策略时,与现有投递目标逻辑一样写成 v2;已是 v2 的文档不变版本。这样回退到当前代码时,旧读取器仍能加载工作区等原有数据,只会忽略不认识的策略段。
需要在现有 Store 中增加的能力只有:
- `accessPolicyFor(botId)`;
- `setAccessPolicy(botId, policy, { incarnation })`;
- `ensure(botId, { initialAccessPolicy })` 初始化;
- `decorateStatus()` 返回当前策略;
- `reconcile()` 和机器人删除事务同步清理策略。
保存仍使用现有每机器人队列、临时文件、`0600` 权限和原子重命名。一次保存失败不能发布半份策略,私聊和群聊不能分两次落盘。
`accessPolicies` 的校验错误必须与工作区、Preset、上下文增强和投递目标隔离:某个机器人策略损坏时,只标记该机器人的访问策略不可用并拒绝其入站消息,不能让整份 `workspaces.json` 失效或影响其他已有功能。
### 9.2 共享权限模块
新增一个浏览器和 Host 均可导入的 `src/channels/shared/access-policy.mjs`,只负责:
- 默认值和完整配置校验;
- 用户标识归一化;
- `direct` / `group` 访问判断;
- 普通消息和命令两种判定结果;
- 自动迁移所需的简单构造函数。
渠道代码只负责提供当前事件的 `conversationType`、发送者候选标识及 WhatsApp 的既有匹配函数。共享模块不解析平台原始 Payload,不依赖 SDK。
### 9.3 动态读取,不重启机器人
生产装配层仿照现有上下文增强注入一个只读 Provider:
```js
{
botId,
getSettings: () => workspaces.accessPolicyFor(botId)
}
```
桥接器在事件到达时读取一次已提交快照。设置保存成功后,下一条事件自然读取新策略;不修改 Token、WebSocket、长轮询或 Runtime 生命周期,也不因改白名单重连机器人。
动态 Provider 同时提供一个只读的 `isPrivileged(senderIds, conversationType)` 判断,优先复用渠道现有 owner/扫码身份及 WhatsApp JID 比较。该判断不持久化到 `accessPolicies`,因此访问设置无法意外撤销接入者的原始权限。
### 9.4 RPC
复用现有各渠道 RPC 和 `createWorkspaceAwareController()`,统一增加:
```text
bot.access-policy.set
{ botId, policy }
```
该端点复用现有 RPC authority、Bot 存在性检查、incarnation 防止删除后同 ID 串写,以及完整状态快照返回方式。Telegram 和 WhatsApp 当前同名端点改为接收新版统一结构,不再保存旧渠道字段。
不新增公网 HTTP 权限接口,也不把访问策略混入主动投递 API。
### 9.5 设置页
在现有机器人设置页中增加共享 `AccessPolicyEditor`:
- 页面根据 `channel + botId` 调用对应渠道现有 RPC;
- “访问设置”固定为 `BOT_SETTINGS_TABS` 中紧跟“投递设置”的第二项;
- 同一页同时渲染 `direct` 和 `group`,复用同一场景编辑组件;
- 复用现有页签、按钮、表单、错误提示和中英文 i18n;
- 渠道定义只提供用户标识名称、占位示例及 `groupSupported`,不复制页面;
- 一次提交完整的 `direct + group` 策略,保存后用 RPC 返回的状态快照更新页面,不做乐观假成功。
## 10. 六条入站接入路径
九个渠道实际只需接入六条消息路径:
| 接入位置 | 覆盖渠道 | 改动 |
| --- | --- | --- |
| `src/channels/shared/text-harness-bridge.mjs` | Slack、Telegram、Discord、WhatsApp | 在共享 `accept()` 中加入一次访问判断和命令判断 |
| `src/channels/weixin/weixin-bridge.mjs` | 微信 | 用共享策略替换普通消息中的硬编码 owner-only 判断,所有者信息继续用于接入和连接测试 |
| `src/channels/feishu/bridge.mjs` | 飞书 | 普通消息和卡片操作改用共享策略;保留群聊响应方式及修复流程原有校验 |
| `src/channels/dingtalk/dingtalk-bridge.mjs` | 钉钉 | 在进入命令、审批和 Session 流程前增加共享判断 |
| `src/channels/wecom/wecom-bridge.mjs` | 企业微信 | 在现有消息快速路径前增加共享判断 |
| `src/channels/qq/qq-bridge.mjs` | QQ | 用共享策略替换私聊 owner 判断,保留群聊 @ 和群作用域成员 ID |
Telegram 的 `telegramInboundAllowed()`、WhatsApp 的 `whatsappInboundAllowed()` 以及微信、飞书、QQ 的旧普通消息硬编码门禁,在等价初始化完成后退出活动入站路径,避免出现“新版允许、旧版又拒绝”的双重权限来源。可暂时保留纯函数供迁移和回归测试使用,但 Runtime 不再调用。
飞书卡片必须区分:
- Harness 问题回答和审批:只要求普通访问权限;
- Session、Workspace、模型、Preset 等命令等价操作:还要求命令权限。
这样既不会让卡片绕过命令限制,也不会误伤正常的人机交互。
## 11. 必须保持不变的功能
新策略与以下现有机制是逻辑“且”关系,不能替代或删除:
| 渠道/能力 | 必须保持的行为 |
| --- | --- |
| 飞书 | `groupResponseMode`、群消息权限授权、Topic Session、卡片回调、`/repair` 平台校验 |
| 钉钉 | 群聊 `isInAtList`、Session Webhook 安全校验、AI Card 和 @发送者 |
| 企业微信 | 平台自身授权范围、群回调语义和流式回复 |
| QQ | `GROUP_AT_MESSAGE_CREATE`、私聊/群聊不同 Open ID、Markdown 回复 |
| Slack | 私聊和 `app_mention`、Thread、Socket Mode 和流式消息 |
| Telegram | 群聊提及/回复、Topic、长轮询、Rich Message 和原生命令菜单 |
| Discord | 私信、首次 @、机器人管理 Thread 和消息编辑流 |
| WhatsApp | 自聊识别、群聊提及/回复、回声过滤、已读和输入状态 |
| 九渠道共享 | 去重、批量输入、问题、审批、工作区、Session、模型、Preset、上下文增强、附件和产物回传 |
群聊最终可处理条件示例:
```text
渠道把该群消息交给机器人
AND 现有 @/回复/Thread/响应方式成立
AND 发送者满足群聊访问策略
AND(若为命令)发送者具有群聊命令权限
```
本方案不改变群内回复的可见性。机器人在群里回复后,群内其他成员是否可见仍由平台和群成员关系决定;白名单只控制谁能触发 dsh-im。
## 12. 错误、安全与并发
- 配置缺失由启动初始化补齐;初始化完成后,运行时缺失或损坏的策略按拒绝处理,不能回退为开放。
- 访问判断在附件下载和 Harness 调用前完成,拒绝事件不能产生模型成本或本地 Session。
- RPC 只返回当前机器人显式保存的策略,不返回 Token、Secret、平台原始事件或被动观察到的成员目录。
- 日志只记录渠道、`botId`、场景和拒绝原因,不记录完整用户 ID、消息正文或旧白名单内容。
- 保存使用完整策略和现有机器人 incarnation;机器人被删除或重新接入后,旧页面提交必须失败。
- 同一机器人的设置写入沿用现有队列串行化;最后一个成功提交的完整策略生效。
- 权限检查使用事件到达时的已提交快照。保存中的草稿和写盘失败内容绝不影响运行态。
- 被拒绝的命令不得转为普通 Prompt;被拒绝的卡片命令不得执行一半后再报错。
## 13. 最小改动清单
1. 新增共享访问策略模块和共享 RPC Payload 校验。
2. 在 `BotWorkspaceStore` v2 文档中增加可选访问策略段的读取、原子保存、状态装饰和删除清理。
3. 在九渠道生产装配中为现有机器人自动初始化/迁移策略,并向 Runtime 注入动态 Provider。
4. 在六条入站路径接入共享判断;飞书卡片额外区分普通交互和命令等价操作。
5. 为现有命令分支补齐无副作用识别,避免用 `/` 前缀粗略判断。
6. 在各渠道现有 RPC 中复用统一的 `bot.access-policy.set`。
7. 在“投递设置”后增加第二个顶层页签“访问设置”,并新增一个共享编辑组件。
8. 删除 Telegram、WhatsApp 机器人卡片上的旧访问设置组件;保留自动迁移和旧字段只读兼容。
9. 更新中英文 README、设置文案、帮助说明和 CHANGELOG。
10. 增加共享、Store、迁移、六条桥接路径、九渠道页面及回归测试;不增加第三方依赖。
## 14. 测试方案
### 14.1 共享策略
- 私聊和群聊互不影响。
- `open`、`allowlist`、空白名单和未知发送者结果正确。
- 开放模式默认命令权限及用户覆盖正确。
- 白名单模式逐用户命令权限正确。
- 开放模式命令例外与白名单分别保存;来回切换模式不会重解释、清空或覆盖另一份名单。
- deny-all 或删除全部可编辑用户行时,未写入访问策略的原 owner/扫码接入者仍可发送普通消息并执行命令;通配符 `*` 不能成为“所有人永久特权”。
- 重复 ID、空 ID、超长 ID、控制字符、缺字段和多余字段被拒绝。
- WhatsApp 多 JID/号码匹配复用现有算法。
- 策略损坏时拒绝而不是开放。
### 14.2 Store 和迁移
- v1 文档首次写入策略时无损升级到 v2;v2 文档保持版本不变。
- 工作区、Preset、上下文增强和投递目标在策略初始化、保存、损坏及删除场景下均不受影响。
- 已有 `accessPolicies[botId]` 时不重复迁移。
- 空名单不会触发第二次迁移。
- Telegram 两种模式和 WhatsApp 三种模式逐项符合第 8 节。
- 微信、飞书、QQ 的当前所有者边界初始化后等价。
- 写盘失败不发布新策略,临时文件不会被误读。
- 删除机器人同时清理访问策略;同 ID 重新接入不会继承旧策略。
### 14.3 入站行为
九渠道分别覆盖:
- 允许用户的普通私聊消息进入原处理流程;拒绝用户不调用 Harness。
- 允许群成员在满足渠道原有触发条件时进入;拒绝成员不能触发。
- 同一用户私聊允许、群聊拒绝,以及相反组合。
- 可执行命令用户执行现有命令;不可执行命令用户收到本地拒绝且不触发副作用。
- 未识别的 `/foo` 保持原有普通消息行为。
- 问题回答和审批不被误判为命令。
- 图片和文件在权限通过后才下载。
- 重复平台事件不重复回复;被拒绝事件不会因重连绕过策略。
渠道专项回归:
- 飞书 mention/all 两种群聊响应方式、群消息授权、Topic、卡片问题/审批和命令卡片。
- 钉钉群 @、AI Card、Session Webhook。
- 企业微信群聊流式回复。
- QQ 扫码私聊所有者、手动绑定 `*`、群成员 @ 和两类 Open ID。
- Slack `app_mention` 和 Thread。
- Telegram 提及、回复、Topic、长轮询和 Rich Message。
- Discord 首次 @ 建 Thread 和已管理 Thread 后续消息。
- WhatsApp 自聊、联系人私聊、群提及/回复、LID/备用 JID 和回声过滤。
- 微信所有者私聊及当前不支持群聊的页面提示。
### 14.4 设置页
- 九渠道机器人齿轮页均显示两个顶层页签:默认的“投递设置”和紧随其后的“访问设置”。
- 访问设置在同一页分别读取和编辑私聊、群聊策略,一次保存两者;每个场景的白名单与命令权限例外也各自保留,不丢草稿、互不串数据。
- 空白名单警告、命令开关、用户新增/删除、保存失败和重复 ID 提示正确。
- Telegram、WhatsApp 旧访问卡片不再出现。
- 飞书“群聊响应方式”仍在原位置并正常保存、授权。
- 微信的群聊区域显示不支持说明。
- 中英文文案完整,键盘页签和表单标签可访问。
### 14.5 全量与实机
自动化最终执行 `npm run check`,并确保现有工作区、上下文增强、主动投递、附件、产物、问题、审批、命令、连接检查、机器人删除与重连测试全部通过。
按本次实施要求,使用本机已登录的飞书客户端,选择一个现有机器人完成群聊和私聊实机验收:
1. 设置页显示迁移后的完整策略,且“访问设置”位于“投递设置”之后;
2. 群聊仍要求真实 `@`,不改变“仅在 @机器人时响应”规则;
3. 在群策略为空白名单、默认禁止命令时,原扫码 owner 仍能执行一个本地命令;
4. 同一 owner 的普通群消息仍能进入 Harness 并得到回复;
5. 在私聊策略为空白名单、默认禁止命令时,原扫码 owner 仍能执行本地命令;
6. 同一 owner 的普通私聊消息仍能进入 Harness 并得到回复;
7. 实测前后策略保持一致,没有为测试临时扩大白名单。
已登录客户端不能安全模拟另一个群成员,因此非 owner 的静默拒绝、命令拒绝和不下载附件由桥接器回归测试覆盖,不把 mock 结果描述成实机结论。
## 15. 验收标准
以下条件全部满足才可认为 Issue #95 完成:
1. 九个 IM 渠道都使用同一策略语义,AI Office 未被修改。
2. 每个机器人的第二个顶层页签均为“访问设置”;私聊、群聊互相独立,每个场景内的白名单和开放模式命令权限例外也分别保存。
3. 群聊只按发送成员控制,不存在群 ID 或群会话范围配置。
4. Telegram、WhatsApp 旧设置自动、幂等、无损迁移,旧入口消失。
5. 飞书群聊响应方式和群消息授权保持原功能;飞书没有被描述成已有用户白名单产品功能。
6. 微信、飞书、QQ 的隐藏发送者边界在切换到新策略时不扩大访问范围。
7. 所有既有渠道触发、Thread/Topic、Session、流式回复、附件、审批和卡片能力通过回归。
8. 普通访问被拒绝时不调用 Harness;命令被拒绝时不产生任何命令副作用。
9. 设置保存无需重连,机器人删除会清理策略,旧页面不能写入已删除机器人的配置。
10. 没有新增数据库、第三方依赖、群目录、角色系统或九份重复实现。
## 16. 实施顺序与回退
建议按以下顺序实施:
1. 共享策略及 Store v2 可选段测试;
2. 九渠道初始化/迁移测试;
3. 六条入站路径接入和命令识别测试;
4. RPC 与共享设置页;
5. 移除 Telegram、WhatsApp 旧入口;
6. 九渠道回归、全量构建和实机验收。
新策略只有在迁移完成、六条入站路径和设置页均通过回归后才取得唯一处理权。回退代码版本时旧 Telegram/WhatsApp 字段仍在,原 `config.json` 未被迁移过程改写;v2 `workspaces.json` 中新增的 `accessPolicies` 可由当前旧版本忽略,因此工作区等旧功能仍可读取。需要明确的限制是:回退后若旧代码再次写入 `workspaces.json`,会丢弃它不认识的新策略段;再次升级时可从保留的旧字段重新迁移,但不承诺恢复只存在于新版中的群聊和命令权限修改。
## 17. 实施与验收记录
### 17.1 自动化与构建
- `npm run check` 通过:1992 项测试全部通过,无失败、取消或跳过。
- 客户端和 Host 构建通过,`lib/client.js`、`lib/index.js` 已更新。
- 包产物验证通过,`git diff --check` 通过。
- 九渠道 Host 初始化、旧配置迁移、统一 RPC、动态策略读取和 owner 特权均有定向回归。
- 五个自定义桥与四个共享文字桥均覆盖普通拒绝、命令拒绝、owner 放行,以及拒绝前不下载附件、不创建 Discord Thread、不调用 Harness。
### 17.2 本地飞书群聊与私聊验收
验收时间:2026-09-01。使用本机已安装并登录的飞书客户端。
| 项目 | 实测值 |
| --- | --- |
| 机器人 | `今天是牢梁`(`bot_9577c8572d454122a4ef86180bf13566`) |
| 群聊 | `DeepSeek大会` |
| 群聊原生触发规则 | `仅在 @机器人时响应`,保持不变 |
| 群聊实测策略 | `group.mode = allowlist`、`group.allowlist.users = []`、`group.open.defaultCanExecuteCommands = false`、`group.open.commandPermissionOverrides = []` |
| 私聊实测策略 | `direct.mode = allowlist`、`direct.allowlist.users = []`、`direct.open.defaultCanExecuteCommands = false`、`direct.open.commandPermissionOverrides = []` |
| owner 数据 | owner 不出现在公开策略用户行,只由 Host 内部特权判断放行 |
群聊真实消息结果(本次实施验收执行):
1. 发送 `@今天是牢梁 /status`,机器人回复“连接正常”,并返回当前工作区和 Agent Preset;证明原扫码 owner 在空白名单、默认禁止命令时仍保留本地命令权限。
2. 发送普通标记消息 `@今天是牢梁 ISSUE95_OWNER_OK_20260901`,机器人回复“已收到。”;证明同一 owner 的普通群消息仍进入原 Harness 流程。
3. 实测前后 `workspaces.json` 中该机器人的群策略完全一致,没有因验收临时开放访问或加入 owner 用户行。
私聊真实消息结果(用户手工执行并确认):
1. 在同一机器人私聊中执行本地命令,命令正常响应;证明 owner 在私聊空白名单、默认禁止命令时仍保留本地命令权限。
2. 向同一机器人发送普通私聊消息,消息正常进入 Harness 并得到回复;证明 owner 的普通私聊访问也不受白名单影响。
3. 私聊验收未修改访问设置;记录时再次确认 `direct` 策略仍为空白名单且默认禁止命令。
结论:指定飞书机器人的群聊和私聊验收均通过。群聊结果由本次实施验收直接执行,私聊结果由用户手工执行并确认;二者共同验证 owner/扫码者“不受白名单和命令权限影响、保留原始权限”的最新要求成立。