@modesto_ma/dsh-channels 0.0.0-stage → 0.1.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.
Files changed (73) hide show
  1. package/README.md +302 -2
  2. package/cordis.patch.yml +3 -0
  3. package/docs/access-control-design.md +550 -0
  4. package/docs/assets/web-settings-panel.png +0 -0
  5. package/docs/assets/web-settings-restart-pending.png +0 -0
  6. package/docs/feasibility-and-design.md +213 -0
  7. package/docs/feishu-onboarding.md +268 -0
  8. package/docs/persistence.md +199 -0
  9. package/docs/refactor-plan.md +56 -0
  10. package/docs/review-dsh-0.2.0.md +389 -0
  11. package/docs/slash-commands-implementation-plan.md +410 -0
  12. package/docs/web-display-only-surface-roadmap.md +363 -0
  13. package/lib/client.js +2226 -0
  14. package/lib/client.js.map +1 -0
  15. package/lib/index.js +10217 -0
  16. package/lib/index.js.map +1 -0
  17. package/package.json +98 -4
  18. package/src/access/cards.ts +188 -0
  19. package/src/access/controller.ts +201 -0
  20. package/src/access/index.ts +13 -0
  21. package/src/access/policy.ts +103 -0
  22. package/src/access/requests.ts +512 -0
  23. package/src/access/store.ts +220 -0
  24. package/src/approvals.ts +8 -0
  25. package/src/cards/approval.ts +114 -0
  26. package/src/cards/index.ts +11 -0
  27. package/src/cards/primitives.ts +82 -0
  28. package/src/cards/question.ts +141 -0
  29. package/src/cards/streaming.ts +563 -0
  30. package/src/cards.ts +8 -0
  31. package/src/cleanup.ts +398 -0
  32. package/src/client/api.ts +262 -0
  33. package/src/client/catalog.ts +92 -0
  34. package/src/client/index.ts +61 -0
  35. package/src/client/panel.tsx +1475 -0
  36. package/src/client/styles.ts +98 -0
  37. package/src/commands/registry.ts +155 -0
  38. package/src/commands.ts +335 -0
  39. package/src/config.ts +206 -0
  40. package/src/domain.ts +428 -0
  41. package/src/dsh-types.ts +11 -0
  42. package/src/feishu/agent-context.ts +62 -0
  43. package/src/feishu/delivery.ts +188 -0
  44. package/src/feishu/inbound-attachments.ts +436 -0
  45. package/src/feishu-channel.ts +339 -0
  46. package/src/health.ts +233 -0
  47. package/src/host/agent.ts +181 -0
  48. package/src/host/commands.ts +63 -0
  49. package/src/host/core.ts +345 -0
  50. package/src/host/index.ts +12 -0
  51. package/src/host/interaction.ts +101 -0
  52. package/src/host/services.ts +320 -0
  53. package/src/index.ts +122 -0
  54. package/src/interactions/approvals.ts +154 -0
  55. package/src/interactions/pending.ts +162 -0
  56. package/src/interactions/questions.ts +274 -0
  57. package/src/questions.ts +8 -0
  58. package/src/routing/inbound.ts +524 -0
  59. package/src/service.ts +2138 -0
  60. package/src/session/manager.ts +352 -0
  61. package/src/session/repair.ts +157 -0
  62. package/src/session/request-policy.ts +192 -0
  63. package/src/session/routes.ts +165 -0
  64. package/src/session-commands.ts +310 -0
  65. package/src/session-export.ts +112 -0
  66. package/src/settings/catalog.ts +309 -0
  67. package/src/settings/contract.ts +508 -0
  68. package/src/settings/overlay.ts +164 -0
  69. package/src/settings/routes.ts +409 -0
  70. package/src/settings/surface.ts +773 -0
  71. package/src/stats.ts +485 -0
  72. package/src/streamer.ts +1108 -0
  73. package/src/workspaces.ts +199 -0
