@modesto_ma/dsh-channels 0.1.0 → 0.3.0
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 +89 -55
- package/docs/access-control-design.md +5 -5
- package/docs/feasibility-and-design.md +54 -26
- package/docs/feishu-onboarding.md +26 -20
- package/docs/persistence.md +4 -4
- package/docs/refactor-plan.md +6 -5
- package/docs/workspace-isolation-design.md +78 -0
- package/lib/client.js +1715 -882
- package/lib/client.js.map +1 -1
- package/lib/index.js +1556 -680
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +23 -0
- package/src/cards/primitives.ts +20 -0
- package/src/cards/streaming.ts +310 -291
- package/src/cleanup.ts +256 -119
- package/src/client/api.ts +20 -0
- package/src/client/catalog.ts +1 -1
- package/src/client/index.ts +1 -1
- package/src/client/panel/access-card.tsx +294 -0
- package/src/client/panel/access-tab.tsx +46 -0
- package/src/client/panel/advanced-tab.tsx +80 -0
- package/src/client/panel/cleanup-card.tsx +106 -0
- package/src/client/panel/connection-tab.tsx +214 -0
- package/src/client/panel/credentials-card.tsx +104 -0
- package/src/client/panel/index.tsx +862 -0
- package/src/client/panel/model-tab.tsx +23 -0
- package/src/client/panel/sessions-tab.tsx +21 -0
- package/src/client/panel/shared.tsx +488 -0
- package/src/client/panel/workspace-tab.tsx +40 -0
- package/src/client/styles.ts +24 -6
- package/src/config.ts +44 -12
- package/src/domain.ts +34 -3
- package/src/feishu/agent-context.ts +9 -5
- package/src/index.ts +32 -14
- package/src/routing/inbound.ts +88 -43
- package/src/service.ts +259 -65
- package/src/session/isolation.ts +277 -0
- package/src/session/manager.ts +63 -20
- package/src/session/repair.ts +2 -3
- package/src/session/routes.ts +2 -0
- package/src/session-commands.ts +41 -14
- package/src/settings/contract.ts +223 -47
- package/src/settings/directory-picker.ts +121 -0
- package/src/settings/routes.ts +49 -2
- package/src/settings/surface.ts +171 -25
- package/src/streamer.ts +70 -30
- package/src/workspaces.ts +50 -28
- package/src/client/panel.tsx +0 -1475
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
5
5
|
- 对话入口:私聊、群聊 @ 机器人、话题群 @ 机器人
|
|
6
6
|
- 收到触发消息:立刻加 **“Typing”** 表情 → 以话题形式回复(**创建话题**)→ 绑定 DSH session → 卡片 2.0 **流式回复** → 成功时更新为 **“DONE”**,失败时更新为 **“No”**
|
|
7
7
|
- 话题内后续消息**无需再 @**,自动延续该 session
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- 工作区归属:**每用户一个工作区**(私聊 = 用户;群聊 = 群 × 用户,同群不同用户互不影响);每个话题在工作区内拥有独立工作目录(默认 `topics/<话题 id>/`,Git 仓库工作区可自动改用 worktree),因此话题之间的文件互不干扰,而工作区数量只随用户增长
|
|
9
|
+
- **附件落盘(话题目录内)**:飞书图片、文件(含富文本消息中附带、普通消息里看不到的 `files`)、合并转发的聊天记录由插件流式保存到**当前话题工作目录的 `attachments/`**(只发附件、不带说明的消息不触发模型请求);Agent 用 `read_image` / `read` / `bash` 按需读取,未引用的附件不消耗 token。每个话题独立目录,因此其它话题的附件不会出现在本话题
|
|
10
10
|
- **HITL 审批**:当模型请求需要授权的动作(如 bash 沙箱升级 `sandbox_permissions`)时,插件监听主机 `approval/request` 事件,在同一话题里**新发一张独立审批决策卡**(✅ 允许 / ❌ 拒绝 按钮),用户点击后回填主机审批结果;超时(`approvalTimeoutMs`,默认 5 分钟)自动拒绝(fail-closed),非会话发起人点击无效
|
|
11
11
|
- **结构化提问**:模型调用 `ask_user_question` 时,插件监听主机 `user-questions/request` 瀑布并发一张问答卡(每个问题一组选项按钮,多选先勾选再「提交本题选择」),答案回填给模型;主机自带的应答者只有 Web 客户端,没有本插件时纯飞书会话会以 `NO_PROVIDER` 失败。超时(`questionTimeoutMs`,默认 5 分钟)或回合取消时本轮提问失败(fail-closed);没有可选项的问题直接提示用文字作答,**阻塞期间普通文本回复即作为该问题的自由文本答案**(同时支持多选后的自定义文本)
|
|
12
12
|
- **准入与授权(默认开启)**:**管理员不受白名单限制**(私聊、任意群聊——包括尚未授权的群——都能直接发起会话);其他人私聊只收到一条固定提示(`/userid` 例外),群聊需要「群已授权 + 用户在全局白名单」两层同时命中。机器人被拉进群后私聊管理员发**群信息卡**(未授权群附授权按钮,用于放行白名单用户);已授权群里未授权用户 @ 会触发用户授权卡。授权入口:飞书 `/auth` 命令、授权卡、Web 设置面板;管理员 open_id 在面板配置(私聊 `/userid` 获取)
|
|
13
13
|
- 全部基于飞书官方 SDK [`@larksuiteoapi/node-sdk`](https://www.npmjs.com/package/@larksuiteoapi/node-sdk)(WebSocket 长连接,免公网)
|
|
14
|
-
- **Web 设置面板**:Web/桌面端 **设置 →
|
|
14
|
+
- **Web 设置面板**:Web/桌面端 **设置 → 飞书渠道**,顶部状态头 + 6 个标签页(连接 / 会话与行为 / 模型 / 工作区 / 访问授权 / 高级);凭据不完整时插件不连接、面板显示三步引导,填写保存后自动连接(保存写入本机覆盖文件,部分字段需重连生效);详见下文「Web 设置面板」章节
|
|
15
15
|
|
|
16
|
-
设计与可行性论证见 [docs/feasibility-and-design.md](docs/feasibility-and-design.md)。
|
|
16
|
+
设计与可行性论证见 [docs/feasibility-and-design.md](docs/feasibility-and-design.md);工作区粒度与会话隔离的取舍见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md)。
|
|
17
17
|
|
|
18
18
|
## 斜杠命令
|
|
19
19
|
|
|
@@ -37,56 +37,58 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
37
37
|
|
|
38
38
|
## 流式卡片
|
|
39
39
|
|
|
40
|
-
卡片 2.0
|
|
40
|
+
卡片 2.0,按**过程 → 正文 → 页脚**三层组织,默认 500ms 刷新(`refreshMs` 可调):
|
|
41
41
|
|
|
42
|
-
1.
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
42
|
+
1. **过程**(推理折叠面板 + 活动状态两行):
|
|
43
|
+
- **推理折叠面板**(`collapsible_panel`):带边框、默认折叠,右侧箭头随折叠状态翻转(折叠向下、展开向上,`icon_expanded_angle: 180`)。有推理内容时,面板标题沿成功链路 **推理中 → 回复中 → 回复结束** 切换,失败终态为 **回复失败**;**推理中阶段始终渲染**(兼作阶段指示灯与占位),**回复中 / 回复结束 / 回复失败阶段仅当存在推理内容时渲染**——无推理的完成/失败回合不会留下空面板
|
|
44
|
+
- **活动状态两行**(小字,**只在当前正在流式输出的那张卡上渲染**,已冻结的续卡/主卡不再重复;**互相独立、互不顶替**),让卡片在“模型不吐字”的阶段(工具执行、subagent、workflow)**不再静默**——按 `statusRefreshMs`(默认 5s)慢节奏刷新计时:
|
|
45
|
+
- **状态行**(委托进度):`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
|
+
- **工具行**(父会话当前工具调用):`tool/call` → `🔧 调用工具:bash · 已运行 Ns`(subagent 工具显示其 description、workflow 工具显示 meta.name),`tool/result` 返回即清除,**回合结束后收起**(完成态只保留结算状态行:工作流结论 / 子代理完成摘要)
|
|
47
|
+
- 两者可同屏:如 `🧵 工作流 xxx 结束(完成)` + `🔧 调用工具:bash`
|
|
48
|
+
2. **正文**:富文本 markdown;超长正文**自动分段投放**——单卡片预算用满后按干净断点(段落 / 标题 / 代码块边界)切分,且**每张卡最多 5 个表格**(飞书硬限制,错误 11310);断点会避开代码围栏与表格,**单个表格/代码块小于单卡预算时不会被从中间切断**(单块本身超过单卡预算时只能硬切,内容仍不丢失)。续片以「📄 回复分段 n」卡片跟进同一话题,**全文无丢失**;续卡被飞书拒收时自动降级为纯文本消息(内容仍送达)。**续卡只留正文**:`📄 回复分段 n` → 可选活动行(仅当它是当前活动卡)→ 正文,无面板、无页脚、无分割线
|
|
49
|
+
3. **结算页脚**(分割线 + 两行小字):**只在主卡、且只在回合结束(`done` / `error`)后渲染一次**——首帧、流式中、续卡都没有,因此不会出现 0/— 的空统计。
|
|
50
|
+
- 第一行:`🧠 模型 {model}({effort}) · 🔑 会话 {sessionId} · ⏱ 推理 {推理时长}`。推理时长为**本轮所有推理阶段的累计墙钟时间**(每个 step 从首个推理增量开始,到该 step 输出结束——首个正文增量或 step 结束——为止;工具执行时间不计入;回合在推理中结束时计到 turn 结束),无推理时为 `—`
|
|
51
|
+
- 第二行:`📊 上下文 {tokens}/{window} ({占比}) · 📩 输入 {n} · 📨 输出 {n} · ⚡ 缓存命中 {pct}`。占比 = 上下文 tokens / 窗口(<10% 保留一位小数,≥10% 取整);没有窗口时只显示 tokens
|
|
52
|
+
- 输入/输出为 session 累计口径;模型、推理强度、缓存命中按**当前轮次**,本轮在请求头产生前失败时模型/缓存命中显示 `—`、推理强度省略,不会沿用上一轮
|
|
51
53
|
|
|
52
54
|
## Web 设置面板(设置 → 飞书渠道)
|
|
53
55
|
|
|
54
56
|
包内含可选的浏览器半区(`lib/client.js`,DSH client module):在 Web/桌面端 **设置** 导航注册「飞书渠道」分区。宿主没有 `webServer` 服务(headless profile)时不加载面板,飞书通道本身不受影响。
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
**连接时机**:启用插件时**不会无条件连接飞书**。插件启动先读取 profile 配置与 Web 覆盖,再检查 `appId` / `appSecret` 两个凭据引用能否解析出值:
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
- **凭据完整** → 建立飞书长连接(重启 DSH、重新加载插件时同样先检查再连接)。
|
|
61
|
+
- **凭据不完整** → 插件正常加载、**不建连接**、不报错,面板显示「待配置」与三步引导;在「连接」标签页填好两项凭据并保存后自动连接。
|
|
62
|
+
- **连接失败**(凭据错误、网络等)→ 记录在面板状态卡,插件保持加载;修好凭据或网络后点「重试」。
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
**面板结构**:顶部固定连接状态头(阶段徽标:待配置 / 未连接 / 连接中 / 已连接 / 连接失败、一句话摘要、刷新按钮),下方按标签页分区:
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
| 标签页 | 内容 |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| 连接 | 连接状态与连接信息(长连接状态、Bot open_id、运行时长、话题路由 / 活动会话 / 在途回复)、未配置时的三步引导、飞书应用凭据、最近一次连接失败原因、需重连的待生效提醒 |
|
|
69
|
+
| 会话与行为 | 私聊模式、普通群 @ 要求、话题兜底、首次私聊引导、权限切换、附件接收 |
|
|
70
|
+
| 模型 | 模型服务商、模型 id、推理档位、Agent 预设(宿主目录驱动的级联下拉) |
|
|
71
|
+
| 工作区 | 工作区根目录(可点「选择目录…」调用系统目录选择框)、过期目录保留期、自动清理、「目录清理」预览与手动执行;话题目录隔离方式(`topicIsolation`)在 profile 配置中设置 |
|
|
72
|
+
| 访问授权 | 管理员 open_id、准入模式、申请有效期;已授权群 / 用户白名单与待处理申请 |
|
|
73
|
+
| 高级 | 流式刷新 / 状态行 / SDK 日志级别、部署级只读参数、Web 覆盖文件路径与「清除 Web 覆盖」 |
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
**连接状态**:阶段徽标(待配置 / 未连接 / 连接中 / 已连接 / 连接失败)、飞书长连接状态、Bot open_id 尾号、运行时长、话题路由数 / 活动会话 / 在途回复、最近一次连接失败原因;每 5 秒自动刷新,也可手动刷新或**重启飞书连接**(会断开当前长连接并中断进行中的回复)。徽标不是健康时会额外显示「健康判定」原因——健康等级由宿主**所有 Cordis fiber** 折叠而来,因此「降级」也可能来自其它插件入口仍在等待依赖/加载(与飞书通道无关),原因行会列出对应插件名;「连接失败」只针对飞书连接本身,插件入口保持加载。
|
|
65
76
|
|
|
66
|
-
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| 工作区与文件 | `baseDir` | 立即生效(只影响之后新建的会话/工作区;已有会话不迁移) |
|
|
69
|
-
| 工作区与文件 | `workspaceRetention`、`workspaceRetentionAuto` | 立即生效(清理动作由「工作区清理」卡片触发) |
|
|
70
|
-
| 行为 | `dmMode`、`requireMention` | 需重启连接 |
|
|
71
|
-
| 行为 | `threadFallback`、`welcomeOnboarding`、`allowPermissionSwitch` | 立即生效 |
|
|
72
|
-
| 行为 | `accessMode`、`adminOpenId`、`authRequestTtlDays` | 立即生效(准入判定每次消息实时读取) |
|
|
73
|
-
| 内容与附件 | `acceptAttachments` | 立即生效 |
|
|
74
|
-
| 模型 | `provider`、`model`、`reasoningEffort`、`agentPreset` | 新会话生效(已有会话的模型/预设创建时已固定) |
|
|
75
|
-
| 流式卡片 | `refreshMs`、`statusRefreshMs`、`loggerLevel` | 需重启连接 |
|
|
77
|
+
**飞书应用凭据**:在「连接 → 飞书应用凭据」卡片里填写 **App ID / App Secret** 并保存,值经宿主 `ctx.credentials` 写入 DSH 凭据库(默认 `$DSH_HOME/.credentials.yaml` 的 `refs:` 段,仍遵循凭据层的解析优先级),面板只显示「已配置 / 未配置」与来源,**永不回显已存值**。两项齐全且通道未运行时保存后自动建立连接;已连接时保存只提示重连;只填了一项时保存成功但**不会连接**(状态保持「待配置」),补齐另一项后自动连接。已存值可以单独「移除已存值」;卡片下方显示的是凭据**引用名**(profile 配置里的 `appId` / `appSecret`),换引用名仍需改 profile 配置。
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
**统一保存**:除连接类操作外,可编辑字段共用一个草稿;每个表单标签页底部是「保存更改 / 放弃更改」,只提交改动过的字段,标注「需重连」的项在下次连接时生效;保存后「连接」标签页顶部横幅列出待生效项并提供一键重连。
|
|
78
80
|
|
|
79
|
-
`
|
|
81
|
+
**模型选择器**:模型与推理在面板里是**独立的「模型」标签页**:宿主 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` 在宿主侧是一条完整的请求路由,因此面板保存时要求**两者同时设置或同时留空**,不允许只选一半。
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
`baseDir` 是**模型/工具产生文件的落点**:**每个用户拥有一个独立工作区**(私聊 `<baseDir>/p2p/<open_id>/`,群聊 `<baseDir>/groups/<chat_id>/<open_id>/`,同一群里不同用户互不影响;旧版本的「用户 × 聊天」共享工作区正好复用为这个新工作区)。每个话题在工作区内拥有独立工作目录 `<工作区>/topics/<话题 id>/`(无话题的降级会话用 session id),所以话题之间的文件互不干扰,而工作区数量只随用户增长。面板里只接受**绝对路径**(`~` 开头按家目录展开,拒绝文件系统根目录),保存时会创建并校验该目录,不可用时当场报错,不会等到飞书里提示「会话创建失败」。已有 Session / 目录不会被迁移,因此切到新目录后旧话题继续用旧目录(话题内 `/renew` 也一样),只有之后新开的会话使用新目录。
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
**可视化选择工作区根目录**:输入框旁的「选择目录…」按钮会调用**宿主机的系统目录选择框**(macOS 的 NSOpenPanel / Windows 的文件夹对话框),选中后把绝对路径填进草稿,仍需点「保存更改」才落盘。该能力转发自宿主可选的 `ctx.directoryPicker` 服务:桌面端/本机 GUI 会话(native 后端)提供按钮;headless、远程或 SSH 会话没有可服务的显示会话(browse 后端),面板不显示按钮、保留手写输入,插件在缺少该服务的旧宿主上照常加载。选择框打开期间请求保持挂起;取消是正常结果,不会报错。
|
|
84
86
|
|
|
85
|
-
|
|
87
|
+
**目录清理**:「工作区」标签页内提供保留期与「目录清理」卡片,用于回收长期不用的话题目录。保留期可选 `off / 7 天 / 30 天 / 90 天 / 180 天`(默认 `off`);卡片实时预览「插件工作区数量 / 可清理目录 / 预计释放空间」,点「立即清理」需二次确认;也可开启 `workspaceRetentionAuto` 在启动后与每日自动执行一次。清理动作针对 `<baseDir>` 下的话题工作目录(当前布局)以及旧版本按话题建立、标题以 `飞书私聊 ·` / `飞书群聊 ·` 开头的工作区:删除目录,旧版工作区额外注销 workspace registry;并把仍指向它们的活跃路由退休为墓碑(该旧话题收到消息会明确提示「话题已过期」,不会恢复到一个已删除的目录)。**用户工作区本身永不删除,Harness Session 日志与事件记录也不删除**;删除不可恢复,`off` 时按钮禁用。未建立飞书连接时(存储域未打开)预览不可用,卡片会提示先连接。
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
**访问授权**:「访问授权」标签页集中管理准入:管理员 open_id(`ou_` 开头,私聊 `/userid` 获取)、准入模式、授权申请有效期;「已授权群」「已授权用户」列表支持增删(群 ID 可从入群群信息卡或 `/groupid` 获得,用户 open_id 可用 `/userid` 查看);「待处理申请」可一键授权/忽略。白名单只约束非管理员:管理员在任何私聊/群聊都直接放行。`guarded` 且管理员未配置时标签页顶部会红字提示“当前会拒绝所有对话”。授权白名单与申请记录保存在插件 `storageDomain`(`channels_routes` 域),重启后保留;未建立飞书连接时白名单尚未加载,标签页会提示「连接建立后加载授权列表」并禁用写操作。
|
|
88
90
|
|
|
89
|
-
**配置层级**:`configSchema` 默认值 < profile `cordis.patch.yml` 的 `config` < Web 面板覆盖。面板只把可编辑字段写入 `$DSH_HOME/dsh-channels/settings.json`(可用配置项 `settingsFile`
|
|
91
|
+
**配置层级**:`configSchema` 默认值 < profile `cordis.patch.yml` 的 `config` < Web 面板覆盖。面板只把可编辑字段写入 `$DSH_HOME/dsh-channels/settings.json`(可用配置项 `settingsFile` 改路径);「高级 → 清除 Web 覆盖」删除该文件并回落到 profile 配置。`baseDir` 可在「工作区」标签页编辑(绝对路径;只影响之后新建的会话);超时、路由 TTL 等仍只读展示。凭据**值**在「连接 → 飞书应用凭据」编辑,凭据**引用名**(`appId` / `appSecret` 配置项)仍由 profile 配置决定。
|
|
90
92
|
|
|
91
93
|
**为什么落在插件自己的文件而不是 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()` 并接受保存触发插件重载。
|
|
92
94
|
|
|
@@ -117,8 +119,9 @@ src/
|
|
|
117
119
|
│ └── services.ts # 工作区 / 存储域 / 凭据 / token / llm / 投影 / HostContext
|
|
118
120
|
├── cards.ts # 卡片 barrel → src/cards/
|
|
119
121
|
├── cards/ # 卡片 2.0 JSON 渲染
|
|
120
|
-
│ ├── primitives.ts # 信封、markdown
|
|
121
|
-
│ ├── streaming.ts #
|
|
122
|
+
│ ├── primitives.ts # 信封、markdown、折叠面板、按钮、分栏等原语
|
|
123
|
+
│ ├── streaming.ts # 流式回复卡构成(面板/活动行/正文/结算页脚)、分段、字节/表格预算
|
|
124
|
+
│ ├── onboarding.ts # 私聊首次触达引导卡
|
|
122
125
|
│ ├── approval.ts # 审批决策卡
|
|
123
126
|
│ └── question.ts # 结构化问答卡
|
|
124
127
|
├── approvals.ts # 审批 barrel → src/interactions/
|
|
@@ -131,6 +134,7 @@ src/
|
|
|
131
134
|
│ ├── routes.ts # 路由存储 + TTL 墓碑
|
|
132
135
|
│ ├── repair.ts # inflight 快照 + 重启修复
|
|
133
136
|
│ ├── manager.ts # live Agent、冷恢复、preset、/renew 事务
|
|
137
|
+
│ ├── isolation.ts # 话题工作目录编排:workingDirectory 切换 / Git worktree + 降级
|
|
134
138
|
│ └── request-policy.ts # 会话级 provider/model/reasoningEffort 覆盖(/model、/effort)
|
|
135
139
|
├── feishu/
|
|
136
140
|
│ ├── delivery.ts # 出站投递:首帧话题卡、结果卡、分段降级、表情生命周期
|
|
@@ -144,12 +148,18 @@ src/
|
|
|
144
148
|
├── stats.ts # 宿主投影优先的 token / 缓存 / 上下文折叠
|
|
145
149
|
├── domain.ts # storageDomain 域声明与路由记录 schema
|
|
146
150
|
├── health.ts # 健康快照与卡片
|
|
147
|
-
├── cleanup.ts #
|
|
151
|
+
├── cleanup.ts # 过期话题目录 / 旧版工作区清理策略(纯逻辑,无 I/O)
|
|
148
152
|
├── session-export.ts # 逻辑日志导出
|
|
149
153
|
├── feishu-channel.ts # 官方 SDK Channel 适配(可注入 stub)
|
|
150
154
|
├── client/ # 浏览器半区:设置 →「飞书渠道」页面(构建到 lib/client.js)
|
|
151
155
|
│ ├── index.ts # client 插件入口:settings.section 注册 + 样式
|
|
152
|
-
│ ├── panel
|
|
156
|
+
│ ├── panel/ # 页面外壳 + 六个标签页
|
|
157
|
+
│ │ ├── index.tsx # 加载 / 5s 状态轮询 / 全局草稿 / catalog 级联 / 顶部状态头 / Tab
|
|
158
|
+
│ │ ├── shared.tsx # 字段渲染原语、保存栏、阶段徽标、草稿校验
|
|
159
|
+
│ │ ├── connection-tab.tsx # 连接:三步引导 / 连接信息 / 待生效提醒 / 凭据卡
|
|
160
|
+
│ │ ├── credentials-card.tsx # App ID / App Secret 编辑(不回显)
|
|
161
|
+
│ │ ├── sessions-tab.tsx / model-tab.tsx / workspace-tab.tsx / access-tab.tsx / advanced-tab.tsx
|
|
162
|
+
│ │ └── cleanup-card.tsx / access-card.tsx
|
|
153
163
|
│ ├── catalog.ts # 选择器目录折叠:Host 字段 + 实时目录 → 表单字段
|
|
154
164
|
│ ├── api.ts # 同源 /dsh-channels/* 请求封装
|
|
155
165
|
│ └── styles.ts # 语义 token 样式(注入一次)
|
|
@@ -157,9 +167,10 @@ src/
|
|
|
157
167
|
│ ├── contract.ts # 双端共享 wire 类型 / 字段清单 / 路由常量
|
|
158
168
|
│ ├── overlay.ts # settings.json 读写、校验、合并
|
|
159
169
|
│ ├── catalog.ts # 宿主选择目录:provider/model/推理档位/preset
|
|
160
|
-
│ ├──
|
|
170
|
+
│ ├── directory-picker.ts # 系统目录选择框适配(ctx.directoryPicker native 后端)
|
|
171
|
+
│ ├── surface.ts # 状态 / 面板视图模型 + 保存 / 清除 / 重连 / 选目录
|
|
161
172
|
│ └── routes.ts # HTTP 路由 + 同源写保护
|
|
162
|
-
└── workspaces.ts #
|
|
173
|
+
└── workspaces.ts # 用户工作区 / 话题目录 / 附件 staging 路径解析
|
|
163
174
|
```
|
|
164
175
|
|
|
165
176
|
`lib/index.js` 与 `lib/client.js` 是构建产物(`pnpm build`):前者是安装后 profile 直接加载的 Host 入口(普通 ESM,不依赖 TypeScript 运行时转换),后者是设置面板的浏览器 bundle。`scripts/verify-host-loader.mjs` / `scripts/verify-client-loader.mjs` 分别做冒烟。
|
|
@@ -184,14 +195,16 @@ DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add link:/path/to/dsh_channels
|
|
|
184
195
|
|
|
185
196
|
npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直接以 ESM 加载,不经过 TypeScript strip;`package.json` 的 `main` 指向它),设置面板是 `lib/client.js`。本地 `link:` 安装前仍必须先执行 `pnpm build`。安装或修改后重启 dsh(桌面端重启应用,`dsh web` 重启服务并刷新页面)。DSH 只加载已构建产物,直接改 `src/` 不会生效;没有 web 载体的 profile 不注册面板,飞书通道照常运行。
|
|
186
197
|
|
|
187
|
-
包内的 `cordis.patch.yml` 会自动把 `@modesto_ma/dsh-channels` 加入 profile 的 bundle 列表,并提供稳定 `id: dsh-channels` 的 Loader 行;profile
|
|
198
|
+
包内的 `cordis.patch.yml` 会自动把 `@modesto_ma/dsh-channels` 加入 profile 的 bundle 列表,并提供稳定 `id: dsh-channels` 的 Loader 行;profile 可在 `$DSH_HOME/profiles/web/cordis.patch.yml` 中按该 id 提供实例配置(不提供时使用默认凭据引用名):
|
|
199
|
+
|
|
200
|
+
`appId` / `appSecret` 也各自有默认引用名 `FEISHU_APP_ID` / `FEISHU_APP_SECRET`:凭据库使用这两个名字时,bundle 行即使暂时没有 `config` 也能完成 Loader 校验并启动 Host 半侧,从而先打开设置面板补全凭据;使用自定义引用名时再按需覆盖。
|
|
188
201
|
|
|
189
202
|
```yaml
|
|
190
203
|
- id: dsh-channels
|
|
191
204
|
config:
|
|
192
205
|
appId: 'FEISHU_APP_ID' # 凭据引用名(非密钥本身),经 ctx.credentials 解析
|
|
193
206
|
appSecret: 'FEISHU_APP_SECRET'
|
|
194
|
-
baseDir: './channels-data' # 工作区根目录(按
|
|
207
|
+
baseDir: './channels-data' # 工作区根目录(按 私聊用户 / 群×用户 组织;可在 Web 面板覆盖,面板要求绝对路径)
|
|
195
208
|
provider: 'deepseek-official' # 可选:省略则用部署默认
|
|
196
209
|
model: 'deepseek-v4-flash' # 可选
|
|
197
210
|
reasoningEffort: 'high' # 可选:模型声明的精确档位 id;省略则用适配器/provider 默认
|
|
@@ -201,12 +214,13 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
201
214
|
requireMention: true # 普通群需 @ 机器人
|
|
202
215
|
dmMode: 'open'
|
|
203
216
|
threadFallback: 'degrade' # 话题被拒时降级为会话级路由
|
|
217
|
+
topicIsolation: 'auto' # 话题目录隔离:auto(Git 仓库用 worktree,否则子目录)/ subdir / worktree
|
|
204
218
|
routeTtlMs: 2592000000 # 话题路由 30 天无活动即过期;后续消息明确拒绝,不新建 Session(0 = 关闭)
|
|
205
219
|
approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟;超时自动拒绝)
|
|
206
220
|
questionTimeoutMs: 300000 # ask_user_question 问答卡等待时长(默认 5 分钟;超时本轮提问失败)
|
|
207
221
|
acceptAttachments: true # 把飞书图片/文件/合并转发记录落盘到工作区 attachments/(不触发模型)
|
|
208
|
-
workspaceRetention: 'off' #
|
|
209
|
-
workspaceRetentionAuto: false #
|
|
222
|
+
workspaceRetention: 'off' # 过期话题目录清理阈值:off / 7d / 30d / 90d / 180d
|
|
223
|
+
workspaceRetentionAuto: false # 每日自动执行一次清理(默认关;删除不可恢复;用户工作区永不删除)
|
|
210
224
|
welcomeOnboarding: true # 私聊首次触达发一张引导卡(按 chat 持久化去重)
|
|
211
225
|
allowPermissionSwitch: false # 是否允许 /permissions <name> 从飞书侧切换权限预设(安全敏感)
|
|
212
226
|
accessMode: 'guarded' # 准入模式:guarded(默认,仅管理员+白名单)| open(旧行为,不安全)
|
|
@@ -231,13 +245,13 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
231
245
|
|
|
232
246
|
也可以不手改文件:安装并在 Web 面板里打开 **设置 → 飞书渠道**,在「飞书应用凭据」卡片填入 App ID / App Secret 保存即可(值同样写进该凭据库)。
|
|
233
247
|
|
|
234
|
-
文件必须仅属主可读(dsh 强制校验,否则拒绝启动):`chmod 600 $DSH_HOME/.credentials.yaml`。解析优先级:进程环境变量 > `.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`(由 `dsh-credentials-local` 提供,热加载)。配置里只写引用名,明文密钥会被 schema
|
|
248
|
+
文件必须仅属主可读(dsh 强制校验,否则拒绝启动):`chmod 600 $DSH_HOME/.credentials.yaml`。解析优先级:进程环境变量 > `.credentials.yaml` > `<cwd>/.env` > `$DSH_HOME/.env`(由 `dsh-credentials-local` 提供,热加载)。配置里只写引用名,明文密钥会被 schema 拒绝。改动凭据后:Web 面板保存会即时写入,未连接时自动建立连接、已连接时提示重连;手改 `.credentials.yaml` 由凭据层热加载,但正在运行的连接不会自动使用新值,需在面板点「重启飞书连接」。
|
|
235
249
|
|
|
236
250
|
验证组合(不启动服务):`DSH_HOME=$DSH_HOME pnpm dsh web --dump-config | grep dsh-channels`
|
|
237
251
|
|
|
238
|
-
启动成功标志(控制台):`[dsh-channels] feishu long connection established, bot open_id: ou_xxx
|
|
252
|
+
启动成功标志(控制台):`[dsh-channels] feishu long connection established, bot open_id: ou_xxx`;凭据不完整时不会连接,日志会提示缺少的凭据引用(如 `飞书应用凭据未配置完整(缺少 FEISHU_APP_SECRET…)`),打开 Web **设置 → 飞书渠道 → 连接** 补齐即可。
|
|
239
253
|
|
|
240
|
-
宿主 profile 已内置所需服务(agent-loop、llm-deepseek、session 持久化、storage-domain、workspace);推荐确认 `dsh-token-meter` 已启用(卡片上下文占用显示)。
|
|
254
|
+
宿主 profile 已内置所需服务(agent-loop、llm-deepseek、session 持久化、storage-domain、workspace、working-directory);推荐确认 `dsh-token-meter` 已启用(卡片上下文占用显示)。Git worktree 隔离模式额外依赖可选的 `@deepseek-ai/dsh-experimental-worktree` 实验插件,缺失时自动回退为普通话题子目录。
|
|
241
255
|
|
|
242
256
|
**工具与 agent preset(web profile)**:web profile 把 bash/fs/todo 等工具行全部标为 `disabled: true`,工具只存在于 agent preset 里,且 `agents.create()` 不会自动应用 preset——插件因此在创建/恢复会话时通过 `setup` 回调挂载 preset(默认 `standard`,可用 `agentPreset` 指定更精简的 preset,如只含 bash/fs 的自定义项),并把 preset id 记录进会话头。若机器人回复把 `<tool_calls>…` 当纯文本吐出,即该会话没挂上 preset;修好后再看会话的 `request/header` 应带 `tools` 字段、日志出现 `tool/call` 事件。无 preset roster 的部署(如 headless)不做此步骤,工具本就位于宿主层。
|
|
243
257
|
|
|
@@ -256,9 +270,27 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
256
270
|
3. 事件订阅方式选**长连接**(WebSocket,免公网)。
|
|
257
271
|
4. 发布版本并把用户加入可用范围;将机器人拉入测试群。
|
|
258
272
|
|
|
273
|
+
## 工作区与会话隔离
|
|
274
|
+
|
|
275
|
+
工作区(DSH workspace registry 中的「项目」)按**人**建立,只在首次需要时创建;每个话题在工作区内拥有独立的**实际工作目录**:
|
|
276
|
+
|
|
277
|
+
| 场景 | 工作区目录(header cwd) | 每个话题的工作目录(实际 cwd) |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| 私聊 | `<baseDir>/p2p/<open_id>/` | `<工作区>/topics/<话题 id>/` |
|
|
280
|
+
| 群聊 | `<baseDir>/groups/<chat_id>/<open_id>/` | 同上 |
|
|
281
|
+
|
|
282
|
+
- 群聊必须是「群 × 用户」:同一个人在不同群之间不共享文件,避免 A 群会话把 B 群内容带出来。
|
|
283
|
+
- 会话创建时以工作区为 header cwd(保证 GUI 里归属该项目、沙箱边界稳定),随后通过 `ctx.workingDirectory` 把实际工作目录切到话题目录;冷恢复会从 Session 日志还原该目录。因此话题之间的文件互不干扰,而工作区数量只随用户增长。
|
|
284
|
+
- `topicIsolation` 控制隔离形式:`auto`(默认)在用户工作区是 Git 仓库(含 `.git`)时给每个话题建一个 `feishu-<话题>` 分支 + worktree(适合多个话题并发改同一个代码仓库),否则用普通子目录;`subdir` 一律子目录;`worktree` 一律尝试 worktree,失败告警并回退子目录。
|
|
285
|
+
- 隔离强度:这是**默认工作目录层面**的隔离(bash / fs / 终端都从话题目录出发),不是防恶意的硬沙箱——沙箱写边界仍是用户工作区。若需要硬边界,可把话题目录直接作为 Session cwd,但会话会失去工作区归属(GUI 显示为未分组)。
|
|
286
|
+
- 附件落在当前话题目录的 `attachments/`;话题建立前收到的附件先暂存 `<baseDir>/_staging/...`,首个 Session 创建时迁入。`/renew` 复用同一话题目录。
|
|
287
|
+
- worktree 模式下清理只删除 checkout 目录;Git 分支与管理记录按 DSH 语义保留(需要时可在仓库里 `git worktree prune` / `git branch -D` 回收)。
|
|
288
|
+
- 旧版本「一个话题一个工作区」(`<baseDir>/topics/...`)不会自动迁移:旧话题继续用原目录,保留期清理可回收;旧版「用户 × 聊天」共享工作区正好作为新的用户工作区复用。
|
|
289
|
+
- 设计取舍与约束详见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md)。
|
|
290
|
+
|
|
259
291
|
## 行为与降级
|
|
260
292
|
|
|
261
|
-
- **附件输入(图片 / 文件 /
|
|
293
|
+
- **附件输入(图片 / 文件 / 会话记录,话题目录内)**:飞书图片和文件经通道**流式**写入当前话题工作目录的 `attachments/`(文件名前缀为收到时间 `YYYYMMDD-HHmmss-`),合并转发的聊天记录原样保存为 `*-forwarded-record.txt`。话题建立之前收到的附件先落到 `<baseDir>/_staging/<chatType>/<chat_id>/<open_id>/`,该话题的第一个 Session 创建时自动迁入其话题目录;`/renew` 在**同一话题**内新建 Session,沿用同一话题目录与附件。**每个新话题都会新建独立工作目录**,因此话题之间看不到彼此的文件。旧版(每话题一个工作区)的话题继续使用原目录,不会自动迁移。除独立图片/文件消息外,**富文本(post)消息附带在 `files` 数组里的文件也会被恢复并落盘**——官方 SDK 的 post 归一化只遍历正文元素、会丢弃 `files`,插件通过原始事件补回。**只发附件(不带说明)的消息不触发模型请求**;插件在 agent 作用域注册一段 system prompt,说明附件目录约定,Agent 需要时用 `read_image` / `read` / `bash` 按需读取(未被引用的附件零 token)。写入成功给消息加 `DONE` 表情、全部失败加 `No`;失败时额外回一条**纯文字提示**(不是卡片),列出附件名与原因(例如资源已过期、文件夹消息暂不支持),避免 Agent 静默读到旧附件。**超过 100 MB 的文件也能保存**:飞书资源接口拒绝单次超过 100 MB 的完整下载(错误 234037),但对 `Range` 请求返回 206,插件因此先探测总大小、再按 32 MiB 分块下载,超大文件(已验证 294 MB)同样能落盘。单条附件失败只影响该附件,不阻塞其它附件。`acceptAttachments: false` 时附件消息保持惰性(不落盘、不触发)。附件数量不设插件侧上限,文件大小也仅受飞书平台上传限制(下载通过分块 Range 绕过单次响应上限),磁盘增长由「目录清理」面板兜底;超大文件下载耗时较长,会占用该话题的处理队列。
|
|
262
294
|
- **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)。
|
|
263
295
|
- **话题创建**:通过回复消息接口 `reply_in_thread: true`(官方文档明示的“以话题方式回复(即创建话题)”)。响应后经 `im.v1.message.get` 取回 `thread_id` 写入路由表。
|
|
264
296
|
- **话题延续**:`im.message.receive_v1` 事件自带 `thread_id`,命中路由表即 `agent.followup()`,无需 @。同一 (chat, user) 的**并发首消息会被串行化**:后到的消息等待在途的会话建立完成,再按串行语义续聊同一 session 或另起新话题——不会重复建 session、覆盖路由,也不会落入“无路由、无 @”的丢弃窗口。
|
|
@@ -266,24 +298,26 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
266
298
|
- **重启恢复**:路由表持久化在 `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 注册表。
|
|
267
299
|
- **重启修复(流式中断)**:每条正在流式的 turn 会把自己的修复凭据(trigger 消息、Typing reaction id、全部段卡 id)写入 `channels_routes` 域的 `inflight` 表(键 = 主卡 message id,turn 结束即删)。重启后长连接一建立,插件就把上一实例留下的中途卡片 patch 成「⚠️ 服务已重启,本次回复中断,请重新发送消息。」终态并补 DONE 表情;patch 失败重试一次(上限 2 次)后放弃。持久化盘点与缺口清单见 [docs/persistence.md](docs/persistence.md)。
|
|
268
300
|
- **消息去重(WS 重投幂等)**:飞书长连接重连会重投未 ack 的事件;每条消息在任何副作用之前先落一条 `processed` 记录(键 = message id,飞书全局唯一),重投消息被直接丢弃——同进程靠内存守卫、跨重启靠持久化记录,每条消息至多处理一次。记录保留 24 小时后由 GC 清扫。
|
|
269
|
-
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner
|
|
270
|
-
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **open_id 在全局用户白名单**,两者缺一不可。未授权消息不建 Session
|
|
301
|
+
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner/工作区路径/会话目录/最后活动时间;旧 DSH Session 与日志不删除,话题目录仅在「目录清理」显式启用时按保留期删除(用户工作区永不删除)。降级的 chat-scoped 路由没有独立话题边界,仍直接清理。陈年 in-flight 快照同时在启动时及每 6 小时清扫。`routeTtlMs: 0` 关闭后续路由过期处理,已产生的墓碑仍保持拒绝语义。
|
|
302
|
+
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **open_id 在全局用户白名单**,两者缺一不可。未授权消息不建 Session、不建话题目录、不落附件、不调模型;群里静默、私聊回固定文案。机器人被拉进任意群时(订阅 `im.chat.member.bot.added_v1`),插件私聊管理员发群信息卡(群名/`chat_id`/成员数/邀请人),未授权群附「授权本群 / 忽略」按钮;已授权群里未授权用户 @ 机器人时,管理员会收到用户授权申请卡(含昵称/open_id)。授权状态持久化在 `channels_routes` 域的 `access_groups` / `access_users` / `access_requests` 表,撤销立即生效。管理员 open_id 在 Web 面板配置;`/auth` 命令、授权卡、面板三种入口等价。
|
|
271
303
|
- **HITL 审批**:插件以 `prepend: true` 注册为宿主 `approval/request` 瀑布的应答者——本频道路由的 session 的审批请求**优先于** Web 网关的通用应答(其面板对飞书用户不可见),非本频道的请求原样 `next()` 交给其他应答者。审批卡是**独立消息**(锚定到该 session 最新流式卡、同话题),绝不嵌入流式卡(其全量替换 patch 会冲掉按钮)。待审批时允许/拒绝按钮以两列等宽方式排列在同一行;决策仅在会话发起人点击时生效:允许 → `allowed-once`、拒绝 → `rejected`、超时 → `rejected`(fail-closed,卡面显示「已超时」)、请求被撤回(abort)→ `cancelled`、卡片发送失败 → `unavailable`(fail-closed);每次决策后卡片更新为终态并移除两个按钮。审批记录(`approval/asked`/`decided`)由宿主会话日志负责,插件不做持久化——重启后遗留问题随 turn 消亡,与 Web 网关一致。需要飞书应用订阅 **card.action.trigger** 回调(见「飞书侧准备」);启动日志 `HITL 审批应答已就绪` 表示生效,`主机未挂载 approval 服务` 表示该部署未启用 HITL。
|
|
272
|
-
-
|
|
304
|
+
- **终态与表情**:成功轮的面板标题变为「回复结束」(无推理内容时不渲染面板),并把 `Typing` 换成 `DONE`;`turn/end.kind = error` 时面板标题为「回复失败」(同样仅当有推理内容),错误只在正文出现一次(`❌ **本轮运行失败**` + 结构化错误消息与错误码),并把 `Typing` 换成 `No`。模型/推理强度/本轮缓存样本不会继承上一轮。
|
|
273
305
|
- **限频**:卡片更新单条消息 5 QPS、应用级 1000 次/分钟;500ms(2 QPS)默认安全,`refreshMs` 下限 200ms;活动状态行的保活心跳默认 5s(`statusRefreshMs` 下限 1s),远低于限频。
|
|
274
|
-
-
|
|
306
|
+
- **卡片体积**:预算常量收敛为三个——`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),面板内表格省略;窗口长度与正文长短无关,不占用也不被正文挤占。
|
|
275
307
|
- **续卡降级**:分段卡片发送被飞书拒绝(如表格数量超限 230099/11310)时,自动改发纯文本消息;卡片更新失败按指数退避重试,不刷屏报错。
|
|
276
308
|
|
|
277
309
|
## 已知取舍(v1)
|
|
278
310
|
|
|
279
311
|
- patch 为全量替换:流式期间用户手动展开折叠面板会被下一次刷新收起;流式结束后不再刷新,可自由展开。
|
|
280
312
|
- 超长正文拆成多张卡片(分段投放):推理内容不跨卡片,始终留在首卡;超出 5 KB 的部分被截断(只保留开头),不追加投放。
|
|
313
|
+
- 活动状态行只在当前正在流式输出的那张卡上渲染:若最后一张续卡被飞书拒收、降级为纯文本(无法再 patch),结算状态行(工作流结论 / 子代理完成摘要)不会显示;主卡结算页脚不受影响。
|
|
281
314
|
- 子代理只显示状态(label / 当前工具 / 计时 / 并行计数),不显示其输出正文;后台子代理(`run_in_background`)在父 turn 结束后不再继续显示进度(卡片随父 turn 收尾)。
|
|
282
315
|
- 私聊 `reply_in_thread` 需真机验证(文档未明示限制,失败走降级路径,见上文)。
|
|
283
316
|
- 会话的 agent 常驻内存,不做空闲回收(重启后按需懒恢复)。
|
|
284
317
|
- 重启修复会**全量替换**中断的卡片:旧进程里已流出的部分正文随之消失(本地没有副本,patch 是全量语义);修复是一次性 + 一次重试,不再无限重试。
|
|
285
318
|
- 路由 GC 是 TTL 兜底:未监听飞书“群解散/话题删除”事件做主动清理;真实话题过期后写入持久化墓碑,用户回来会收到明确拒绝,不会在旧话题中误建新 Session。
|
|
286
319
|
- 消息去重是**至多一次**语义:接受后、副作用完成前崩溃的极小窗口内,重投消息会被丢弃——中断修复卡提示“请重新发送消息”,用户重发即可(新 message id 正常处理)。
|
|
320
|
+
- 插件启用时**不会无条件连接飞书**:只有两项凭据都能解析时才建连,凭据不完整时插件照常加载、面板显示「待配置」引导;连接失败只影响连接本身,插件入口保持加载。
|
|
287
321
|
- Web 面板编辑的是当前凭据**引用名**背后的值(profile 配置 `appId` / `appSecret` 指向的 ref);更换引用名仍需编辑 profile 配置并重启 DSH。凭据提供方只读时面板会禁用输入并提示。
|
|
288
322
|
- Web 面板的模型字段优先渲染为宿主目录驱动的下拉(provider → model → 推理档位);宿主没有 LLM 服务或目录查询失败时退回自由文本,手填错误仍会在下一次模型请求时由 Host 报错;「重启飞书连接」会中断进行中的回复。
|
|
289
323
|
- 持久化盘点与全部缺口状态见 [docs/persistence.md](docs/persistence.md)。
|
|
@@ -300,4 +334,4 @@ pnpm verify:client # client loader 冒烟
|
|
|
300
334
|
pnpm check # 以上全部
|
|
301
335
|
```
|
|
302
336
|
|
|
303
|
-
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。`tests/settings.test.ts` 覆盖:覆盖层 schema
|
|
337
|
+
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。`tests/settings.test.ts` 覆盖:覆盖层 schema/层级、面板视图模型、连接阶段(待配置/未连接/已连接/失败)、凭据保存按就绪度连接、HTTP 路由(含同源写保护)、`apply()` 的 webServer 接线、模型选择器的宿主目录与降级行为;`tests/client-catalog.test.ts` 覆盖浏览器半区的目录折叠(含依赖项解禁回归);以及 `FeishuService` 的覆盖加载 / 热更新 / 重启 / 连接失败记录 / 监听器释放。
|
|
@@ -84,7 +84,7 @@ SDK 支持 `groupAllowlist` / `dmMode: 'allowlist'` + `dmAllowlist`,但插件*
|
|
|
84
84
|
独立审计还补充了三个同源缺口,都应在 P1/P2 顺手处理:
|
|
85
85
|
|
|
86
86
|
- **route-free / 未知 slash 在 owner 门之前就有副作用**:`/help` 会建话题发卡、`/health` 会泄露路由数、live Agent 数、bot open_id 尾号;非法命令名也会回输入错误卡(`src/service.ts:868-882`、`:942-994`)。
|
|
87
|
-
- **HITL 审批卡在群话题内对群成员可见**:审批卡携带工具名、reason、sessionId(`src/cards/approval.ts:62-90
|
|
87
|
+
- **HITL 审批卡在群话题内对群成员可见**:审批卡携带工具名、reason、sessionId(`src/cards/approval.ts:62-90`),流式卡结算后的页脚第一行仍带完整 session id、在群里可见(`buildModelLine`,`src/cards/streaming.ts:291-295`)。新模型下群内只有管理员和已授权用户,属“已授权成员可见”的可接受范围,后续可改私聊投递。
|
|
88
88
|
- **错误回显未脱敏**:入站异常会原样回 `dsh-channels 处理失败:<error>`(`src/service.ts:754-761`),可能带本地路径/Session 信息。
|
|
89
89
|
|
|
90
90
|
### 2.6 身份发现能力已经有了
|
|
@@ -392,7 +392,7 @@ SDK 的 `LarkChannel` 已内置 `EventMap['botAdded']`(`im.chat.member.bot.add
|
|
|
392
392
|
1. 装好插件、配好凭据、重启;启动日志:
|
|
393
393
|
`[dsh-channels] 准入=guarded,管理员未配置:所有对话将被拒绝;私聊机器人发送 /userid 获取 open_id`
|
|
394
394
|
2. 在飞书私聊机器人发 `/userid`(任何私聊发送者都可用,只回固定文本、不建会话)→ 拿到 `ou_xxx`;
|
|
395
|
-
3. Web 面板「飞书渠道 →
|
|
395
|
+
3. Web 面板「飞书渠道 → 访问授权 → 管理员 open_id」填入 `ou_xxx` 保存(立即生效);
|
|
396
396
|
4. 面板状态显示管理员已配置;管理员私聊可正常对话。
|
|
397
397
|
|
|
398
398
|
### 场景 B:给非管理员开启群的访问(三种入口)
|
|
@@ -426,7 +426,7 @@ SDK 的 `LarkChannel` 已内置 `EventMap['botAdded']`(`im.chat.member.bot.add
|
|
|
426
426
|
|
|
427
427
|
## 7. Web 面板
|
|
428
428
|
|
|
429
|
-
### 7.1
|
|
429
|
+
### 7.1 访问授权新增字段(`access` 组)
|
|
430
430
|
|
|
431
431
|
| key | kind | 说明 |
|
|
432
432
|
|---|---|---|
|
|
@@ -462,7 +462,7 @@ SDK 的 `LarkChannel` 已内置 `EventMap['botAdded']`(`im.chat.member.bot.add
|
|
|
462
462
|
UI:
|
|
463
463
|
|
|
464
464
|
- 顶部警示条:`guarded` 且管理员未配置时红字提示“当前会拒绝所有对话,请先配置管理员 open_id”;
|
|
465
|
-
- 「管理员」行:显示当前 open_id /
|
|
465
|
+
- 「管理员」行:显示当前 open_id / 未配置(编辑在上方准入表单里);
|
|
466
466
|
- 「已授权群」列表:群名(未知时显示 chat_id)、来源、时间 + 移除;输入框可手填 chat_id 添加;
|
|
467
467
|
- 「已授权用户」列表:昵称(未知时显示 open_id)+ 移除;输入框可手填 open_id 添加;
|
|
468
468
|
- 「待处理申请」列表:群申请 / 用户申请,各带「授权」「忽略」;
|
|
@@ -504,7 +504,7 @@ UI:
|
|
|
504
504
|
- [x] `InboundRouter` 接入 `access.admit`;话题/附件/问答/续写的 sender + chatId 校验
|
|
505
505
|
- [x] 命令面准入:`/userid`、`/groupid` bootstrap 放行;`/auth` 管理员专用;其他命令(含非法/未知命令)先判准入再发卡
|
|
506
506
|
- [x] `/auth` 核心子命令:`status` / `allow` / `revoke` / `list` / `user add|remove`
|
|
507
|
-
- [x] Web
|
|
507
|
+
- [x] Web 面板:「访问授权」标签页加 `adminOpenId`/准入模式;白名单卡片(群/用户增删,自动授权仍无开关);`/dsh-channels/access*` 路由(同源写保护)
|
|
508
508
|
- [x] 入群事件接线(wrapper `onBotAdded` + `getChatInfo`):管理员私聊收群信息卡,未授权群附「授权本群 / 忽略」按钮
|
|
509
509
|
- [x] 管理员引导:启动日志、`/userid` 文案、面板未配置警示
|
|
510
510
|
- [x] 错误兜底文案脱敏
|