@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -55
- package/docs/access-control-design.md +7 -5
- package/docs/attachment-pipeline-v2.md +33 -41
- package/docs/feishu-onboarding.md +33 -33
- package/docs/persistence.md +6 -4
- package/docs/refactor-plan.md +1 -1
- package/docs/workspace-isolation-design.md +7 -8
- package/lib/client.js +173 -287
- package/lib/client.js.map +1 -1
- package/lib/index.js +3001 -2627
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/access/cards.ts +9 -9
- package/src/access/controller.ts +10 -10
- package/src/access/index.ts +1 -0
- package/src/access/policy.ts +10 -10
- package/src/access/requests.ts +32 -31
- package/src/access/store.ts +12 -12
- package/src/access/targets.ts +56 -0
- package/src/cards/approval.ts +96 -26
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +1 -0
- package/src/cards/picker.ts +115 -0
- package/src/cards/streaming.ts +56 -7
- package/src/client/api.ts +5 -25
- package/src/client/panel/access-card.tsx +24 -15
- package/src/client/panel/access-tab.tsx +3 -3
- package/src/client/panel/connection-tab.tsx +70 -1
- package/src/client/panel/index.tsx +11 -62
- package/src/client/panel/shared.tsx +0 -23
- package/src/client/panel/workspace-tab.tsx +2 -18
- package/src/client/styles.ts +2 -0
- package/src/commands/registry.ts +2 -2
- package/src/config.ts +25 -56
- package/src/domain.ts +28 -125
- package/src/feishu/inbound-attachments.ts +1 -1
- package/src/feishu/manifest.ts +8 -31
- package/src/feishu/message-read.ts +19 -22
- package/src/feishu/scopes.ts +171 -0
- package/src/feishu/send-file.ts +543 -0
- package/src/feishu/slash-commands.ts +134 -0
- package/src/feishu/tools.ts +72 -112
- package/src/feishu-channel.ts +270 -9
- package/src/index.ts +5 -4
- package/src/interactions/approvals.ts +187 -7
- package/src/interactions/pending.ts +3 -3
- package/src/routing/inbound.ts +73 -106
- package/src/service.ts +463 -438
- package/src/session/manager.ts +1 -1
- package/src/session/routes.ts +2 -3
- package/src/settings/contract.ts +68 -85
- package/src/settings/routes.ts +3 -17
- package/src/settings/surface.ts +37 -88
- package/src/streamer.ts +94 -5
- package/src/workspaces.ts +21 -29
- package/src/cleanup.ts +0 -546
- package/src/client/panel/cleanup-card.tsx +0 -106
- package/src/feishu/pending-attachments.ts +0 -562
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
6
6
|
- 收到触发消息:立刻加 **“Typing”** 表情 → 以话题形式回复(**创建话题**)→ 绑定 DSH session → 卡片 2.0 **流式回复** → 成功时更新为 **“DONE”**,失败时更新为 **“No”**
|
|
7
7
|
- 话题内后续消息**无需再 @**,自动延续该 session
|
|
8
8
|
- 工作区归属:**每用户一个工作区**(私聊 = 用户;群聊 = 群 × 用户,同群不同用户互不影响);每个话题在工作区内拥有独立工作目录(默认 `topics/<话题 id>/`,Git 仓库工作区可自动改用 worktree),因此话题之间的文件互不干扰,而工作区数量只随用户增长
|
|
9
|
-
- **入站附件(v2,零等待)**:收到消息时**会话、话题、流式卡片立即建立,不等附件下载**。触发消息中的图片自动预取为首轮原生 `ImageBlock`,模型直接可见;文件默认不下载,prompt 用 `<feishu_context>` 清单声明“附件未加载”,模型需要时调用 `feishu_attachment_fetch` 按 `message_id` 懒取;引用消息与话题历史以 `<feishu_quote>` / `<feishu_history>` 摘录,更早的消息用 `feishu_topic_messages` 翻页(发现的资源同时获得 fetch
|
|
10
|
-
-
|
|
9
|
+
- **入站附件(v2,零等待)**:收到消息时**会话、话题、流式卡片立即建立,不等附件下载**。触发消息中的图片自动预取为首轮原生 `ImageBlock`,模型直接可见;文件默认不下载,prompt 用 `<feishu_context>` 清单声明“附件未加载”,模型需要时调用 `feishu_attachment_fetch` 按 `message_id` 懒取;引用消息与话题历史以 `<feishu_quote>` / `<feishu_history>` 摘录,更早的消息用 `feishu_topic_messages` 翻页(发现的资源同时获得 fetch 授权)。**只发附件、不带说明的消息完全静默**:不回复、不建会话、不下载、不加表情,本地也不留任何状态;要让模型看到它,请引用那条消息再提问(多个文件可合并转发后引用),话题内的附件也可由模型用 `feishu_topic_messages` 读到。所有入站图片/文件统一存入 **DSH 原生附件库**(不可变、内容寻址去重、读取校验、随 Session 回放;插件不承担入站附件清理)。显式 `attachmentStore: 'archive'`、宿主未挂载原生附件服务或关闭 fetch 工具时退回旧版工作区 `attachments/` 落盘。完整设计见 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
10
|
+
- **把生成的文件发给用户**:模型在话题工作目录里生成图片、报告、表格等文件后,可调用 `feishu_send_file` 直接发回当前话题/会话(可识别图片 ≤10 MB 以图片消息发送,其余以文件消息发送,文件上限 30 MB)。只能发送**当前话题工作目录内**的普通文件(`..` 与符号链接都按真实路径校验,不能读到其他话题或系统文件);需开通 `im:resource` 权限,缺失时工具返回可读的 `PERMISSION_DENIED`,回合不受影响。开关:`feishuTools.sendFile`(默认开)
|
|
11
|
+
- **HITL 审批**:当模型请求需要授权的动作(如 bash 沙箱升级 `sandbox_permissions`)时,插件监听主机 `approval/request` 事件,在同一话题里**新发一张独立审批决策卡**:卡上直接显示将执行的命令(或工具参数)和提权目标,提供 ✅ 允许 / ❌ 拒绝 / ♾️ 本话题不再询问 三个按钮;也可以在话题里直接回复「允许」「拒绝」「全部允许」,回复其他内容视为拒绝并作为下一条指令。等待期间流式主卡显示「⏳ 等待你审批」。超时(`approvalTimeoutMs`,默认 5 分钟)自动拒绝(fail-closed),非会话发起人点击或回复无效
|
|
11
12
|
- **结构化提问**:模型调用 `ask_user_question` 时,插件监听主机 `user-questions/request` 瀑布并发一张问答卡(每个问题一组选项按钮,多选先勾选再「提交本题选择」),答案回填给模型;主机自带的应答者只有 Web 客户端,没有本插件时纯飞书会话会以 `NO_PROVIDER` 失败。超时(`questionTimeoutMs`,默认 5 分钟)或回合取消时本轮提问失败(fail-closed);没有可选项的问题直接提示用文字作答,**阻塞期间普通文本回复即作为该问题的自由文本答案**(同时支持多选后的自定义文本)
|
|
12
|
-
- **准入与授权(默认开启)**:**管理员不受白名单限制**(私聊、任意群聊——包括尚未授权的群——都能直接发起会话);其他人私聊只收到一条固定提示(`/userid` 例外),群聊需要「群已授权 + 用户在全局白名单」两层同时命中。机器人被拉进群后私聊管理员发**群信息卡**(未授权群附授权按钮,用于放行白名单用户);已授权群里未授权用户 @ 会触发用户授权卡。授权入口:飞书 `/auth` 命令、授权卡、Web 设置面板;管理员
|
|
13
|
+
- **准入与授权(默认开启)**:**管理员不受白名单限制**(私聊、任意群聊——包括尚未授权的群——都能直接发起会话);其他人私聊只收到一条固定提示(`/userid` 例外),群聊需要「群已授权 + 用户在全局白名单」两层同时命中。机器人被拉进群后私聊管理员发**群信息卡**(未授权群附授权按钮,用于放行白名单用户);已授权群里未授权用户 @ 会触发用户授权卡。授权入口:飞书 `/auth` 命令、授权卡、Web 设置面板;管理员 user_id 在面板配置(私聊 `/userid` 获取),**留空时自动使用飞书应用所有者**
|
|
13
14
|
- 全部基于飞书官方 SDK [`@larksuiteoapi/node-sdk`](https://www.npmjs.com/package/@larksuiteoapi/node-sdk)(WebSocket 长连接,免公网)
|
|
14
15
|
- **Web 设置面板**:Web/桌面端 **设置 → 飞书渠道**,顶部状态头 + 6 个标签页(连接 / 会话与行为 / 模型 / 工作区 / 访问授权 / 高级);凭据不完整时插件不连接、面板显示三步引导,填写保存后自动连接(保存写入本机覆盖文件,部分字段需重连生效);详见下文「Web 设置面板」章节
|
|
15
16
|
|
|
@@ -18,35 +19,40 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
18
19
|
## 斜杠命令
|
|
19
20
|
|
|
20
21
|
- `/help`:列出当前支持的斜杠命令,无需绑定 Session
|
|
21
|
-
- `/userid`:仅在私聊中直接回复**发送者自己的**
|
|
22
|
+
- `/userid`:仅在私聊中直接回复**发送者自己的** user_id,无需绑定 Session,也不创建话题;**任何私聊用户都可以用**(准入配置/授权引导用,不消耗 token)
|
|
22
23
|
- `/groupid`:仅在群聊中直接回复当前群组 ID,无需绑定 Session,也不创建话题;普通群聊遵循 `requireMention`,话题内无需重复 @
|
|
23
24
|
- `/health`:显示 Harness 与飞书通道健康状态,无需绑定 Session
|
|
24
25
|
- `/status`:显示当前 Session 状态与性能指标
|
|
25
26
|
- `/workspace`:显示当前 Session 工作目录
|
|
26
|
-
- `/effort`:查看当前 Session
|
|
27
|
-
- `/model`:查看当前模型与该 provider
|
|
27
|
+
- `/effort`:查看当前 Session 推理强度;当前模型声明了可选档位时,回复卡附档位按钮,点一下即切换;`/effort <档位-id>` 切换,`/effort reset` 恢复配置默认
|
|
28
|
+
- `/model`:查看当前模型与该 provider 的可用模型,回复卡附模型按钮,点一下即切换;`/model <模型-id>`(沿用当前 provider)、`/model <provider>/<model>` 切换,`/model reset` 恢复部署默认
|
|
28
29
|
- `/compact`:调用 Harness 原生命令压缩当前 Session 上下文
|
|
29
30
|
- `/renew`:在当前话题创建并绑定新 Session
|
|
30
|
-
- `/stop`:中断当前正在运行的回合(不冷恢复 Session
|
|
31
|
-
- `/permissions`:查看当前 Session 的权限预设(沙箱 + 审批策略)与可选清单;仅在 `allowPermissionSwitch: true` 时支持 `/permissions <name>`
|
|
32
|
-
- `/auth`:**仅管理员**。`/auth status` 查看准入状态;`/auth allow` / `/auth revoke` 在群内授权/撤销当前群;`/auth user add
|
|
31
|
+
- `/stop`:中断当前正在运行的回合(不冷恢复 Session;排队中的后续消息保留并接着运行;没有运行中的回合时明确提示)。流式卡片上的「⏹ 停止」按钮效果相同
|
|
32
|
+
- `/permissions`:查看当前 Session 的权限预设(沙箱 + 审批策略)与可选清单;仅在 `allowPermissionSwitch: true` 时支持 `/permissions <name>` 切换,此时回复卡附预设按钮
|
|
33
|
+
- `/auth`:**仅管理员**。`/auth status` 查看准入状态;`/auth allow` / `/auth revoke` 在群内授权/撤销当前群;`/auth user add @用户` / `/auth user remove @用户`(也可直接写 `user_id`)管理全局用户白名单,@ 时自动取被 @ 成员的 user_id、忽略对机器人的 @;`/auth list` 列出白名单与待处理申请。即使群尚未授权,管理员也能在群内用 `/auth`(这是“通过对话授权群”的入口)
|
|
33
34
|
- `/history`:显示当前话题的 Session 历史(含 `ctx.sessionQuery` 折叠出的会话标题与 live 标记)
|
|
34
35
|
- `/export`:导出当前 Session 的逻辑日志
|
|
35
36
|
|
|
37
|
+
选择卡上的按钮等同于在该卡下输入对应命令(如点「deepseek-v4-pro」= `/model deepseek-v4-pro`),走同样的准入、归属与校验;只有话题发起人点击、且卡片仍在原聊天里时才生效,话题换绑 Session 后旧卡上的按钮失效。按钮最多显示 12 个,其余可直接输入命令。
|
|
38
|
+
|
|
36
39
|
`/new` 是 `/renew` 的别名。`/help` 由命令注册表按分组生成,新增命令只需在注册表里加一行。
|
|
37
40
|
|
|
41
|
+
**飞书原生斜杠指令面板**(可选,`slashCommandPanel: true` 开启,默认关闭;部分企业不允许应用添加斜杠指令):开启后每次连上飞书,插件在后台调用 `application/v7/app_slash_commands` 把命令注册表同步到飞书原生面板,用户在聊天框输入 `/` 即可直接选择命令(飞书客户端有约 5 分钟缓存)。同步是全量对齐且幂等:缺的创建、描述变了的原地更新、注册表里已没有的或重复的删除(别名不单独列出),注册表不变时再次连接不产生任何写操作;超时(30 秒)或断开后不再发起新的写操作。需要可选权限 `application:app_slash_command:read`(读取现有命令)与 `application:app_slash_command:write`(增删改),两项只在开启时列入权限自检。关闭时插件不读写面板。同步失败只影响面板列表,直接输入命令照常可用;失败原因显示在 Web 设置「连接」页并打印一条 warn。
|
|
42
|
+
|
|
38
43
|
## 流式卡片
|
|
39
44
|
|
|
40
|
-
卡片 2.0,按**过程 → 正文 →
|
|
45
|
+
卡片 2.0,按**过程 → 正文 → 停止按钮 → 页脚**组织,默认 500ms 刷新(`refreshMs` 可调):
|
|
41
46
|
|
|
42
47
|
1. **过程**(推理折叠面板 + 活动状态两行):
|
|
43
|
-
- **推理折叠面板**(`collapsible_panel`):带边框、默认折叠,右侧箭头随折叠状态翻转(折叠向下、展开向上,`icon_expanded_angle: 180`)。有推理内容时,面板标题沿成功链路 **推理中 → 回复中 → 回复结束** 切换,失败终态为
|
|
48
|
+
- **推理折叠面板**(`collapsible_panel`):带边框、默认折叠,右侧箭头随折叠状态翻转(折叠向下、展开向上,`icon_expanded_angle: 180`)。有推理内容时,面板标题沿成功链路 **推理中 → 回复中 → 回复结束** 切换,失败终态为 **回复失败**,被停止的终态为 **已停止**;**推理中阶段始终渲染**(兼作阶段指示灯与占位),**回复中 / 回复结束 / 回复失败阶段仅当存在推理内容时渲染**——无推理的完成/失败回合不会留下空面板
|
|
44
49
|
- **活动状态两行**(小字,**只在当前正在流式输出的那张卡上渲染**,已冻结的续卡/主卡不再重复;**互相独立、互不顶替**),让卡片在“模型不吐字”的阶段(工具执行、subagent、workflow)**不再静默**——按 `statusRefreshMs`(默认 5s)慢节奏刷新计时:
|
|
45
50
|
- **状态行**(委托进度):`tool-workflow/run-start / agent-start / agent-end / run-end`(工作流进度:完成数/总数、阶段、活动成员)→ run 结束转为 `🧵 工作流 xxx 结束(完成)` 并**保留到最终卡片**(后续工具调用、正文输出都不会顶掉它);**子代理进度**(跨会话映射):子代理是独立 session(`header.parentSession` 指向主会话),其 `subagent/descriptor`(label)、`tool/call`(当前工具)被折叠为 `🤖 子代理执行中:检索实现方案 (read) · 已运行 45s`,并行子代理显示计数,完成后在最终卡片汇总为 `🤖 子代理完成:a、b`。**只显示状态,不显示输出**——子代理正文是父模型的中间产物,绝不进卡片正文
|
|
46
51
|
- **工具行**(父会话当前工具调用):`tool/call` → `🔧 调用工具:bash · 已运行 Ns`(subagent 工具显示其 description、workflow 工具显示 meta.name),`tool/result` 返回即清除,**回合结束后收起**(完成态只保留结算状态行:工作流结论 / 子代理完成摘要)
|
|
47
52
|
- 两者可同屏:如 `🧵 工作流 xxx 结束(完成)` + `🔧 调用工具:bash`
|
|
48
53
|
2. **正文**:富文本 markdown;超长正文**自动分段投放**——单卡片预算用满后按干净断点(段落 / 标题 / 代码块边界)切分,且**每张卡最多 5 个表格**(飞书硬限制,错误 11310);断点会避开代码围栏与表格,**单个表格/代码块小于单卡预算时不会被从中间切断**(单块本身超过单卡预算时只能硬切,内容仍不丢失)。续片以「📄 回复分段 n」卡片跟进同一话题,**全文无丢失**;续卡被飞书拒收时自动降级为纯文本消息(内容仍送达)。**续卡只留正文**:`📄 回复分段 n` → 可选活动行(仅当它是当前活动卡)→ 正文,无面板、无页脚、无分割线
|
|
49
|
-
3.
|
|
54
|
+
3. **停止按钮**:回合进行中,当前正在流式输出的那张卡(主卡或最新续卡)正文下方显示「⏹ 停止」,效果同 `/stop`。首帧不带按钮;排在后面的消息的卡片在轮到它运行时才出现按钮。只有话题发起人在原聊天里点击才生效,点击后按钮变为不可点的「正在停止…」;其他人点击只会在按钮下加一行「仅发起该会话的用户可停止」。回合结束后按钮移除;经 `/stop` 或按钮停止的回合面板标题为 **已停止**,主卡页脚上方显示「⏹ 已按请求停止本轮回复」。
|
|
55
|
+
4. **结算页脚**(分割线 + 两行小字):**只在主卡、且只在回合结束(`done` / `stopped` / `error`)后渲染一次**——首帧、流式中、续卡都没有,因此不会出现 0/— 的空统计。
|
|
50
56
|
- 第一行:`🧠 模型 {model}({effort}) · 🔑 会话 {sessionId} · ⏱ 推理 {推理时长}`。推理时长为**本轮所有推理阶段的累计墙钟时间**(每个 step 从首个推理增量开始,到该 step 输出结束——首个正文增量或 step 结束——为止;工具执行时间不计入;回合在推理中结束时计到 turn 结束),无推理时为 `—`
|
|
51
57
|
- 第二行:`📊 上下文 {tokens}/{window} ({占比}) · 📩 输入 {n} · 📨 输出 {n} · ⚡ 缓存命中 {pct}`。占比 = 上下文 tokens / 窗口(<10% 保留一位小数,≥10% 取整);没有窗口时只显示 tokens
|
|
52
58
|
- 输入/输出为 session 累计口径;模型、推理强度、缓存命中按**当前轮次**,本轮在请求头产生前失败时模型/缓存命中显示 `—`、推理强度省略,不会沿用上一轮
|
|
@@ -65,17 +71,17 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
65
71
|
|
|
66
72
|
| 标签页 | 内容 |
|
|
67
73
|
| --- | --- |
|
|
68
|
-
| 连接 | 连接状态与连接信息(长连接状态、Bot open_id、运行时长、话题路由 / 活动会话 /
|
|
69
|
-
| 会话与行为 | 「行为」:私聊模式、普通群 @
|
|
74
|
+
| 连接 | 连接状态与连接信息(长连接状态、Bot open_id、运行时长、话题路由 / 活动会话 / 在途回复)、权限自检、斜杠指令面板同步结果、未配置时的三步引导、飞书应用凭据、最近一次连接失败原因、需重连的待生效提醒 |
|
|
75
|
+
| 会话与行为 | 「行为」:私聊模式、普通群 @ 要求、话题兜底、首次私聊引导、原生斜杠指令面板同步、权限切换;「内容与附件」:附件接收、存储后端、图片预取与上限、首轮历史;「模型工具」:`feishu_attachment_fetch` / `feishu_topic_messages` / `feishu_send_file` 开关与上限、被动读取预算 |
|
|
70
76
|
| 模型 | 模型服务商、模型 id、推理档位、Agent 预设(宿主目录驱动的级联下拉) |
|
|
71
|
-
| 工作区 |
|
|
72
|
-
| 访问授权 | 管理员
|
|
77
|
+
| 工作区 | 工作区根目录(可点「选择目录…」调用系统目录选择框);话题目录隔离方式(`topicIsolation`)在 profile 配置中设置 |
|
|
78
|
+
| 访问授权 | 管理员 user_id、准入模式、申请有效期;已授权群 / 用户白名单与待处理申请 |
|
|
73
79
|
| 高级 | 流式刷新 / 状态行 / SDK 日志级别、部署级只读参数、Web 覆盖文件路径与「清除 Web 覆盖」 |
|
|
74
80
|
|
|
75
81
|
**入站附件与模型工具(v2)**:「会话与行为」标签页新增两组可编辑字段,与 profile 配置同名、写入同一 Web 覆盖文件(`$DSH_HOME/dsh-channels/settings.json`):
|
|
76
82
|
|
|
77
|
-
- **内容与附件**:附件接收(`acceptAttachments`)、入站附件存储后端(`attachmentStore`:DSH 原生附件库 / 工作区 `attachments/`)、触发消息图片预取(`prefetchImages`)及单图 / 总量 / 整段超时上限(`prefetchImageMaxBytes` / `prefetchTotalMaxBytes` / `prefetchTimeoutMs`)、首轮话题历史条数(`historyBootstrapLimit
|
|
78
|
-
- **模型工具**:`feishu_attachment_fetch` 开关与单次资源数 / 字节预算 / 超时(`feishuTools.attachmentFetch*`)、`feishu_topic_messages` 开关与超时(`feishuTools.topicMessages
|
|
83
|
+
- **内容与附件**:附件接收(`acceptAttachments`)、入站附件存储后端(`attachmentStore`:DSH 原生附件库 / 工作区 `attachments/`)、触发消息图片预取(`prefetchImages`)及单图 / 总量 / 整段超时上限(`prefetchImageMaxBytes` / `prefetchTotalMaxBytes` / `prefetchTimeoutMs`)、首轮话题历史条数(`historyBootstrapLimit`)。
|
|
84
|
+
- **模型工具**:`feishu_attachment_fetch` 开关与单次资源数 / 字节预算 / 超时(`feishuTools.attachmentFetch*`)、`feishu_topic_messages` 开关与超时(`feishuTools.topicMessages*`)、`feishu_send_file` 开关(`feishuTools.sendFile`)、被动引用/历史读取预算(`promptContextTimeoutMs`)。
|
|
79
85
|
|
|
80
86
|
`feishuTools.*` 是嵌套配置:面板只把改动过的子键写进覆盖层,其余子键保持原值(Web 覆盖或 profile 配置),不会因部分保存变成未定义。字段默认值、边界与降级规则见下文配置示例和 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
81
87
|
|
|
@@ -87,19 +93,17 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
87
93
|
|
|
88
94
|
**模型选择器**:模型与推理在面板里是**独立的「模型」标签页**:宿主 LLM 服务可用时,`provider` / `model` / `reasoningEffort` 与 `agentPreset` 渲染为**下拉选择**而不是手填:provider 来自 `ctx.llm.listProviders()`,model 来自所选 provider 的 `listModels()`,推理档位来自 `resolveModelInfo(provider, model).reasoning.efforts`,Agent 预设来自 `agentPresets.list()`。每个选择器的首项是「留空 = 部署默认 / 适配器默认 / roster 默认」,有部署默认模型(`agentDefaultModel`)或 roster 默认预设时会在标签里带上具体值。选择按依赖级联:先选 provider 才会加载 model,选了 model 才会加载推理档位;宿主暂时无法枚举的项(服务未挂载、适配器查询失败/超时)自动退回原来的自由文本框,已保存但当前列表里没有的值会作为「当前配置,不在宿主机列表中」选项保留,不会在打开面板时被静默改掉。级联查询走只读的 `GET /dsh-channels/catalog?provider=…&model=…`(Host 侧短 TTL 缓存,单次查询 4 秒上限)。`provider` 与 `model` 在宿主侧是一条完整的请求路由,因此面板保存时要求**两者同时设置或同时留空**,不允许只选一半。
|
|
89
95
|
|
|
90
|
-
`baseDir` 是**模型/工具产生文件的落点**(入站附件默认进 DSH 原生附件库、不落这里;仅 archive 后端才写话题目录):**每个用户拥有一个独立工作区**(私聊 `<baseDir>/p2p/<
|
|
96
|
+
`baseDir` 是**模型/工具产生文件的落点**(入站附件默认进 DSH 原生附件库、不落这里;仅 archive 后端才写话题目录):**每个用户拥有一个独立工作区**(私聊 `<baseDir>/p2p/<user_id>/`,群聊 `<baseDir>/groups/<chat_id>/<user_id>/`,同一群里不同用户互不影响;旧版本的「用户 × 聊天」共享工作区正好复用为这个新工作区)。每个话题在工作区内拥有独立工作目录 `<工作区>/topics/<话题 id>/`(无话题的降级会话用 session id),所以话题之间的文件互不干扰,而工作区数量只随用户增长。面板里只接受**绝对路径**(`~` 开头按家目录展开,拒绝文件系统根目录),保存时会创建并校验该目录,不可用时当场报错,不会等到飞书里提示「会话创建失败」。已有 Session / 目录不会被迁移,因此切到新目录后旧话题继续用旧目录(话题内 `/renew` 也一样),只有之后新开的会话使用新目录。
|
|
91
97
|
|
|
92
98
|
**可视化选择工作区根目录**:输入框旁的「选择目录…」按钮会调用**宿主机的系统目录选择框**(macOS 的 NSOpenPanel / Windows 的文件夹对话框),选中后把绝对路径填进草稿,仍需点「保存更改」才落盘。该能力转发自宿主可选的 `ctx.directoryPicker` 服务:桌面端/本机 GUI 会话(native 后端)提供按钮;headless、远程或 SSH 会话没有可服务的显示会话(browse 后端),面板不显示按钮、保留手写输入,插件在缺少该服务的旧宿主上照常加载。选择框打开期间请求保持挂起;取消是正常结果,不会报错。
|
|
93
99
|
|
|
94
|
-
|
|
100
|
+
**访问授权**:「访问授权」标签页集中管理准入:管理员 user_id(私聊 `/userid` 获取)、准入模式、授权申请有效期;「已授权群」「已授权用户」列表支持增删(群 ID 可从入群群信息卡或 `/groupid` 获得,用户 user_id 可用 `/userid` 查看);「待处理申请」可一键授权/忽略。白名单只约束非管理员:管理员在任何私聊/群聊都直接放行。管理员留空时自动使用飞书应用所有者(连接时通过「获取应用信息」接口查询),标签页会显示“已自动使用应用所有者”;`guarded` 且既未配置管理员、也查不到应用所有者时,标签页顶部会红字提示“当前会拒绝所有对话”并附上查询失败原因。授权白名单与申请记录保存在插件 `storageDomain`(`channels_routes_v2` 域),重启后保留;未建立飞书连接时白名单尚未加载,标签页会提示「连接建立后加载授权列表」并禁用写操作。
|
|
95
101
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
**配置层级**:`configSchema` 默认值 < profile `cordis.patch.yml` 的 `config` < Web 面板覆盖。面板只把可编辑字段写入 `$DSH_HOME/dsh-channels/settings.json`(可用配置项 `settingsFile` 改路径);「高级 → 清除 Web 覆盖」删除该文件并回落到 profile 配置。`baseDir` 可在「工作区」标签页编辑(绝对路径;只影响之后新建的会话);超时、路由 TTL 等仍只读展示。凭据**值**在「连接 → 飞书应用凭据」编辑,凭据**引用名**(`appId` / `appSecret` 配置项)仍由 profile 配置决定。
|
|
102
|
+
**配置层级**:`configSchema` 默认值 < profile `cordis.patch.yml` 的 `config` < Web 面板覆盖。面板只把可编辑字段写入 `$DSH_HOME/dsh-channels/settings.json`;「高级 → 清除 Web 覆盖」删除该文件并回落到 profile 配置。`baseDir` 可在「工作区」标签页编辑(绝对路径;只影响之后新建的会话);超时、路由 TTL 等仍只读展示。凭据**值**在「连接 → 飞书应用凭据」编辑,凭据**引用名**(`appId` / `appSecret` 配置项)仍由 profile 配置决定。已移除的目录清理配置 `workspaceRetention` / `workspaceRetentionAuto` 与覆盖文件路径配置 `settingsFile` 若仍留在 profile 或覆盖文件中会被静默忽略,面板下次保存时从覆盖文件中去掉。
|
|
99
103
|
|
|
100
104
|
**为什么落在插件自己的文件而不是 profile 的 `cordis.patch.yml`?** DSH 提供官方 `ctx.configEditor.edit()`,可以安全写 profile patch(完整 Config 校验、保留注释与 `!!js`、原子替换、失败回滚、与 HMR 串行);但它是「完整配置编辑」通道——本插件的 Config 是 zod,没有 schemastery `.volatile()` 标记,普通字段写入会触发 Loader 重载插件,等于每次保存都断开并重连飞书;而且该服务只在有 `profileContext` 的启动器(桌面端 / `dsh web`)存在。`$DSH_HOME/<plugin>/` 是 DSH 插件保存运行时数据的惯例(如 dsh-pet 的 `$DSH_HOME/dsh-pet/`),因此面板偏好写入 `$DSH_HOME/dsh-channels/settings.json`:保存即生效(需重连字段除外)、可一键清除回退、不依赖 `profileContext`;profile 的 `cordis.patch.yml` 继续作为部署级基础配置层。若将来要求「所有配置统一进 profile」,迁移路径是改用 `ctx.configEditor.edit()` 并接受保存触发插件重载。
|
|
101
105
|
|
|
102
|
-
**安全边界**:面板路由与其他本机 DSH 路由一样**不带鉴权**;写操作(PUT / DELETE / POST)额外校验 `Origin` 与 `Host` 同源,拒绝跨站页面改写配置或凭据。**授权白名单与待处理申请同样面板可写,因此“本机访问面板 = 可授权”**;不要把 DSH Web 服务绑定到不可信网络。凭据值只走写路径、响应永不回显,也不会写进 Web 覆盖文件。`baseDir`
|
|
106
|
+
**安全边界**:面板路由与其他本机 DSH 路由一样**不带鉴权**;写操作(PUT / DELETE / POST)额外校验 `Origin` 与 `Host` 同源,拒绝跨站页面改写配置或凭据。**授权白名单与待处理申请同样面板可写,因此“本机访问面板 = 可授权”**;不要把 DSH Web 服务绑定到不可信网络。凭据值只走写路径、响应永不回显,也不会写进 Web 覆盖文件。`baseDir` 可写意味着同源页面能改变模型文件的落点(面板已拒绝文件系统根目录)。
|
|
103
107
|
|
|
104
108
|
**构建**:DSH 只加载已构建的浏览器 bundle,改了 `src/client/` 或 `src/settings/contract.ts` 后需要重新构建并重启应用:
|
|
105
109
|
|
|
@@ -109,6 +113,17 @@ pnpm verify:host # 以纯 ESM 导入 Host bundle,检查 Cordis 插件面
|
|
|
109
113
|
pnpm verify:client # 用 __ModuleLoader__ shim 做 loader 冒烟
|
|
110
114
|
```
|
|
111
115
|
|
|
116
|
+
## 发布
|
|
117
|
+
|
|
118
|
+
版本号由 semantic-release 按 Conventional Commits 计算,**只在手动触发时发布**;推送到 `main` / `dev` 只跑检查,可以先累积多笔提交。
|
|
119
|
+
|
|
120
|
+
| 分支 | 版本 | npm dist-tag |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `main` | 正式版,如 `0.5.0` | `latest` |
|
|
123
|
+
| `dev` | 预发布版,如 `0.5.0-beta.1` | `beta` |
|
|
124
|
+
|
|
125
|
+
在 GitHub Actions 的 `pipeline` workflow 点击 **Run workflow**,选择 `main` 或 `dev`:`dry_run` 默认勾选,只预览下一个版本号;取消勾选才真正发布 npm 包、打 tag 并创建 GitHub Release。安装 beta 版用 `@modesto_ma/dsh-channels@beta`。
|
|
126
|
+
|
|
112
127
|
## 目录结构
|
|
113
128
|
|
|
114
129
|
```
|
|
@@ -128,7 +143,8 @@ src/
|
|
|
128
143
|
├── cards.ts # 卡片 barrel → src/cards/
|
|
129
144
|
├── cards/ # 卡片 2.0 JSON 渲染
|
|
130
145
|
│ ├── primitives.ts # 信封、markdown、折叠面板、按钮、分栏等原语
|
|
131
|
-
│ ├── streaming.ts #
|
|
146
|
+
│ ├── streaming.ts # 流式回复卡构成(面板/活动行/正文/停止按钮/结算页脚)、分段、字节/表格预算
|
|
147
|
+
│ ├── picker.ts # /model、/effort、/permissions 选择卡与按钮回传解析
|
|
132
148
|
│ ├── onboarding.ts # 私聊首次触达引导卡
|
|
133
149
|
│ ├── approval.ts # 审批决策卡
|
|
134
150
|
│ └── question.ts # 结构化问答卡
|
|
@@ -148,10 +164,11 @@ src/
|
|
|
148
164
|
│ ├── delivery.ts # 出站投递:首帧话题卡、结果卡、分段降级、表情生命周期
|
|
149
165
|
│ ├── native-attachments.ts # 原生附件存储/降级:saveImage/saveFileStream、生效后端选择
|
|
150
166
|
│ ├── inbound-attachments.ts # 入站附件:不下载分类、图片预取、旧版工作区存档(archive)
|
|
151
|
-
│ ├── pending-attachments.ts # 静默登记:metadata-only 表、scope 隔离、TTL/容量
|
|
152
167
|
│ ├── manifest.ts # <feishu_context> 清单:只列未加载附件、去重、转义、行上限
|
|
153
168
|
│ ├── message-read.ts # 消息读取:get/mget/list、引用摘录、首轮历史、统一渲染
|
|
169
|
+
│ ├── slash-commands.ts # 连接后把命令注册表幂等同步到飞书原生斜杠指令面板
|
|
154
170
|
│ ├── tools.ts # feishu_attachment_fetch / feishu_topic_messages 与每 route 白名单
|
|
171
|
+
│ ├── send-file.ts # feishu_send_file:把话题工作目录内的图片/文件发回当前会话
|
|
155
172
|
│ └── agent-context.ts # agent 作用域 system prompt:native/archive 双模式附件指引
|
|
156
173
|
├── routing/
|
|
157
174
|
│ └── inbound.ts # 入站路由:去重、准入、路由决策树、建/续会话、首触引导
|
|
@@ -161,7 +178,6 @@ src/
|
|
|
161
178
|
├── stats.ts # 宿主投影优先的 token / 缓存 / 上下文折叠
|
|
162
179
|
├── domain.ts # storageDomain 域声明与路由记录 schema
|
|
163
180
|
├── health.ts # 健康快照与卡片
|
|
164
|
-
├── cleanup.ts # 过期话题目录 / 旧版工作区清理策略(纯逻辑,无 I/O)
|
|
165
181
|
├── session-export.ts # 逻辑日志导出
|
|
166
182
|
├── feishu-channel.ts # 官方 SDK Channel 适配(可注入 stub)
|
|
167
183
|
├── client/ # 浏览器半区:设置 →「飞书渠道」页面(构建到 lib/client.js)
|
|
@@ -172,7 +188,7 @@ src/
|
|
|
172
188
|
│ │ ├── connection-tab.tsx # 连接:三步引导 / 连接信息 / 待生效提醒 / 凭据卡
|
|
173
189
|
│ │ ├── credentials-card.tsx # App ID / App Secret 编辑(不回显)
|
|
174
190
|
│ │ ├── sessions-tab.tsx / model-tab.tsx / workspace-tab.tsx / access-tab.tsx / advanced-tab.tsx
|
|
175
|
-
│ │ └──
|
|
191
|
+
│ │ └── access-card.tsx
|
|
176
192
|
│ ├── catalog.ts # 选择器目录折叠:Host 字段 + 实时目录 → 表单字段
|
|
177
193
|
│ ├── api.ts # 同源 /dsh-channels/* 请求封装
|
|
178
194
|
│ └── styles.ts # 语义 token 样式(注入一次)
|
|
@@ -231,7 +247,7 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
231
247
|
routeTtlMs: 2592000000 # 话题路由 30 天无活动即过期;后续消息明确拒绝,不新建 Session(0 = 关闭)
|
|
232
248
|
approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟;超时自动拒绝)
|
|
233
249
|
questionTimeoutMs: 300000 # ask_user_question 问答卡等待时长(默认 5 分钟;超时本轮提问失败)
|
|
234
|
-
acceptAttachments: true # 接收飞书图片/文件/合并转发记录(native
|
|
250
|
+
acceptAttachments: true # 接收飞书图片/文件/合并转发记录(native 下图片预取、文件懒取、纯附件静默不回复;false 时附件消息完全惰性)
|
|
235
251
|
attachmentStore: 'native' # 入站附件存储后端:native(DSH 原生附件库,默认)/ archive(旧版工作区 attachments/)
|
|
236
252
|
prefetchImages: true # 触发消息图片预取为首轮 ImageBlock(native 生效;关闭后图片也需工具懒取)
|
|
237
253
|
prefetchImageMaxBytes: 20971520 # 单张预取图片上限(20 MiB)
|
|
@@ -239,8 +255,6 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
239
255
|
prefetchTimeoutMs: 10000 # 图片预取整段超时(10 s;到期保留已取回图片,其余降级清单)
|
|
240
256
|
historyBootstrapLimit: 10 # 新 Session 首轮附带本话题最近 N 条消息摘要(0 关闭,上限 20)
|
|
241
257
|
promptContextTimeoutMs: 3000 # 被动引用摘录 / 首轮历史的整段读取预算(3 s;超时静默省略)
|
|
242
|
-
pendingAttachmentTtlMs: 604800000 # 静默登记元数据保留期(7 天;0 = 不按时间清理)
|
|
243
|
-
pendingAttachmentLimit: 10 # 静默登记每 (会话,话题,发送者) scope 容量上限
|
|
244
258
|
feishuTools: # 模型工具(Web 面板按子键合并,不会把未改子键写空)
|
|
245
259
|
attachmentFetch: true # feishu_attachment_fetch:按需懒取已知消息的图片/文件(关闭即整体回退 archive)
|
|
246
260
|
attachmentFetchMaxResources: 10 # 单次最多取回资源数(1–50)
|
|
@@ -248,15 +262,14 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
248
262
|
attachmentFetchTimeoutMs: 120000 # 单次工具调用整段超时(120 s)
|
|
249
263
|
topicMessages: true # feishu_topic_messages:读取引用消息 / 话题历史并授权其附件
|
|
250
264
|
topicMessagesTimeoutMs: 20000 # 单次话题读取整段超时(20 s;被动摘录用独立 3 s 预算)
|
|
251
|
-
|
|
252
|
-
workspaceRetentionAuto: false # 每日自动执行一次清理(默认关;删除不可恢复;用户工作区永不删除)
|
|
265
|
+
sendFile: true # feishu_send_file:把话题工作目录内生成的图片/文件发给用户(需 im:resource)
|
|
253
266
|
welcomeOnboarding: true # 私聊首次触达发一张引导卡(按 chat 持久化去重)
|
|
267
|
+
slashCommandPanel: false # 连接时把命令同步到飞书原生 / 面板(需 app_slash_command 读写权限;改后需重连)
|
|
254
268
|
allowPermissionSwitch: false # 是否允许 /permissions <name> 从飞书侧切换权限预设(安全敏感)
|
|
255
269
|
accessMode: 'guarded' # 准入模式:guarded(默认,仅管理员+白名单)| open(旧行为,不安全)
|
|
256
|
-
|
|
270
|
+
adminUserId: 'a1b2c3d4' # 管理员 user_id(可留空,留空时自动使用应用所有者;都没有时 guarded 下拒绝所有对话;面板可改)
|
|
257
271
|
authRequestTtlDays: 7 # 入群/用户授权申请卡有效期(天,1–30)
|
|
258
272
|
loggerLevel: 'info'
|
|
259
|
-
# settingsFile: '/custom/path/settings.json' # 可选:Web 面板覆盖文件路径(默认 $DSH_HOME/dsh-channels/settings.json)
|
|
260
273
|
```
|
|
261
274
|
|
|
262
275
|
`reasoningEffort` 是每个 Session 的初始/重置默认值,并在新建、恢复或借用 Agent 的作用域内以 `prepend` 外层覆盖 `agent/request`,因此胜过已存在的模型选择与 provider 默认;借用外部 Agent 时,插件关闭只移除这条频道策略,不销毁外部 Agent。话题所有者可用 `/effort <档位-id>` 覆盖当前 Session,`/effort` 查看生效来源,`/effort reset` 恢复该配置值(未配置时恢复模型适配器/provider 默认)。成功选择由 Session 自带的 `command/run` / `command/done` 日志恢复,重启后仍生效;新建 Session 不继承旧 Session 的覆盖。
|
|
@@ -290,16 +303,20 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
290
303
|
2. 权限(至少):
|
|
291
304
|
- `im:message`(获取与发送单聊、群组消息;附件资源下载 `messageResource.get` 也走消息资源接口,具体 scope 以控制台该接口要求为准)
|
|
292
305
|
- `im:message.p2p_msg:readonly`(读取用户发给机器人的单聊消息)
|
|
306
|
+
- `contact:user.employee_id:readonly`(获取用户 user ID,**必需**):插件用 `user_id` 识别用户(准入、话题归属、工作区目录)。未开通时事件不带 `user_id`,所有用户消息都会被丢弃并打印 warn;外部联系人/跨租户用户没有本租户 `user_id`,同样无法使用
|
|
293
307
|
- `im:message:readonly`(读取消息内容:引用摘录、话题历史、`feishu_topic_messages`)
|
|
294
308
|
- `im:message.group_msg`(读取群组消息,**敏感权限**,需企业管理员审批):读取群/话题历史(含机器人入群前的可见历史)。未开通时引用/历史块静默省略、普通对话不受影响,按“机器人可见范围”降级
|
|
295
309
|
- 事件:**接收消息 im.message.receive_v1**(含“接收群聊中 @ 机器人消息”)
|
|
296
310
|
- 事件:**机器人进群 im.chat.member.bot.added_v1**(入群后私聊管理员发群信息卡);对应的群信息读取权限(如 `im:chat:readonly`,以控制台要求为准)
|
|
297
311
|
- 私聊需要能主动给管理员发消息(`im:message` 已覆盖;管理员需在应用可用范围内)
|
|
298
|
-
-
|
|
312
|
+
- 「获取应用信息」权限(**可选**,如 `admin:app.info:readonly` / `application:application:self_manage`,以控制台要求为准):未配置 `adminUserId` 时,插件在每次连接时调用 `application/v6/applications/:app_id` 查询应用所有者并让其成为管理员;调不通(企业内不便申请该权限等)时日志与面板会给出飞书返回的错误码,并提示在「访问授权」手动填写管理员 user_id,填写后不再依赖它
|
|
313
|
+
- 回调:**卡片回传交互 card.action.trigger**(流式卡停止按钮、`/model` 等选择卡、HITL 审批 / `ask_user_question` 问答按钮的回传事件,同样走长连接)
|
|
314
|
+
- `im:resource`(获取与上传图片或文件资源,**可选**;`im:resource:upload` 亦可):`feishu_send_file` 上传图片/文件后发给用户。未开通时该工具返回可读的 `PERMISSION_DENIED`;关闭 `feishuTools.sendFile` 后自检不再检查此项
|
|
299
315
|
- `im:message.reactions:write_only`(发送、删除消息表情回复)
|
|
300
316
|
- `im:message:update`(更新卡片)
|
|
301
317
|
|
|
302
318
|
> 消息读取权限缺失时不会报死错:引用摘录与首轮话题历史静默省略(插件生命周期内首次降级输出一条 warn,之后 debug 级),显式调用 `feishu_topic_messages` 返回可读的 `PERMISSION_DENIED`。附件下载仍依赖 `im:message` 一类的消息资源权限;下载被拒时工具与 archive 路径都会返回可读原因。
|
|
319
|
+
> **连接后权限自检**:每次连上飞书后,插件调用 `application/v6/scopes` 读取应用的租户权限授予状态,与上面的清单对比(私聊关闭时不检查 `im:message.p2p_msg:readonly`,`requireMention: false` 时 `im:message.group_msg` 升为必需;`feishuTools.sendFile` 关闭时不检查 `im:resource`;未配置管理员且应用所有者查询被飞书拒绝时追加可选项「获取应用信息」)。缺失项在 Web 设置「连接」页以红字列出(标注必需/可选与用途,每项附开发者后台权限页深链,另有一键打开全部缺失项的链接),并在日志打印一条 warn;读取失败时显示飞书返回的原因。自检只做提示,不阻断连接;开通权限后需发布新版本并重新连接才会刷新。
|
|
303
320
|
3. 事件订阅方式选**长连接**(WebSocket,免公网)。
|
|
304
321
|
4. 发布版本并把用户加入可用范围;将机器人拉入测试群。
|
|
305
322
|
|
|
@@ -309,41 +326,41 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
309
326
|
|
|
310
327
|
| 场景 | 工作区目录(header cwd) | 每个话题的工作目录(实际 cwd) |
|
|
311
328
|
|---|---|---|
|
|
312
|
-
| 私聊 | `<baseDir>/p2p/<
|
|
313
|
-
| 群聊 | `<baseDir>/groups/<chat_id>/<
|
|
329
|
+
| 私聊 | `<baseDir>/p2p/<user_id>/` | `<工作区>/topics/<话题 id>/` |
|
|
330
|
+
| 群聊 | `<baseDir>/groups/<chat_id>/<user_id>/` | 同上 |
|
|
314
331
|
|
|
315
332
|
- 群聊必须是「群 × 用户」:同一个人在不同群之间不共享文件,避免 A 群会话把 B 群内容带出来。
|
|
316
333
|
- 会话创建时以工作区为 header cwd(保证 GUI 里归属该项目、沙箱边界稳定),随后通过 `ctx.workingDirectory` 把实际工作目录切到话题目录;冷恢复会从 Session 日志还原该目录。因此话题之间的文件互不干扰,而工作区数量只随用户增长。
|
|
317
334
|
- `topicIsolation` 控制隔离形式:`auto`(默认)在用户工作区是 Git 仓库(含 `.git`)时给每个话题建一个 `feishu-<话题>` 分支 + worktree(适合多个话题并发改同一个代码仓库),否则用普通子目录;`subdir` 一律子目录;`worktree` 一律尝试 worktree,失败告警并回退子目录。
|
|
318
335
|
- 隔离强度:这是**默认工作目录层面**的隔离(bash / fs / 终端都从话题目录出发),不是防恶意的硬沙箱——沙箱写边界仍是用户工作区。若需要硬边界,可把话题目录直接作为 Session cwd,但会话会失去工作区归属(GUI 显示为未分组)。
|
|
319
|
-
- **入站附件(v2)**:native 后端下图片/文件存入 DSH 原生附件库,**不写入话题目录**,因此工作区与话题目录只承载 Agent 产物;仅显式 `archive` 后端(或宿主缺原生附件服务)才写当前话题目录的 `attachments/`,话题建立前先暂存 `<baseDir>/_staging/...`。v2 主路径不再写 `_staging`;旧版遗留的暂存文件仅保留一次性迁入,按 D12 在 v2 发布后的下一个 minor
|
|
320
|
-
- worktree
|
|
321
|
-
- 旧版本「一个话题一个工作区」(`<baseDir>/topics
|
|
336
|
+
- **入站附件(v2)**:native 后端下图片/文件存入 DSH 原生附件库,**不写入话题目录**,因此工作区与话题目录只承载 Agent 产物;仅显式 `archive` 后端(或宿主缺原生附件服务)才写当前话题目录的 `attachments/`,话题建立前先暂存 `<baseDir>/_staging/...`。v2 主路径不再写 `_staging`;旧版遗留的暂存文件仅保留一次性迁入,按 D12 在 v2 发布后的下一个 minor 版本移除。`/renew` 复用同一话题目录。
|
|
337
|
+
- **插件不删除任何工作区或话题目录**。需要回收磁盘时手动删除不再需要的话题目录即可:该话题下次对话时会重建一个空目录继续使用(worktree 模式下改建新的 worktree 分支)。手动删除 worktree checkout 后,可在仓库里 `git worktree prune` / `git branch -D` 回收 Git 记录。
|
|
338
|
+
- 旧版本「一个话题一个工作区」(`<baseDir>/topics/...`)不会自动迁移:旧话题继续用原目录;旧版「用户 × 聊天」共享工作区正好作为新的用户工作区复用。
|
|
322
339
|
- 设计取舍与约束详见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md)。
|
|
323
340
|
|
|
324
341
|
## 行为与降级
|
|
325
342
|
|
|
326
|
-
- **入站附件(v2:零等待 + 清单 + 图片预取 + 文件懒取)**:收到触发消息时先建会话 / 话题 / 卡片第一帧并持久化路由,**不在建立前下载任何附件字节**。随后组装的 prompt 依次为:说明文字 → `<feishu_quote>` 引用摘录 → 预取成功的 `ImageBlock` → `<feishu_context>` 未加载附件清单 → (首轮)`<feishu_history>` 话题历史;空块不产生 token。触发消息中的图片在路由建立后、`followup` 前**预取**进首轮(单图 `prefetchImageMaxBytes`、总量 `prefetchTotalMaxBytes`、整段 `prefetchTimeoutMs`,失败/超限/超时降级为清单项);文件**默认不下载**,清单只给 `message_id / file_key / 类型 / 文件名`,模型需要时调用 `feishu_attachment_fetch` 懒取(图片回 `ImageBlock`,文件流式入库 + `FileBlock` + 只读路径兜底)。富文本(post)消息 `files[]` 里 SDK
|
|
343
|
+
- **入站附件(v2:零等待 + 清单 + 图片预取 + 文件懒取)**:收到触发消息时先建会话 / 话题 / 卡片第一帧并持久化路由,**不在建立前下载任何附件字节**。随后组装的 prompt 依次为:说明文字 → `<feishu_quote>` 引用摘录 → 预取成功的 `ImageBlock` → `<feishu_context>` 未加载附件清单 → (首轮)`<feishu_history>` 话题历史;空块不产生 token。触发消息中的图片在路由建立后、`followup` 前**预取**进首轮(单图 `prefetchImageMaxBytes`、总量 `prefetchTotalMaxBytes`、整段 `prefetchTimeoutMs`,失败/超限/超时降级为清单项);文件**默认不下载**,清单只给 `message_id / file_key / 类型 / 文件名`,模型需要时调用 `feishu_attachment_fetch` 懒取(图片回 `ImageBlock`,文件流式入库 + `FileBlock` + 只读路径兜底)。富文本(post)消息 `files[]` 里 SDK 归一化会丢的文件也会被恢复并进入清单/预取。**只发附件、不带说明的消息完全静默**:不回复、不建会话、不下载、不加表情,本地不写任何记录。插件不扫描历史去找这类消息:用户需要引用那条消息再提问(多个文件先合并转发再引用),被引用消息的附件进入清单与 fetch 白名单;话题内的附件也可由模型调用 `feishu_topic_messages` 读到并获得授权。
|
|
327
344
|
- **引用与话题历史**:触发消息的 `parent_id` 解析为 `<feishu_quote>` 摘录(单条 ≤400 字符、整块 ≤4k,合并转发展开子消息并标注容器 id),被引用消息的附件同时进入清单与 fetch 白名单;新 Session 首轮自动带最近 `historyBootstrapLimit` 条(默认 10、上限 20、0 关闭)的 `<feishu_history>`(单条 ≤400 字符、整块 ≤8k,排除触发消息与卡片)。更早的消息用 `feishu_topic_messages` 按 `limit`(默认 10、上限 50)与 `cursor` 翻页;读到的消息与附件即获得 fetch 授权。被动读取使用独立的 `promptContextTimeoutMs`(默认 3 s)预算,缺权限/超时/失败时对应块静默省略,回合照常运行(首次降级一条 warn)。
|
|
328
|
-
- **入站附件存储后端与降级矩阵**:`attachmentStore: 'native'`(默认)且宿主挂载 `ctx.attachments`、`feishuTools.attachmentFetch=true`、`acceptAttachments=true` 时走 native 懒取;以下任一条件成立即整体回退旧版 archive(同步下载到工作区 `attachments/`、纯文本 prompt、无清单、不挂 fetch 工具):显式 `attachmentStore: 'archive'`;宿主未挂载原生附件库(一次性 warn);`feishuTools.attachmentFetch=false`(文件无法懒取)。`acceptAttachments=false` 时走 archive
|
|
329
|
-
-
|
|
345
|
+
- **入站附件存储后端与降级矩阵**:`attachmentStore: 'native'`(默认)且宿主挂载 `ctx.attachments`、`feishuTools.attachmentFetch=true`、`acceptAttachments=true` 时走 native 懒取;以下任一条件成立即整体回退旧版 archive(同步下载到工作区 `attachments/`、纯文本 prompt、无清单、不挂 fetch 工具):显式 `attachmentStore: 'archive'`;宿主未挂载原生附件库(一次性 warn);`feishuTools.attachmentFetch=false`(文件无法懒取)。`acceptAttachments=false` 时走 archive 的惰性分支:附件消息完全不保存、不触发、不列入清单,混合消息只保留剥离占位符后的文字。`feishu_topic_messages` 两个后端都挂载(archive 下描述会标注附件不可取)。archive 保留旧实现细节:文件流式落盘且文件名前缀 `YYYYMMDD-HHmmss-`,合并转发记录写 `*-forwarded-record.txt`,成功加 `DONE`、失败加 `No` 并回一条纯文字失败提示,>100 MB 文件先探测大小再按 32 MiB `Range` 分块下载;该路径会在会话建立前下载,属于降级而非默认体验。
|
|
346
|
+
- **附件与目录保留**:DSH 原生附件库**永不自动清理,插件也不提供清理入口**(D6;磁盘增长用户已接受)。插件同样不删除工作区、话题目录、`_staging` 与 Session 日志;archive 后端的 `attachments/` 副本位于话题目录,随运维手动删除目录一起删除。v2 主路径不再写 `_staging`,仅对旧版遗留文件做一次性迁入(成功时一条 warn),按 D12 在 v2 发布后的下一个 minor 版本移除。切换到 native 后,历史话题里的旧 archive 附件目录不会被新会话枚举,但 Session 历史仍可按原路径读取。
|
|
330
347
|
- **三类会话行为矩阵(v2)**:
|
|
331
348
|
|
|
332
349
|
| 场景 | 私聊 | 对话群(未 @) | 对话群(@) | 已绑定话题(无 @) |
|
|
333
350
|
| --- | --- | --- | --- | --- |
|
|
334
|
-
| 纯附件消息 |
|
|
335
|
-
| 文字 + 附件 | 建会话;图片预取 / 文件清单 |
|
|
351
|
+
| 纯附件消息 | 静默(不建会话/不回复/不下载;下一轮列入清单) | 静默(后续 @ 建话题时列入清单) | 静默(纯附件不触发) | 静默(下一轮续聊时列入清单) |
|
|
352
|
+
| 文字 + 附件 | 建会话;图片预取 / 文件清单 | 不触发;后续 @ 建话题时列入清单 | 建会话;图片预取 / 文件清单 | 续聊;图片预取 / 文件清单 |
|
|
336
353
|
| 纯文字 | 建会话 | 不触发 | 建会话 | 续聊同一 Session |
|
|
337
354
|
- **DSH 0.2.0 兼容**:会话日志读取改用 `Session.snapshotEvents()`(整段 `Session.events` getter 已被 DSH 删除);正文/推理增量改用瞬时事件 `agent/assistant-stream`(持久化 `assistant/chunk` 已不再产生,改为把流内嵌进 `assistant/message.stream`),并处理 `abandoned` 尝试的文本回滚;卡片 token / 缓存命中 / 上下文窗口优先读宿主 `sessionProjections`(`sessionStats` / `tokenUsage` / `contextPressure`),未挂载时回退到本地增量折叠 + `llm.resolveModelInfo`。完整评估、证据与实施状态见 [docs/review-dsh-0.2.0.md](docs/review-dsh-0.2.0.md)。
|
|
338
355
|
- **话题创建**:通过回复消息接口 `reply_in_thread: true`(官方文档明示的“以话题方式回复(即创建话题)”)。响应后经 `im.v1.message.get` 取回 `thread_id` 写入路由表。
|
|
339
356
|
- **话题延续**:`im.message.receive_v1` 事件自带 `thread_id`,命中路由表即 `agent.followup()`,无需 @。同一 (chat, user) 的**并发首消息会被串行化**:后到的消息等待在途的会话建立完成,再按串行语义续聊同一 session 或另起新话题——不会重复建 session、覆盖路由,也不会落入“无路由、无 @”的丢弃窗口。
|
|
340
|
-
- **降级(`threadFallback: 'degrade'`)**:私聊或群聊拒绝话题回复(如 230071)时,自动改为普通回复,路由键退化为 `chat:p2p:<chat_id>:<
|
|
357
|
+
- **降级(`threadFallback: 'degrade'`)**:私聊或群聊拒绝话题回复(如 230071)时,自动改为普通回复,路由键退化为 `chat:p2p:<chat_id>:<user_id>` / `chat:group:<chat_id>:<user_id>`(会话级延续);`strict` 则直接失败。
|
|
341
358
|
- **重启恢复**:路由表持久化在 `storageDomain`;消息到达时 `ctx.agents.resume()` 懒恢复 session。若会话已被其他 owner 恢复为 live(如 Web UI 打开过该会话、并发冷恢复竞态),插件**借用**该 live agent 直接 `followup()`,不再报 "cannot prepare session … while it is live"。借用的 agent 不归插件 dispose;shutdown 与在途 resume 竞态时,刚发布的 agent 会被立即 dispose 以避免泄漏进下一实例的 live 注册表。
|
|
342
|
-
- **重启修复(流式中断)**:每条正在流式的 turn 会把自己的修复凭据(trigger 消息、Typing reaction id、全部段卡 id)写入 `
|
|
359
|
+
- **重启修复(流式中断)**:每条正在流式的 turn 会把自己的修复凭据(trigger 消息、Typing reaction id、全部段卡 id)写入 `channels_routes_v2` 域的 `inflight` 表(键 = 主卡 message id,turn 结束即删)。重启后长连接一建立,插件就把上一实例留下的中途卡片 patch 成「⚠️ 服务已重启,本次回复中断,请重新发送消息。」终态并补 DONE 表情;patch 失败重试一次(上限 2 次)后放弃。持久化盘点与缺口清单见 [docs/persistence.md](docs/persistence.md)。
|
|
343
360
|
- **消息去重(WS 重投幂等)**:飞书长连接重连会重投未 ack 的事件;每条消息在任何副作用之前先落一条 `processed` 记录(键 = message id,飞书全局唯一),重投消息被直接丢弃——同进程靠内存守卫、跨重启靠持久化记录,每条消息至多处理一次。记录保留 24 小时后由 GC 清扫。
|
|
344
|
-
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner/工作区路径/会话目录/最后活动时间;旧 DSH Session
|
|
345
|
-
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **
|
|
346
|
-
- **HITL 审批**:插件以 `prepend: true` 注册为宿主 `approval/request` 瀑布的应答者——本频道路由的 session 的审批请求**优先于** Web 网关的通用应答(其面板对飞书用户不可见),非本频道的请求原样 `next()` 交给其他应答者。审批卡是**独立消息**(锚定到该 session 最新流式卡、同话题),绝不嵌入流式卡(其全量替换 patch
|
|
361
|
+
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner/工作区路径/会话目录/最后活动时间;旧 DSH Session、日志与话题目录都不删除。降级的 chat-scoped 路由没有独立话题边界,仍直接清理。陈年 in-flight 快照同时在启动时及每 6 小时清扫。`routeTtlMs: 0` 关闭后续路由过期处理,已产生的墓碑仍保持拒绝语义。
|
|
362
|
+
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **user_id 在全局用户白名单**,两者缺一不可。未授权消息不建 Session、不建话题目录、不落附件、不调模型;群里静默、私聊回固定文案。机器人被拉进任意群时(订阅 `im.chat.member.bot.added_v1`),插件私聊管理员发群信息卡(群名/`chat_id`/成员数/邀请人),未授权群附「授权本群 / 忽略」按钮;已授权群里未授权用户 @ 机器人时,管理员会收到用户授权申请卡(含昵称/user_id)。授权状态持久化在 `channels_routes_v2` 域的 `access_groups` / `access_users` / `access_requests` 表,撤销立即生效。管理员 user_id 在 Web 面板配置,留空时为飞书应用所有者(连接时查询,需可选的「获取应用信息」权限;查询失败则 fail closed,手动填写管理员即可);`/auth` 命令、授权卡、面板三种入口等价。
|
|
363
|
+
- **HITL 审批**:插件以 `prepend: true` 注册为宿主 `approval/request` 瀑布的应答者——本频道路由的 session 的审批请求**优先于** Web 网关的通用应答(其面板对飞书用户不可见),非本频道的请求原样 `next()` 交给其他应答者。审批卡是**独立消息**(锚定到该 session 最新流式卡、同话题),绝不嵌入流式卡(其全量替换 patch 会冲掉按钮)。宿主请求不带工具参数,插件在 `session/event` 中按会话记录**当前回合**的 `tool/call`(回合结束即清空,不回读历史),按 `callId` 取出命令显示在卡上(超过 600 字截断并注明)。决策仅在会话发起人操作时生效:允许 → `allowed-once`;本话题不再询问 → `allowed-once`,并在内存里记下「工具名 + 提权目标」,该 session 之后的同类请求直接 `allowed-once`、不再发卡,同 session 里其他被覆盖的待审批卡一并允许(`/renew` 换 session 或重启后失效,提升到 `danger-full-access` 时按钮标红);拒绝 → `rejected`;文字回复中「允许 / 同意 / 好 / y」等、「拒绝 / 不行 / n」等、「全部允许」等关键词作用于该 session 最新一张待审批卡,其他文字把该 session 所有待审批卡按 `rejected` 结算后照常作为下一条消息交给 Agent;超时 → `rejected`(fail-closed,卡面显示「已超时」)、请求被撤回(abort)→ `cancelled`、卡片发送失败 → `unavailable`(fail-closed);每次决策后卡片更新为终态(注明按钮 / 文字回复 / 不再询问 / 被新消息取代)并移除按钮。审批记录(`approval/asked`/`decided`)由宿主会话日志负责,插件不做持久化——重启后遗留问题随 turn 消亡,与 Web 网关一致。需要飞书应用订阅 **card.action.trigger** 回调(见「飞书侧准备」);启动日志 `HITL 审批应答已就绪` 表示生效,`主机未挂载 approval 服务` 表示该部署未启用 HITL。
|
|
347
364
|
- **终态与表情**:成功轮的面板标题变为「回复结束」(无推理内容时不渲染面板),并把 `Typing` 换成 `DONE`;`turn/end.kind = error` 时面板标题为「回复失败」(同样仅当有推理内容),错误只在正文出现一次(`❌ **本轮运行失败**` + 结构化错误消息与错误码),并把 `Typing` 换成 `No`。模型/推理强度/本轮缓存样本不会继承上一轮。
|
|
348
365
|
- **限频**:卡片更新单条消息 5 QPS、应用级 1000 次/分钟;500ms(2 QPS)默认安全,`refreshMs` 下限 200ms;活动状态行的保活心跳默认 5s(`statusRefreshMs` 下限 1s),远低于限频。
|
|
349
366
|
- **卡片体积**:预算常量收敛为三个——`CARD_TARGET_BYTES`(28 KB,分段目标)、`CARD_MAX_BYTES`(30 KB,飞书硬上限,仅作 `buildCard` 的绝对兜底)、`REASONING_MAX_BYTES`(5 KB,推理面板内容窗口);**不再有面板预留**(原 5.5 KB 的 `CARD_PANEL_RESERVE_BYTES` 已删除),分段改为按整卡真实渲染测量:主卡探测面板最大窗口 + 完成态页脚,续卡无面板/页脚、可用满 28 KB(约 27.8 K ASCII 字符,100 KB ASCII 正文约 4 张卡)。正文超长自动分段为多张续卡,断点统一避开代码围栏与表格(段落/标题/行断点;单个代码块/表格小于单卡预算时整体换卡,超过单卡预算时只能硬切、内容不丢;每卡 ≤ 5 个表格,全文保留)。推理面板超出 5 KB 时保留头部并追加「…(内容过长,已截断)」(可见窗口约 4.96 K ASCII 字 / 1.65 K 中文字 / 1.24 K emoji),面板内表格省略;窗口长度与正文长短无关,不占用也不被正文挤占。
|
|
@@ -353,10 +370,11 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
353
370
|
|
|
354
371
|
- **v2 附件(图片预取的等待窗口)**:卡片、话题与会话先建立,但首轮 `followup` 会等图片预取(默认整段 ≤10 s,`prefetchTimeoutMs` 可调);这是“首轮图片直接可见”与首字延迟之间的取舍,慢网下可调小或关闭 `prefetchImages`。不做后台预取/预下载。
|
|
355
372
|
- **v2 附件(文件懒取依赖模型调用)**:文件默认不在 prompt 里,模型必须主动调用 `feishu_attachment_fetch`;插件用清单“未加载”措辞、工具描述与 guidance 收敛漏调风险,但不做预下载兜底(决定 D4)。
|
|
356
|
-
- **v2 附件(授权白名单进程内)**:每 route 的已知消息 LRU 上限 128
|
|
357
|
-
- **v2 附件(清理与保留)**:DSH
|
|
373
|
+
- **v2 附件(授权白名单进程内)**:每 route 的已知消息 LRU 上限 128,进程重启后白名单清空;本人在本话题(会话级路由为聊天主线)发的消息由 fetch 工具回读飞书校验后仍可授权;话题之间不互通;他人的引用/历史消息需要重新出现在引用或历史中才可取。附件密集翻页仍可能挤掉更早条目。
|
|
374
|
+
- **v2 附件(清理与保留)**:DSH 原生附件库不可自动清理、插件不提供清理入口,磁盘随使用增长;插件也不删除工作区与话题目录,需要时由运维手动删除。旧 `_staging` 仅一次性迁入兼容(成功时 warn),按 D12 在 v2 发布后的下一个 minor 版本移除。
|
|
375
|
+
- **v2 附件(未触发的附件需要引用)**:只发附件、不带文字的消息不登记、也不在下一轮自动列出;“先发文件、再单独提问”而不引用时模型看不到该文件。私聊与普通群需要引用那条消息(多个文件可合并转发后引用);话题内模型还可以用 `feishu_topic_messages` 读到。首次私聊引导卡写明了这一用法。
|
|
358
376
|
- **v2 附件(权限降级)**:缺 `im:message:readonly` / `im:message.group_msg` 时引用摘录与首轮历史静默省略(首次一条 warn),显式调用 `feishu_topic_messages` 返回可读 `PERMISSION_DENIED`;普通对话不受影响。入群前历史只保证“机器人可见范围”。
|
|
359
|
-
- **v2 附件(升级连续性)**:旧 archive 部署切到 native 后,历史话题里已有的 `attachments/` 不会被新会话枚举;Session
|
|
377
|
+
- **v2 附件(升级连续性)**:旧 archive 部署切到 native 后,历史话题里已有的 `attachments/` 不会被新会话枚举;Session 历史仍可按原路径读取,旧目录可手动删除。
|
|
360
378
|
- **`/renew` 的会话语义**:同一话题内新建 Session 沿用话题目录、复用附件;但新 Session 首轮不注入 `<feishu_history>`(仅 `startSession` 路径注入首轮历史,当前语义保留,若产品期望“新会话即带话题历史”需另行确认)。
|
|
361
379
|
- **degraded 群会话的读取容器**:话题创建失败降级为 chat 路由后,其容器是整群主时间线——同群他人主时间线消息可被读取/登记(topic 消息已明确拒绝);是否按 sender 进一步收紧待产品确认。
|
|
362
380
|
- **合并转发读取有界**:单条读取子消息超过 50 条时截断(summary 标注数量)、无翻页;工具输出渲染上限 50 行 / 12k 字符。
|
|
@@ -386,4 +404,4 @@ pnpm verify:client # client loader 冒烟
|
|
|
386
404
|
pnpm check # 以上全部
|
|
387
405
|
```
|
|
388
406
|
|
|
389
|
-
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。附件管线相关测试:`tests/native-attachments.test.ts`(存储层与 archive 降级)、`tests/
|
|
407
|
+
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。附件管线相关测试:`tests/native-attachments.test.ts`(存储层与 archive 降级)、`tests/feishu-tools.test.ts`(两个工具的白名单 / 上限 / 取消)、`tests/manifest.test.ts`(清单格式与转义)、`tests/inbound-image-prefetch.test.ts`(预取降级)、`tests/message-read.test.ts`(引用 / 历史读取);接入链路由 `tests/service.test.ts` / `tests/inbound-attachments.test.ts` 覆盖。`tests/settings.test.ts` 覆盖:覆盖层 schema/层级、面板视图模型、连接阶段(待配置/未连接/已连接/失败)、凭据保存按就绪度连接、HTTP 路由(含同源写保护)、`apply()` 的 webServer 接线、模型选择器的宿主目录与降级行为;`tests/client-catalog.test.ts` 覆盖浏览器半区的目录折叠(含依赖项解禁回归);以及 `FeishuService` 的覆盖加载 / 热更新 / 重启 / 连接失败记录 / 监听器释放。
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# dsh-channels 准入与授权(Access Control)设计
|
|
2
2
|
|
|
3
3
|
> 状态:已实现(P1 + 入群群信息卡/用户授权卡;P3 增强项仍待做)。
|
|
4
|
+
>
|
|
5
|
+
> 用户标识已从 open_id 改为 `user_id`:下文的 open_id / `ou_...` / `adminOpenId` 现在分别对应 `user_id` / `adminUserId`,存储域改为 `channels_routes_v2`,旧数据不迁移(见 README「飞书侧准备」的 `contact:user.employee_id:readonly` 权限)。
|
|
4
6
|
> 目标:唯一管理员(Web 面板配置 open_id)**不受白名单限制**,在任意私聊/群聊(含未授权群)都能直接发起会话;**非管理员**需要「群已授权 + 用户在白名单」两层同时命中。机器人可以被拉进任意群,但非管理员未授权时不做任何回复、不产生任何会话。
|
|
5
7
|
|
|
6
8
|
## 0. 一句话方案
|
|
@@ -320,8 +322,8 @@ bot 回显过滤 / chatType / 空内容 / dmMode
|
|
|
320
322
|
|
|
321
323
|
```ts
|
|
322
324
|
{
|
|
323
|
-
name: 'auth', group: '会话', description: '
|
|
324
|
-
usage: '[status|allow|revoke|list|user add
|
|
325
|
+
name: 'auth', group: '会话', description: '查看/管理访问授权,可 @ 成员授权(仅管理员)',
|
|
326
|
+
usage: '[status|allow|revoke|list|user add <@用户|user_id>|user remove <@用户|user_id>]',
|
|
325
327
|
channelPlane: true, agentScoped: false,
|
|
326
328
|
}
|
|
327
329
|
```
|
|
@@ -331,7 +333,7 @@ bot 回显过滤 / chatType / 空内容 / dmMode
|
|
|
331
333
|
| `/auth` / `/auth status` | 任意 | 显示:当前是否管理员、本群/本私聊是否已授权、白名单数量 |
|
|
332
334
|
| `/auth allow` | 群聊(管理员) | 把**当前群**加入群白名单,回执“已授权本群” |
|
|
333
335
|
| `/auth revoke` | 群聊(管理员) | 把当前群移出白名单,立即生效 |
|
|
334
|
-
| `/auth user add
|
|
336
|
+
| `/auth user add <@用户\|user_id>` | 私聊/群聊(管理员) | 加入用户白名单;`@成员` 取 mention 里的 user_id,忽略对机器人的 @,没有 user_id 的外部用户被拒绝 |
|
|
335
337
|
| `/auth user remove <open_id>` | 同上 | 移出用户白名单 |
|
|
336
338
|
| `/auth list` | 任意(管理员) | 列出群白名单、用户白名单与待处理申请 |
|
|
337
339
|
|
|
@@ -524,7 +526,7 @@ UI:
|
|
|
524
526
|
### P3:增强
|
|
525
527
|
|
|
526
528
|
- [ ] 审计表 + 面板最近记录
|
|
527
|
-
- [
|
|
529
|
+
- [x] `/auth user add @成员`(用 mention 里的 user_id)
|
|
528
530
|
- [ ] 逐群用户授权(`access_users.chatId` 可选作用域)
|
|
529
531
|
- [ ] bot removed / 群解散的授权回收(评估直接订阅原始事件)
|
|
530
532
|
- [ ] health/status 暴露 `accessMode`、`adminConfigured`、pending 数
|
|
@@ -547,4 +549,4 @@ UI:
|
|
|
547
549
|
- 管理员未配置时收到入群事件只记日志,不落申请记录(面板看不到);
|
|
548
550
|
- 面板结算的 grant `source` 记为 `card`(与飞书卡片共用结算函数),若要区分来源可加参数;
|
|
549
551
|
- 授权申请过期目前只在启动时清扫一次;运行中依赖结算时判断;
|
|
550
|
-
-
|
|
552
|
+
- 逐群用户授权、bot removed 自动回收、审计表与面板最近记录等按 §10 P3 处理。
|