@@ -0,0 +1,213 @@
1
+ # dsh-channels · 飞书渠道插件 可行性验证与设计(v1)
2
+
3
+ > 状态:可行性验证完成 ✅ —— 需求清单 14 项,14 项均可基于飞书官方 SDK(`@larksuiteoapi/node-sdk` 1.73.0)与 DSH 现有扩展点实现。
4
+ > 验证方式:官方 Node SDK 1.73.0 npm 包类型定义 + 官方文档原文 + 社区先例(nanobot、dsh-im-feishu)。
5
+
6
+ ## 1. 结论
7
+
8
+ | # | 需求点 | 结论 | 支撑 |
9
+ |---|--------|------|------|
10
+ | 1 | 一个 dsh 实例对应一个飞书机器人 | ✅ | 插件读取单组 appId/appSecret 凭据,启动一个 WSClient 长连接 |
11
+ | 2 | 私聊 | ✅ | `im.message.receive_v1` 事件 `chat_type = "p2p"` |
12
+ | 3 | 群聊 @ 机器人 | ✅ | 事件 `mentions[]` 含被 @ 成员,比对机器人 open_id |
13
+ | 4 | 话题群 @ 机器人 | ✅ | 话题群消息事件自带 `thread_id`,@ 判定同 #3 |
14
+ | 5 | 收到消息立刻回 “Typing” 表情 | ✅ | `im.v1.message.reaction.create`,官方内置表情 `emoji_type: "Typing"`(官方表情文案说明文档收录,非自定义表情) |
15
+ | 6 | 立刻基于该条消息创建话题 | ✅ | `im.v1.message.reply` + `reply_in_thread: true`(官方文档明示“以话题方式进行回复(即创建话题)”);响应返回 `thread_id` |
16
+ | 7 | 私聊也能创建话题 | ⚠️ 大概率 ✅ | `reply_in_thread` 文档未限制 chat_type;飞书客户端支持单聊话题。**需真机验证**,失败则降级为“单聊整体即话题”(见 §7 风险 R1) |
17
+ | 8 | 基于话题创建 session,话题内后续消息延续该 session(无需再 @) | ✅ | 事件携带 `thread_id`;插件维护 `thread_id → sessionId` 映射,`thread_id` 命中即路由到既有 agent(`agent.followup()`) |
18
+ | 9 | 私聊:**每个话题一个工作区** | ✅ | DSH 工作区=磁盘目录:`<data>/topics/p2p/<open_id>/<topic>/`,`ctx.workspaceRegistry.create(path, title)` |
19
+ | 10 | 群聊:**每个话题一个工作区**(按 用户 × 群 分组,同群不同用户互不影响) | ✅ | `<data>/topics/groups/<chat_id>/<open_id>/<topic>/`,session 以 `meta.cwd` 落盘归属该工作区 |
20
+ | 11 | 流式卡片(card schema 2.0),500ms 刷新 | ✅ | `msg_type: "interactive"` 卡片 2.0 JSON;`im.v1.message.patch` 整体替换卡片。单条消息更新限频 **5 QPS**(官方文档),2 QPS(500ms)安全 |
21
+ | 12 | 折叠面板:带边框、默认折叠、成功三态与失败终态、右侧箭头随折叠翻转 | ✅ | 卡片 2.0 原生组件 `collapsible_panel`:`border`、`expanded: false`、`header.title`、`icon` + `icon_expanded_angle: 180`(展开时箭头旋转向上) |
22
+ | 13 | 正文富文本 | ✅ | 卡片 2.0 `markdown` 组件(lark markdown 子集) |
23
+ | 14 | session 信息两行小字 | ✅ | `note` 组件 + `img` 图标;数据来自 DSH:session id、usage(input/output/cacheRead/cacheWrite)、`ctx.tokenMeter` 上下文压力、模型/推理强度 |
24
+ | 15 | 流式结束:移除 Typing;成功改为 DONE,失败改为 No | ✅ | 读取 Harness `turn/end.reason`;`kind: error` 渲染结构化错误并 create `emoji_type: "No"`,其余终态 create `emoji_type: "DONE"` |
25
+
26
+ **结论:第一版可以实现,全部使用飞书官方 SDK,无需任何非官方接口或 hack。**
27
+
28
+ ## 2. 飞书侧事实依据(验证过的官方文档原文)
29
+
30
+ - 表情:官方[表情文案说明](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message-reaction/emojis-introduce) 收录 `Typing` 与 `DONE` 两个内置表情,`emoji_type` 直接填 `"Typing"` / `"DONE"`,**无需上传自定义表情**。
31
+ - 创建话题:官方[话题概述](https://open.feishu.cn/document/im-v1/message/thread-introduction) 场景表原文——“以话题的方式进行回复(即创建话题):调用回复消息接口时,填写参数 `reply_in_thread: true`”。
32
+ - 话题延续:回复[回复消息](https://open.feishu.cn/document/server-docs/im-v1/message/reply) 接口文档——“如果要回复的消息已经是话题形式的消息,则默认以话题形式进行回复”,即对话题内消息 reply 会自动留在原话题。
33
+ - 事件:`im.message.receive_v1` 的 `message` 含 `message_id/root_id/parent_id/chat_id/thread_id/chat_type/content/mentions`(SDK 1.73.0 类型定义)。
34
+ - 卡片更新:[更新已发送的消息卡片](https://open.feishu.cn/document/server-docs/im-v1/message-card/patch) ——“单条消息更新频控为 5 QPS”,应用级 1000 次/分钟、50 次/秒;卡片消息体上限 **30 KB**。
35
+ - 卡片 2.0 折叠面板:[collapsible_panel](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/containers/collapsible-panel) —— 原生支持 `expanded`、`border`、`header.icon`、`icon_expanded_angle`(展开时图标旋转角度)。
36
+ - 发送限频:向同一用户 / 同一群发送消息均为 5 QPS(回复消息接口文档)。
37
+ - 权限:`im:message.p2p_msg:readonly`(读用户私聊消息)、接收群聊中 @ 机器人消息事件、`im:message`、`im:message.reactions:write_only`、`im:message:update`(卡片更新)。
38
+
39
+ ## 3. 总体架构
40
+
41
+ ```
42
+ 飞书客户端 ──(WS 长连接, 免公网)──► @larksuiteoapi/node-sdk WSClient
43
+ │
44
+ ▼
45
+ ┌───────────────────────────────┐
46
+ │ dsh-channels (本仓库) │
47
+ │ │
48
+ │ 事件路由器 │
49
+ │ ├─ 过滤机器人自己的消息 │
50
+ │ ├─ 解析 text/post 内容 │
51
+ │ ├─ @ 判定 (mentions) │
52
+ │ └─ thread_id → 会话路由器 │
53
+ │ │
54
+ │ FeishuSessionManager │
55
+ │ ├─ storageDomain 持久化: │
56
+ │ │ thread_id → sessionId │
57
+ │ │ workspaceKey 注册表 │
58
+ │ ├─ 懒恢复: ctx.agents.resume │
59
+ │ └─ 新建: ctx.agents.create │
60
+ │ (meta.cwd → 工作区目录) │
61
+ │ │
62
+ │ CardStreamingRenderer │
63
+ │ ├─ 订阅 session/event │
64
+ │ │ (assistant/chunk: │
65
+ │ │ 文本 + reasoning 增量) │
66
+ │ ├─ 500ms 节流 → patch 卡片 │
67
+ │ └─ turn/end → 成功 DONE / 失败 No │
68
+ │ │
69
+ │ ReactionController │
70
+ │ └─ Typing 添加/移除、DONE/No 添加 │
71
+ └───────────────┬───────────────┘
72
+ │ ctx.agents / ctx.workspaceRegistry
73
+ │ ctx.tokenMeter / ctx.storageDomain
74
+ ▼
75
+ DSH Host(agent-loop + LLM + persistence)
76
+ ```
77
+
78
+ ### 3.1 关键映射与状态
79
+
80
+ | 概念 | 映射 | 存储 |
81
+ |------|------|------|
82
+ | 工作区(私聊) | `(bot, open_id, topic)` → 目录 `<data>/topics/p2p/<open_id>/<topic>/` | `ctx.workspaceRegistry` + 磁盘 |
83
+ | 工作区(群聊) | `(bot, chat_id, open_id, topic)` → 目录 `<data>/topics/groups/<chat_id>/<open_id>/<topic>/` | 同上 |
84
+ | session | `thread_id` → DSH `sessionId`(每话题一个) | 插件 `storageDomain` 域 `channels_routes` |
85
+ | 会话延续 | 事件 `thread_id` 命中映射 → `agent.followup()` | — |
86
+
87
+ ### 3.2 消息处理流程
88
+
89
+ **首次触发(私聊任意消息 / 群聊 @ 消息):**
90
+
91
+ 1. 收到事件 → 异步 `reaction.create("Typing")`(非阻塞,记录 reaction_id)。
92
+ 2. 确定工作区目录(不存在则 mkdir + `workspaceRegistry.create`)。
93
+ 3. 通过 `im.message.reply(message_id, { reply_in_thread: true, msg_type: "interactive", content: 卡片初始 JSON })` 创建话题并发出卡片第一帧(折叠面板标题“推理中”),拿到 `thread_id` + `message_id`。
94
+ 4. `ctx.agents.create({ sessionId: <uuid>, meta: { cwd: 工作区目录 }, agentOptions: { provider, model, maxTokens }, setup: 安装 reasoning effort / preset 等 })`;`setup` 中注入首个 user 消息(`seed` 或创建后 `followup`)。
95
+ 5. 持久化 `thread_id → sessionId` 映射。
96
+
97
+ **话题内续聊(无需 @):**
98
+
99
+ 1. 事件带 `thread_id` 且映射命中 → `agent.followup(user message)`(排队进 next-turn 队列)。
100
+ 2. 若 agent 已因重启消失 → `ctx.agents.resume({ resumeSessionId })` 懒恢复后 followup。
101
+
102
+ **话题群(话题形式群):** 事件必带 `thread_id`;未映射的话题上收到 @ 消息 → 建 session;已有映射 → 直接续聊。机器人不在话题群内“创建”话题(话题由成员在客户端创建),只在话题内回复。
103
+
104
+ **流式渲染:**
105
+
106
+ - 订阅 `session/event`,按 `(sessionId, turn)` 聚合:`assistant/chunk` 里 `text-delta` → 正文,`reasoning-delta` → 折叠面板内容。
107
+ - **活动状态两行**(互相独立、互不顶替):**状态行**折叠 `tool-workflow/run-start / agent-start / agent-end / run-end`(workflow 进度:完成数/总数、阶段、活动成员、结束原因),run 结束转为 `结束(完成)` 并**保留到最终卡片**(后续工具调用/正文不顶掉);子代理完成后汇总为 `子代理完成:a、b` 同样留在最终卡片;**工具行**显示父会话当前 `tool/call`(subagent 取 description、workflow 取 meta.name),`tool/result` 即清、最终卡片收起。两行可同屏(如 `🧵 工作流 xxx 结束(完成)` + `🔧 调用工具:bash`);无正文输出的等待期以 `statusRefreshMs`(默认 5s)慢心跳刷新“已运行 Ns”计时——进入 subagent / workflow 时卡片不再静默等待。
108
+ - **子代理进度(跨会话映射)**:子代理是独立 session(header `parentSession` 指向主会话、`origin: 'subagent'`),其 `subagent/descriptor`(label)、`tool/call`(当前工具)、`turn/end`(结束移除)经全局 `session/event` 总线折叠进主卡片的同一状态行;workflow 成员经 `tool-workflow/agent-start` 的 `childId` 登记在 run 折叠里、跳过通用子代理折叠以免重复。**只显示状态不显示输出**:子代理正文是父模型的中间产物(工具契约“You receive its result, not its intermediate steps”),绝不进卡片正文。
109
+ - 500ms 节流器(仅内容变化时 patch),拼接卡片 2.0 JSON 全量 `im.v1.message.patch`。
110
+ - 折叠面板成功三态:首个 reasoning 增量前“推理中”;推理结束、正文开始后“回复中”;成功 `turn/end` 后“回复结束”。失败 `turn/end` 则进入“回复失败”。
111
+ - 终态:`turn/end.reason.kind === "error"` 时卡片进入「回复失败」并展示 `error.message` / `error.code`,再执行 `reaction.delete(Typing)` → `reaction.create("No")`;其他终态进入「回复结束」并添加 `DONE`。每轮模型头与 usage 单独折叠,失败发生在 `request/header` 前时不显示上一轮的模型、推理强度或缓存命中。
112
+
113
+ ### 3.3 卡片 JSON(卡片 JSON 2.0 结构)
114
+
115
+ > 整卡采用**当前 2.0 结构**:声明 `schema: "2.0"`,组件统一放在 `body.elements` 下。2.0 是富文本(标题/引用/表格/代码块)和流式能力的第一等结构,官方 Lark 机器人扩展现行实现同款。注意两点踩坑史:
116
+ > - `schema: "2.0"` 与顶层 `elements` 混用会被拒(`230099 / 200621: unknown property, property: elements`)——2.0 的组件必须在 `body` 里;
117
+ > - 折叠面板 header 的 `icon` 只支持 `standard_icon`(图标库 token)或 `custom_icon`,`tag: "img"` 非法(面板会被客户端丢弃);此处用内置 `down-small-ccm_outlined` 下箭头,无需上传图片。
118
+
119
+ ```jsonc
120
+ {
121
+ "schema": "2.0",
122
+ "config": { "width_mode": "fill" },
123
+ "body": {
124
+ "elements": [
125
+ { // 第一段:推理折叠面板
126
+ "tag": "collapsible_panel",
127
+ "expanded": false,
128
+ "border": { "color": "grey", "corner_radius": "8px" },
129
+ "header": {
130
+ "title": { "tag": "plain_text", "content": "推理中" }, // 推理中|回复中|回复结束
131
+ "icon": { "tag": "standard_icon", "token": "down-small-ccm_outlined" }, // 折叠向下
132
+ "icon_position": "right",
133
+ "icon_expanded_angle": 180 // 展开时旋转 180° → 箭头向上
134
+ },
135
+ "elements": [ { "tag": "markdown", "content": "<推理内容>" } ]
136
+ },
137
+ { "tag": "hr" },
138
+ { // 第二段:回复正文(富文本 markdown,2.0 全语法)
139
+ "tag": "markdown", "content": "<正文>"
140
+ },
141
+ { "tag": "hr" },
142
+ { // 第三段:session 信息,两行小字(2.0 无 note 组件 → markdown + notation 字号)
143
+ "tag": "markdown",
144
+ "text_size": "notation",
145
+ "content": "🔑 om_session_xxxx · ⬆ input token 12,345 · ⬇ output token 678"
146
+ },
147
+ { "tag": "markdown", "text_size": "notation",
148
+ "content": "🧠 模型 deepseek-v4-pro(max) · ⚡ 缓存命中 87.3% · 📊 上下文 24.1k/128k" }
149
+ ]
150
+ }
151
+ }
152
+ ```
153
+
154
+ - 第一行:`session id · input token · output token`(session 累计;可配置为按本轮回复合计)。
155
+ - 第二行:`模型 model(effort) · 缓存命中 cacheRead/(cacheRead+input) · 上下文 pressureTokens/contextWindow`。
156
+ - 单卡片 JSON 目标 ≤ 28 KB(飞书上限 30 KB,留 2 KB 冗余给切分后 token 数字增长);正文超长时**自动分段投放**——在段落/标题/代码块边界切分,**每张卡最多 5 个表格**(飞书硬限制 230099/11310,表格绝不从中间切断),续片以「回复分段 n」卡片跟进同一话题,全文无丢失;续卡被拒收时自动降级为纯文本消息;推理不跨卡,面板有独立的 5 KB 上限(`REASONING_MAX_BYTES`,含截断标记,超出保留头部 + 截断提示)且面板内表格省略;分段时预留 `CARD_PANEL_RESERVE_BYTES`(5.5 KB)给面板,正文与面板合计不超单卡目标。
157
+
158
+ ## 4. DSH 侧集成点(全部为公开扩展点)
159
+
160
+ | 用途 | API |
161
+ |------|-----|
162
+ | 建 session + agent | `ctx.agents.create({ sessionId, meta: { cwd }, agentOptions, setup })` |
163
+ | 恢复 session | `ctx.agents.resume({ resumeSessionId, setup })` |
164
+ | 续聊入队 | `agent.followup(message)`(next-turn FIFO,自动唤醒驱动) |
165
+ | 流式增量 | 订阅 `session/event`,`assistant/chunk`(`StreamChunk` 含 `text-delta` / `reasoning-delta`),`assistant/message` 锚点含 usage |
166
+ | token / 缓存 / 上下文 | usage:`{ inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens }`;上下文:`ctx.tokenMeter.measure(session)` + `ctx.llm.resolveModelInfo().context` |
167
+ | 工作区 | `ctx.workspaceRegistry.create(path, title)`(目录存在才可注册) |
168
+ | 插件自身状态 | `ctx.storageDomain`(defineDomain + sqlite/json 后端) |
169
+ | 加载方式 | 用户 cordis.yml 增加 `- name: '/path/to/plugin/src/index.ts'`(教程 07 确认本地文件路径加载,支持 HMR) |
170
+
171
+ 依赖宿主已配置:`dsh-agent-loop`、`dsh-llm-deepseek`、`dsh-session-persistence-*`、`dsh-storage-*`、`dsh-token-meter`(推荐)、`dsh-workspace`。
172
+
173
+ ## 5. 风险与不确定点
174
+
175
+ | # | 风险 | 等级 | 对策 |
176
+ |---|------|------|------|
177
+ | R1 | 私聊 `reply_in_thread` 是否生效未经文档明示 | 中 | 真机验证;失败自动降级:私聊整体视为“话题”,续聊继续同一 session(行为退化为常规 DM 机器人),不阻塞主流程 |
178
+ | R2 | patch 全量替换卡片:用户手动展开折叠面板后,下一次 patch(500ms 内)会把它重新折叠 | 低(体验) | v1 接受:流式期间保持默认折叠;流式结束后不再 patch,用户可自由展开。备选:CardKit 2.1 局部更新(需评估申请门槛),列入 v2 |
179
+ | R3 | 卡片 30 KB 上限 / 单卡表格数量上限(230099/11310) | 低 | 单卡预算 28 KB(2 KB 冗余);正文超长按干净断点分段投放(每卡 ≤ 5 表,表格不断裂,全文保留),分段预算预留面板 5.5 KB;续卡被拒降级纯文本;推理面板独立 5 KB 上限,与正文长度无关 |
180
+ | R4 | 消息/表情/更新的 5 QPS 限频 | 低 | patch 2 QPS 天然安全;多个并发 session 时加全局节流与 429 退避重试(错误码 230020) |
181
+ | R5 | 机器人收不到自己发的消息(需过滤) | 低 | 事件 `sender.sender_type` / open_id 过滤 |
182
+ | R6 | 用户撤回触发消息 | 低 | reaction 调用失败静默容忍(错误码 231002 等),不阻断流程 |
183
+ | R7 | 话题群中机器人回复方式 | 低 | 对话题内消息 reply 默认即话题回复(官方文档确认),无需 reply_in_thread |
184
+
185
+ ## 6. 实施计划
186
+
187
+ | 里程碑 | 内容 | 产出 |
188
+ |--------|------|------|
189
+ | M0 脚手架 | pnpm workspace、`src/index.ts` 插件骨架(apply(ctx) + Config schema)、类型声明合并 | 可加载的空插件 |
190
+ | M1 飞书接入 | WSClient 长连接、事件订阅、文本/post 解析、@ 判定、凭据配置(`ctx.credentials` 凭据库引用,不落明文) | 能收消息并回文本 |
191
+ | M2 话题与路由 | Typing/DONE reaction、reply_in_thread 建话题、storageDomain 路由表、懒恢复、工作区目录创建 | 私聊/群聊/话题群三通道贯通 |
192
+ | M3 流式卡片 | 卡片 2.0 渲染器、session/event 订阅、500ms 节流 patch、折叠面板三态、session 信息两行 | 完整流式体验 |
193
+ | M4 联调加固 | 真机自建应用全流程验证(含 R1 私聊话题)、限频/退避/异常兜底、README + 配置文档 | 可交付 v1 |
194
+
195
+ ## 7. 仓库结构(计划)
196
+
197
+ ```
198
+ dsh_channels/
199
+ ├── docs/feasibility-and-design.md # 本文档
200
+ ├── src/
201
+ │ ├── index.ts # 插件入口 apply(ctx)
202
+ │ ├── config.ts # 配置 schema(appId/appSecret 为凭据引用名,经 ctx.credentials 解析)
203
+ │ ├── feishu/client.ts # WSClient 生命周期
204
+ │ ├── feishu/events.ts # 事件路由/过滤/@ 判定
205
+ │ ├── feishu/reactions.ts # Typing/DONE 控制器
206
+ │ ├── feishu/threads.ts # reply_in_thread 建话题/回复
207
+ │ ├── feishu/card.ts # 卡片 2.0 JSON 渲染器(折叠面板/正文/信息行)
208
+ │ ├── core/workspace.ts # (chat,user) → 工作区目录
209
+ │ ├── core/sessions.ts # thread_id → sessionId 路由 + 懒恢复
210
+ │ └── core/streamer.ts # session/event 聚合 + 500ms 节流 patch
211
+ ├── tests/ # 以 stub 官方 SDK 的单元测试
212
+ └── package.json # 依赖 @larksuiteoapi/node-sdk + DSH peer 包
213
+ ```
@@ -0,0 +1,268 @@
1
+ # dsh-channels · 真机联调手册
2
+
3
+ 本文档指导你**手动安装** dsh-channels 插件到本机 dsh,配置飞书机器人,并逐项验证私聊/群聊/话题群三条通道的流式卡片体验。所有步骤均已在本机 dsh checkout(web profile)上实测通过。
4
+
5
+ > 约定:下文用 `$HARNESS` 表示你的 deepseek-harness 仓库目录,`$PLUGIN` 表示本仓库目录(dsh_channels)。
6
+
7
+ ## 0. 前置条件
8
+
9
+ - dsh 仓库可运行(`cd $HARNESS && pnpm dsh --help` 正常)
10
+ - 飞书企业账号 + 开发者后台访问权限
11
+ - 一个飞书测试群(用于群聊 / 话题群验证)
12
+
13
+ ## 1. 飞书开放平台配置
14
+
15
+ 1. 打开 [飞书开放平台](https://open.feishu.cn/app),创建**企业自建应用**,名称随意(如 dsh-channels)。
16
+ 2. 进入应用 → **添加应用能力** → 开启 **机器人**。
17
+ 3. **权限管理**,开通以下权限(发布版本时生效):
18
+ | 权限 | 用途 |
19
+ |------|------|
20
+ | `im:message`(获取与发送单聊、群组消息) | 收发消息、回复话题 |
21
+ | `im:message.p2p_msg:readonly`(读取用户发给机器人的单聊消息) | 私聊通道 |
22
+ | `im:message.reactions:write_only`(发送、删除消息表情回复) | Typing / DONE / No 表态 |
23
+ | `im:message:update`(更新消息卡片) | 流式卡片刷新(patch) |
24
+ | `im:chat:readonly` 一类(获取群信息) | 入群通知卡展示群名/成员数(具体 scope 以控制台要求为准) |
25
+
26
+ > 卡片更新接口本身支持 `im:message` 一项即可,但建议把 `im:message:update` 也加上,避免组合差异。
27
+ 4. **事件与回调** → 事件订阅方式选择 **使用长连接接收事件**(WebSocket,免公网 IP)。
28
+ - 添加事件:**接收消息 im.message.receive_v1**(勾选“接收群聊中 @ 机器人消息”)。
29
+ - 添加事件:**机器人进群 im.chat.member.bot.added_v1**(机器人被拉进群后私聊管理员发群信息卡;未订阅时退化为“群里第一次未授权 @ 时补发”)。
30
+ - 添加回调:**卡片回传交互 card.action.trigger**(HITL 审批、授权卡的按钮回传,同样走长连接)。
31
+ 5. **凭证与基础信息** 页复制 `App ID` 与 `App Secret`。
32
+ 6. **版本管理与发布** → 创建版本(权限与事件配置随版本生效)→ 发布。
33
+ 7. **可用范围**:把自己(和测试同事)加入可用范围。
34
+ 8. 建一个测试群,把机器人拉进群;如需验证话题群,另建一个“话题群”(创建群时选话题模式),同样拉入机器人。
35
+
36
+ ## 2. 安装插件到 dsh
37
+
38
+ dsh 的插件装在 **profile** 里(`$DSH_HOME/profiles/<name>/`,`$DSH_HOME` 未设置时默认 `~/.dsh`)。web 界面使用 `web` profile,首次启动自动初始化。安装命令把 pnpm 转发到 profile 目录执行:
39
+
40
+ 安装已发布的 npm 版本(推荐):
41
+
42
+ ```bash
43
+ cd $HARNESS
44
+ DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add @modesto_ma/dsh-channels
45
+ ```
46
+
47
+ 本地 checkout 开发时:
48
+
49
+ ```bash
50
+ cd $HARNESS
51
+ DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add "link:$PLUGIN"
52
+ ```
53
+
54
+ 说明:
55
+
56
+ - npm 安装直接使用包内已构建的 `lib/index.js` 与 `lib/client.js`;`link:` 则把本地插件 checkout 链接进 profile,安装前需在插件仓库执行 `pnpm build`。
57
+ - 本包声明了 `dsh.bundle`;安装成功后,`dsh plugin` 会自动把 `@modesto_ma/dsh-channels` 加入 profile 的 bundle 列表,并由包内 `cordis.patch.yml` 提供稳定 `id: dsh-channels` 的 Loader 行。
58
+ - pnpm 可能输出 `[WARN] Issues with peer dependencies`(`@deepseek-ai/cordis` 由 dsh 统一提供,属预期)与 `[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: protobufjs`——**安装实际已成功**,`dsh plugin` 的非零退出码可忽略;想消除告警可在 profile 目录的 `pnpm-workspace.yaml` 加 `onlyBuiltDependencies: [protobufjs]`。
59
+ - **改了插件代码后**:运行检查并重启 dsh 即可,不必重复安装;只有 `package.json`、bundle patch 或依赖关系发生变化时才重新执行 `add`。
60
+
61
+ 验证安装与组合(不启动服务):
62
+
63
+ ```bash
64
+ cd $HARNESS
65
+ DSH_HOME=$DSH_HOME pnpm dsh web --dump-config | grep -A 9 "dsh-channels"
66
+ # 安装完成后即应看到包内 bundle 提供的 Loader 行
67
+ ```
68
+
69
+ ## 3. 配置插件
70
+
71
+ ### 3.1 凭据写入 dsh 凭据库
72
+
73
+ 插件配置里只填**凭据引用名**,密钥本身放在 dsh 的受管凭据库 `$DSH_HOME/.credentials.yaml`(与 `DEEPSEEK_API_KEY` 同一文件,热加载,`DEEPSEEK_API_KEY` 就在里面):
74
+
75
+ ```yaml
76
+ # $DSH_HOME/.credentials.yaml
77
+ FEISHU_APP_ID: cli_xxxxxxxxxxxxxxxx
78
+ FEISHU_APP_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
79
+ ```
80
+
81
+ **权限必须是 600**(dsh 会强制校验,文件可被他人读取时直接拒绝启动):
82
+
83
+ ```bash
84
+ chmod 600 $DSH_HOME/.credentials.yaml
85
+ ```
86
+
87
+ 解析优先级(由 `dsh-credentials-local` 提供,热加载):进程环境变量 > `.credentials.yaml` > 启动目录 `.env` > `$DSH_HOME/.env`。不想用凭据库时,直接导出环境变量也能生效。凭据改动后重启 dsh 生效。
88
+
89
+ ### 3.2 写补丁层
90
+
91
+ 编辑 `$DSH_HOME/profiles/web/cordis.patch.yml`。包内 bundle 已插入 Loader 行,这里仅按稳定 id 覆盖该实例的完整配置:
92
+
93
+ ```yaml
94
+ # 用户补丁层:只配置 bundle 已提供的 dsh-channels 行
95
+ - id: dsh-channels
96
+ config:
97
+ appId: 'FEISHU_APP_ID' # 凭据引用名(密钥在 .credentials.yaml)
98
+ appSecret: 'FEISHU_APP_SECRET'
99
+ baseDir: './channels-data' # 工作区根目录(相对路径按 dsh 进程 cwd 解析;可在 Web 面板覆盖为绝对路径)
100
+ provider: 'deepseek-official' # 可选,省略用部署默认
101
+ model: 'deepseek-v4-flash' # 可选
102
+ reasoningEffort: 'high' # 可选,精确模型声明的档位 id
103
+ refreshMs: 500 # 卡片刷新间隔(下限 200ms)
104
+ requireMention: true # 普通群需 @ 机器人
105
+ dmMode: 'open' # 私聊通道开关
106
+ threadFallback: 'degrade' # 话题被拒时降级为会话级路由
107
+ approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟)
108
+ accessMode: 'guarded' # 准入模式:guarded(默认)| open(旧行为,不安全)
109
+ adminOpenId: 'ou_xxx' # 管理员 open_id(可留空,留空时 guarded 下拒绝一切对话)
110
+ authRequestTtlDays: 7 # 入群/用户授权申请卡有效期(天)
111
+ loggerLevel: 'info'
112
+ ```
113
+
114
+ 从旧版配置迁移时,删除原来的 `insert` / `id: channels` 条目;不要同时保留旧行和 `id: dsh-channels`,否则同一机器人会建立两个长连接并重复处理消息。
115
+
116
+ | 配置项 | 默认 | 说明 |
117
+ |--------|------|------|
118
+ | `appId` / `appSecret` | 必填 | 凭据引用名(shell 标识符),经 `ctx.credentials` 解析;明文密钥会被拒绝 |
119
+ | `baseDir` | `./channels-data` | 工作区目录树根:每个话题一个独立工作区,`topics/p2p/<open_id>/<topic>/`、`topics/groups/<chat_id>/<open_id>/<topic>/`(模型/工具产生文件的落点);Web 面板可覆盖,只接受绝对路径,`~` 展开,保存时创建并校验目录,只影响之后新建的话题 |
120
+ | `provider` / `model` | 部署默认 | 新建 session 的路由;省略时继承部署默认 |
121
+ | `reasoningEffort` | 适配器/provider 默认 | Session 初始/重置推理档位;可在话题内用 `/effort` 覆盖 |
122
+ | `maxTokens` | 适配器默认 | 每次模型请求输出上限 |
123
+ | `refreshMs` | `500` | 流式卡片刷新频率(200–10000) |
124
+ | `dmMode` | `open` | 私聊开关 |
125
+ | `requireMention` | `true` | 普通群是否需要 @ 机器人(话题内续聊不受影响) |
126
+ | `threadFallback` | `degrade` | 话题回复被拒时:`degrade` 降级为会话级延续 / `strict` 直接失败 |
127
+ | `approvalTimeoutMs` | `300000` | HITL 审批决策卡等待时长(10s–1h);超时自动拒绝 |
128
+ | `accessMode` | `guarded` | 准入模式:`guarded` 仅管理员+白名单;`open` 保留旧行为(任何可用范围用户可对话,不安全) |
129
+ | `adminOpenId` | 未设置 | 唯一管理员 open_id(`ou_` 开头);留空 + guarded = 拒绝一切对话(`/userid` 引导仍可用)。面板可改 |
130
+ | `authRequestTtlDays` | `7` | 入群/用户授权申请卡有效期(1–30 天) |
131
+ | `loggerLevel` | `info` | 官方 SDK 日志级别 |
132
+
133
+ ### 3.3 初始化管理员与授权(默认准入)
134
+
135
+ 插件默认 `accessMode: 'guarded'`:**没有配置管理员时,所有对话都会被拒绝**(这是 fail-closed 的安全默认值)。初始化顺序:
136
+
137
+ 1. 启动 dsh 后在飞书**私聊机器人**发送 `/userid`(未配置管理员时任何私聊用户都可以用,只回固定文本,不建会话)→ 复制返回的 `ou_...`;
138
+ 2. 打开 Web **设置 → 飞书渠道 → 常用配置**,把 `ou_...` 填进「管理员 open_id」并保存(立即生效);也可以直接写 profile 的 `adminOpenId`;
139
+ 3. 现在管理员私聊机器人即可正常对话。**其他用户私聊会收到一条固定提示**(“非管理员禁用私聊功能哦…”),不会建会话;
140
+ 4. **给群授权**(三种入口任选;目的是放行**非管理员**,管理员本人在任何群都能直接对话):
141
+ - 机器人被拉进群 → 管理员私聊收到**群信息卡**(群名/`chat_id`/成员数/邀请人)→ 点「授权本群」;
142
+ - 管理员在群里发 `/auth allow` → 回执「已授权本群」;`/auth revoke` 撤销;
143
+ - Web 面板「访问与授权 → 已授权群」手动添加 `chat_id`(可用 `/groupid` 查看);
144
+ 5. **给用户授权**(三种入口任选):
145
+ - 已授权群里未授权用户 @ 机器人 → 管理员收到**用户授权申请卡**(含昵称/open_id)→ 点「授权用户」;
146
+ - 管理员发 `/auth user add ou_xxx`;`/auth user remove ou_xxx` 撤销;
147
+ - Web 面板「访问与授权 → 已授权用户」手动添加 open_id(用户可用 `/userid` 查看);
148
+ 6. 授权/撤销**立即生效**;白名单与待处理申请持久化在插件 `channels_routes` 域,重启不丢。`/auth list` 可随时查看白名单与待处理申请。
149
+
150
+ > 说明:**管理员不受白名单限制**——机器人被拉进新群后,管理员可以直接 @ 机器人开会话,不需要先授权该群;授权群/用户只影响**非管理员**。管理员在任意群里都可以使用 `/auth`、`/groupid` 等命令。
151
+
152
+ 验证组合无误:
153
+
154
+ ```bash
155
+ cd $HARNESS
156
+ DSH_HOME=$DSH_HOME pnpm dsh web --dump-config | grep -B 1 -A 10 "name: dsh-channels"
157
+ ```
158
+
159
+ ## 4. 启动与日志判读
160
+
161
+ ```bash
162
+ cd $HARNESS
163
+ DSH_HOME=$DSH_HOME pnpm dsh web # 默认 http://127.0.0.1:3080
164
+ ```
165
+
166
+ **连接成功**(终端会打出):
167
+
168
+ ```
169
+ [dsh-channels] feishu long connection established, bot open_id: ou_xxxxxxxx
170
+ [dsh-channels] HITL 审批应答已就绪(决策卡片超时 300000 ms)
171
+ [dsh-channels] 准入=guarded,管理员已配置
172
+ ```
173
+
174
+ > 准入相关控制台输出:`准入=open`(不安全,仅调试)、`准入=guarded,管理员已配置`、`准入=guarded 但未配置管理员 open_id:所有对话都会被拒绝…`(此时按 §3.3 配置管理员即可)。
175
+
176
+ > 第二行只在宿主挂载了 `approval` 服务时出现;若显示 `主机未挂载 approval 服务,HITL 审批应答关闭`,说明该部署未启用 HITL(`approval/request` 事件永远不会派发),并非插件故障。
177
+
178
+ **常见启动失败**:
179
+
180
+ | 输出 | 原因 |
181
+ |------|------|
182
+ | `could not resolve bot identity via /open-apis/bot/v3/info` | App ID/Secret 错误,或未开启机器人能力 |
183
+ | `permission_denied` / `invalid tenant access token` | 同上,先核对凭证再查权限 |
184
+ | `credential "FEISHU_APP_ID" is not configured` | `.credentials.yaml` 缺对应条目(或环境变量未导出),见 §3.1 |
185
+ | `is readable beyond its owner (mode 644)` | `.credentials.yaml` 权限过宽,`chmod 600` 后重启 |
186
+ | `dsh: plugin tree failed to load … dsh-channels` | 插件本身加载失败(多为 `name:` 拼写或未安装),看后续 cause |
187
+ | 无任何 `dsh-channels` 输出 | 插件未装载:检查 `--dump-config` 是否含条目、补丁层是否用了 `insert:` 格式 |
188
+
189
+ > 说明:插件内部其余日志走 dsh 的 cordis logger,web 组合下部分早期日志可能只进缓冲不落终端;上面两条**生命周期日志**(连接成功 / 启动失败)额外直接输出到控制台,联调时以它们为准。
190
+
191
+ ## 5. 三通道联调清单
192
+
193
+ | # | 场景 | 操作 | 预期 |
194
+ |---|------|------|------|
195
+ | 1 | 私聊 | 给机器人发一条消息 | 消息下立刻出现 **Typing** 表情 → 以话题形式回复卡片(**推理中** 折叠面板)→ 正文流式输出(500ms 刷新,面板变 **回复中**)→ 成功变 **回复结束**、Typing 变 **DONE**;卡片底部两行 session 信息 |
196
+ | 1a | 错误推理强度 | 配置该模型不支持的 `reasoningEffort` 后发消息 | 卡片变 **回复失败**,显示 Harness 返回的错误消息和错误码(如 `UNSUPPORTED_REASONING_EFFORT`),Typing 变 **No**;模型/推理强度/缓存不沿用上一轮 |
197
+ | 1b | Session 推理强度切换 | 话题内依次执行 `/effort`、`/effort tiny`、发普通消息、重启后续聊、`/effort reset` | 首次显示配置来源;切换后下一轮请求使用 `tiny`,重启恢复后仍保持;reset 后恢复配置值。若模型不支持 `tiny`,下一轮按 1a 失败,不会静默回退 |
198
+ | 2 | 私聊续聊 | 在刚建的话题里直接再发消息(**不 @**) | 继续同一 session:卡片底部 session id 不变,input/output token 累计增长 |
199
+ | 3 | 群聊 @ | 在普通群 @ 机器人发消息 | 同 #1;该消息成为新话题根 |
200
+ | 4 | 群聊话题内 | 在上一步的话题里直接发消息(不 @) | 延续该 session,不再要求 @ |
201
+ | 5 | 同群第二个用户 | 换另一个人 @ 机器人 | 新话题 + 新 session;dsh 工作区列表中出现**独立的第二个工作区**(`topics/groups/<chat_id>/<用户 open_id>/<topic>/`),与原用户的话题工作区互不可见 |
202
+ | 6 | 话题群 | 在话题群某话题中 @ 机器人 | 机器人直接在该话题内回复卡片(不新建话题) |
203
+ | 7 | 折叠面板 | 点面板标题 | 展开推理内容,右侧箭头翻转向上;再点收起 |
204
+ | 8 | 重启恢复 | 重启 dsh 后在旧话题里发消息 | 机器人懒恢复原 session,token 统计继续累计 |
205
+ | 9 | 降级路径 | 在关闭话题功能的群里 @ 机器人 | 正常回复(普通回复气泡),后续 @ 延续同一 session |
206
+ | 10 | HITL 审批 | 让模型执行需要审批的动作(如提示沙箱升级:`bash` 被沙箱拒绝后带 `sandbox_permissions` 重试) | 同一话题内出现独立的「⚠️ 需要你的审批」卡片(✅ 允许 / ❌ 拒绝);点允许后卡片变「✅ 已允许执行」且动作继续,点拒绝变「❌ 已拒绝」;不点则到时(默认 5 分钟)变「⏱ 已超时未处理,自动拒绝」 |
207
+ | 11 | 管理员初始化(准入) | 未配置 `adminOpenId` 时私聊 `/userid`,把 open_id 填入面板保存 | 未配置时 `/userid` 也只回固定文本、不建会话;保存后面板显示管理员已配置,管理员私聊正常对话 |
208
+ | 12 | 非管理员私聊 | 用另一个飞书账号私聊机器人 | 只收到一条固定提示(“非管理员禁用私聊功能哦…”);不建话题/会话、不产生模型调用;该账号发 `/userid` 仍可拿到自己的 ID |
209
+ | 13 | 入群通知 | 把机器人拉进一个新群 | 管理员私聊收到群信息卡(群名/`chat_id`/成员数/邀请人),未授权群带「授权本群 / 忽略」按钮;群里没有任何机器人消息 |
210
+ | 14 | 管理员直通(免群授权) | 刚被拉进未授权群时,管理员直接 @ 机器人发消息 | 正常开会话(管理员不受白名单限制);不授权该群也不影响管理员使用 |
211
+ | 14a | 群授权与群内准入 | 管理员点「授权本群」(或在群内发 `/auth allow`);未授权用户 @;管理员 `/auth user add <open_id>` 后该用户再 @ | 授权前非管理员 @ 无响应(管理员除外);授权后白名单用户可分别开会话;未授权用户 @ 不建会话且管理员收到用户授权卡;授权该用户后可正常开会话 |
212
+ | 15 | 撤销与持久化 | `/auth revoke`(群内)/ `/auth user remove <open_id>`;重启 dsh 后再试 | 撤销后对应主体立即无法开启对话;重启后已授权群/用户仍有效(持久化) |
213
+ | 16 | 话题越权防护 | 授权群里用户 A 创建话题后,用户 B 在 A 的话题里发普通消息/附件/回答提问 | B 的消息被静默丢弃,不会进入 A 的 Session、不会写入 A 的工作区;A 自己发言正常 |
214
+
215
+ ### 5.0 HITL 审批验证点(重点)
216
+
217
+ 1. **卡片形态**:审批卡是流式卡之外**新发的独立消息**,锚定在最新流式卡下、同一话题内;流式卡本身不带按钮。
218
+ 2. **按钮回传**:待审批时允许/拒绝按钮在同一行等宽排列;点击后卡片立即更新为终态并移除两个按钮。若点击无反应,先核对 §1 第 4 步是否已订阅 **card.action.trigger** 回调并**重新发布版本**(回调订阅随版本生效)。
219
+ 3. **归属校验**:群里非发起人点击审批卡:卡片出现「ℹ️ 仅发起该会话的用户可操作此卡片」提示,动作不生效。
220
+ 4. **超时**:`approvalTimeoutMs` 内不点 → 卡片变「已超时」,动作被自动拒绝(fail-closed),模型收到拒绝结果后会换路径继续。
221
+ 5. **多会话并行**:不同话题的审批互不串扰(每张卡只结算自己的问题)。
222
+
223
+ ### 5.1 私聊话题验证点(重点)
224
+
225
+ 飞书官方文档没有明示 `reply_in_thread` 对**私聊**的约束。联调时在私聊里观察第 1 步:消息是否真的聚合成“话题”形态(话题气泡)。若飞书拒绝话题回复,插件按 `threadFallback: 'degrade'` 自动改为普通回复,此时私聊内所有消息延续同一个 session(等价于“私聊整体即话题”),功能不受阻。若你希望私聊必须话题形态,把 `threadFallback` 设为 `strict` 并反馈错误码。
226
+
227
+ ### 5.2 验证工作区归属
228
+
229
+ 打开 dsh Web GUI → 工作区列表,应看到:
230
+
231
+ ```
232
+ 飞书私聊 · ou_xxx · omt_xxx (对应 <baseDir>/topics/p2p/ou_xxx/omt_xxx/)
233
+ 飞书群聊 · oc_yyy · ou_xxx · omt_xxx (对应 <baseDir>/topics/groups/oc_yyy/ou_xxx/omt_xxx/)
234
+ ```
235
+
236
+ 每个新话题会创建新的工作区目录;同一话题内 `/renew` 仍复用该工作区。
237
+
238
+ ## 6. 常见问题排查
239
+
240
+ | 现象 | 排查 |
241
+ |------|------|
242
+ | 私聊无响应 | 用户是否在应用**可用范围**内;权限 `im:message.p2p_msg:readonly` 是否随版本发布;若返回的是“非管理员禁用私聊功能哦”,说明准入生效(把该用户加入白名单或让管理员对话) |
243
+ | 私聊提示“机器人尚未配置管理员” | `accessMode: 'guarded'` 且未配置 `adminOpenId`:按 §3.3 在面板填写管理员 open_id |
244
+ | 机器人被拉进群但管理员没收到群信息卡 | 是否订阅 **im.chat.member.bot.added_v1** 事件并重新发布版本;群信息读取权限是否已开通(`getChatInfo` 失败只会缺群名,不影响卡片本体) |
245
+ | 群内 @ 机器人无响应(登录台无 access 报错) | 管理员不受白名单限制,可直接对话;**非管理员**需要该群已授权且自己在用户白名单里。可用 `/auth status`(管理员)或 Web 面板「访问与授权」确认 |
246
+ | `/auth` 无反应 | `/auth` 仅管理员可用;非管理员在群里会被静默忽略、私聊会收到固定提示 |
247
+ | 群聊 @ 无响应 | 事件订阅是否勾选“接收群聊中 @ 机器人消息”;机器人是否在群内且未被群主禁言 |
248
+ | 有 Typing 但无卡片 | 权限 `im:message` / `im:message:update`;卡片 2.0 需要较新的飞书客户端(升级客户端后重试) |
249
+ | 卡片正文只到某长度 | 旧版行为;新版单卡预算 28 KB(30 KB 上限留 2 KB 冗余),超长正文自动分段为多张续卡(「回复分段 n」,每卡 ≤ 5 个表格),全文保留;推理面板独立 5 KB 上限,超出的推理只保留开头并追加「已截断」提示 |
250
+ | 报错 230099 / `ErrCode: 11310 card table number over limit` | 飞书单卡表格数量超限(真机验证上限 5 个);新版已按“每卡最多 5 个表格”自动切分,不再触发;历史版本请重装插件并重启 dsh |
251
+ | 流式期间手动展开面板又被收起 | v1 已知取舍:patch 是全量替换卡片,500ms 内的下一次刷新会重置折叠状态;流式结束后可自由展开 |
252
+ | 错误码 `230013` | 用户不在可用范围 / 被禁用 |
253
+ | 错误码 `230071` | 该群不支持话题回复 → 自动降级路径(见 §5.1) |
254
+ | 错误码 `230099`(`parse card json err` / `unknown property, property: elements`) | 卡片结构混用:2.0 声明下把组件放在顶层 `elements`(应在 `body.elements`)。新版插件已改为规范 2.0 结构:重装插件并重启 dsh |
255
+ | 折叠面板不显示 / 正文不是富文本 | 旧版卡片的两个已知问题(`icon.tag: "img"` 非法、1.0 结构 markdown 语法受限),新版插件已切 2.0 结构 + 内置 `standard_icon` 箭头:重装插件并重启 dsh |
256
+ | 审批卡不出现 | 确认该部署启用了 HITL:启动日志应有「HITL 审批应答已就绪」;若显示「主机未挂载 approval 服务」则该部署不会派发审批请求;另外确认当前会话的审批策略不是 `never`(`/permission danger-full-access` 会关闭审批) |
257
+ | 审批卡点击无反应 | 未订阅 **card.action.trigger** 回调(或订阅后未重新发布版本),见 §1 第 4 步;按 §5.0 重新核对 |
258
+ | 错误码 `231001` / `231002` | 表态表情失败:表情名(`Typing`/`DONE`/`No`)必须一字不差;机器人需在会话内 |
259
+
260
+ ## 7. 改代码 → 生效的循环
261
+
262
+ ```bash
263
+ # 1) 修改 $PLUGIN 源码后:
264
+ cd $PLUGIN && pnpm typecheck && pnpm test
265
+ # 2) link 安装无需重装,直接重启:
266
+ cd $HARNESS
267
+ DSH_HOME=$DSH_HOME pnpm dsh web
268
+ ```