@cubeai/dsh-feishu-channels 0.0.0-stage → 0.2.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 (65) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +15 -0
  3. package/README.md +56 -2
  4. package/client.js +4189 -0
  5. package/cordis.patch.yml +4 -0
  6. package/docs/coexistence.md +112 -0
  7. package/docs/dsh-feishu-channels-plan.md +1406 -0
  8. package/docs/ui-revamp-plan.md +263 -0
  9. package/docs/verification.md +120 -0
  10. package/icon.svg +17 -0
  11. package/locale/en.json +6 -0
  12. package/locale/zh.json +6 -0
  13. package/package.json +76 -4
  14. package/src/channels/connection-state.js +12 -0
  15. package/src/channels/diagnostics.js +77 -0
  16. package/src/channels/factory.js +20 -0
  17. package/src/channels/feishu-app.js +267 -0
  18. package/src/channels/feishu.js +399 -0
  19. package/src/channels/media.js +9 -0
  20. package/src/channels/meta.js +24 -0
  21. package/src/channels/qr/feishu.js +84 -0
  22. package/src/channels/qr/hub.js +229 -0
  23. package/src/channels/qr/render.js +8 -0
  24. package/src/channels/qr/shared.js +50 -0
  25. package/src/engine/abort.js +35 -0
  26. package/src/engine/access.js +23 -0
  27. package/src/engine/agent-options.js +19 -0
  28. package/src/engine/approval.js +70 -0
  29. package/src/engine/assistant-settings.js +38 -0
  30. package/src/engine/assistant-text.js +11 -0
  31. package/src/engine/atomic-file.js +45 -0
  32. package/src/engine/chat-commands.js +752 -0
  33. package/src/engine/choice-delivery.js +20 -0
  34. package/src/engine/choices.js +103 -0
  35. package/src/engine/command-locale.js +70 -0
  36. package/src/engine/command-messages.js +434 -0
  37. package/src/engine/command-permissions.js +38 -0
  38. package/src/engine/command-replies.js +59 -0
  39. package/src/engine/credentials.js +54 -0
  40. package/src/engine/deferred-delivery.js +260 -0
  41. package/src/engine/delivery-history.js +39 -0
  42. package/src/engine/file-delivery.js +215 -0
  43. package/src/engine/file-input.js +52 -0
  44. package/src/engine/file-log.js +25 -0
  45. package/src/engine/gateway.js +1337 -0
  46. package/src/engine/image-input.js +31 -0
  47. package/src/engine/json-state.js +25 -0
  48. package/src/engine/keyed-queue.js +20 -0
  49. package/src/engine/merge.js +56 -0
  50. package/src/engine/message-progress.js +274 -0
  51. package/src/engine/question.js +201 -0
  52. package/src/engine/quiet-logger.js +25 -0
  53. package/src/engine/reply-stream.js +86 -0
  54. package/src/engine/router.js +713 -0
  55. package/src/engine/secure-file.js +8 -0
  56. package/src/engine/seen-store.js +43 -0
  57. package/src/engine/session-export.js +64 -0
  58. package/src/engine/session-history.js +30 -0
  59. package/src/engine/session-id.js +62 -0
  60. package/src/engine/session-store.js +109 -0
  61. package/src/engine/session-title.js +29 -0
  62. package/src/engine/split.js +291 -0
  63. package/src/engine/workspace-path.js +9 -0
  64. package/src/index.js +62 -0
  65. package/src/manager.js +1325 -0
@@ -0,0 +1,263 @@
1
+ # 飞书频道 UI / 应用配置改造 — 可行性分析
2
+
3
+ > 日期:2026-11-04
4
+ > 范围:在现有 `@cubeai/dsh-feishu-channels` 上调整设置入口、频道页、添加账号流程、扫码建应用后的配置、账号展示与账号设置。
5
+
6
+ ## 实施状态(2026-11-04)
7
+
8
+ - 1–4、6 已实现;`pnpm test` 93 项通过,`pnpm run check` 通过。
9
+ - `settings.section` 已无 `feishu-channel`;侧边栏仍保留 `任务 | 频道 | 飞书频道`。
10
+ - 扫码 begin 已用真实飞书接口验证,QR URL 包含 `name`、`createOnly=true`、`addons`。
11
+ - 应用配置 provisioning(名称/权限/重定向 URL)已实现;真实写入结果需要用户扫码创建测试账号后读取 `config.provisioning`。
12
+ - 真实测试确认:缺少 `application:application:patch` 时写入失败;补上该权限后 v7 返回 success,5 个重定向 URL 全部写入,保留该操作。
13
+
14
+ ## 1. 总结论
15
+
16
+ | # | 需求 | 可行性 | 关键说明 |
17
+ |---|---|---|---|
18
+ | 1 | 去掉 DSH 设置里的“飞书频道”,全部移到频道页 | **可** | 删除 `settings.section` 注册;频道页承载账号/设置/待批准 |
19
+ | 2 | 频道页标题换成“添加飞书账号”按钮;右侧图标按钮去掉 | **可** | 用自绘账号页替换官方任务树头部,最直接 |
20
+ | 3 | 点击按钮直接弹“配置飞书”对话框 | **可** | 频道页持有 `BindModal` 状态,不经过设置页 |
21
+ | 4 | 对话框改名、只留快捷绑定、加“频道名称” | **可** | 隐藏手动页签/表单;`settings.name` 贯穿 QR 和账号保存 |
22
+ | 5 | 新建 App 用输入名称、加两个权限、加 5 个重定向 URL | **部分可** | 名称/权限可在扫码确认页预填;重定向 URL 只能走应用管理 OpenAPI,存在平台限制,需真机验证 |
23
+ | 6 | 添加后立即显示账号名;加“设置”按钮;显示 App ID/Secret | **可** | 账号来自 `/channels` 的 `accounts`,不依赖首条消息;Secret 只做掩码展示 |
24
+
25
+ **结论:可以按这个方向改。** 1–4、6 是纯插件内改造;5 的“名称/权限”可以做预填,5 的“重定向 URL”要接受平台可能拒绝的事实,并准备降级提示。
26
+
27
+ ---
28
+
29
+ ## 2. 第 5 项:飞书平台能力核查
30
+
31
+ ### 2.1 名称:可以预填,不能保证锁定
32
+
33
+ 飞书 SDK 的 `registerApp` 支持 `appPreset.name`,它会被编码成二维码 URL 的 `name` 参数:
34
+
35
+ ```text
36
+ verification_uri_complete?name=<输入的名称>
37
+ ```
38
+
39
+ SDK 文档明确说明:这是**预填**,用户扫码后仍可在飞书确认页修改;最终名称以用户提交的为准。
40
+
41
+ 因此:
42
+
43
+ - 我们在二维码 URL 里带上用户输入的“频道名称”,飞书建应用页会默认填好。
44
+ - 创建成功后可以**尝试**调用 `application/v7/applicationBase.patch` 改名(`i18ns[].name`),但该接口文档注明“仅支持开发者后台创建的自建应用,不包含通过机器人助手等其他渠道创建的自建应用”,对扫码创建的 PersonalAgent **可能被拒绝**。
45
+ - 不能承诺“用户在确认页无法改”。如果必须 100% 锁定,平台没有提供这个能力。
46
+
47
+ ### 2.2 权限:可在扫码确认页预填(addons)
48
+
49
+ SDK 的 `registerApp` 支持 `addons.scopes`:
50
+
51
+ ```js
52
+ addons: {
53
+ scopes: {
54
+ tenant: ['im:message'],
55
+ user: ['im:message.send_as_user'],
56
+ },
57
+ }
58
+ ```
59
+
60
+ SDK 会把 `addons` 做 `JSON → gzip → base64url`,放到二维码 URL 的 `addons` 参数。飞书确认页会把这些权限作为**增量**预填,用户确认后生效。
61
+
62
+ 注意:
63
+
64
+ - SDK 注明:`addons` 需要平台灰度开启;未开启时整个参数会被忽略,走默认模板。
65
+ - 平台目录里不存在的权限名会被静默丢弃;SDK 不校验名称。
66
+ - 创建后可以再**尝试** `application/v7/applicationConfig.patch` 的 `scope.add_scopes`(`tenant` / `user`)作为兜底;该接口同样有“仅开发者后台应用”的限制,可能失败。
67
+ - 建议绑定完成后调用 `application/v6/application.get` 校验实际 scopes;缺失时在账号设置里显示警告,不阻断绑定。
68
+
69
+ ### 2.3 重定向 URL:只能走应用管理 OpenAPI,且有限制
70
+
71
+ `addons` **不支持** `security.*`(SDK 明确说敏感配置被拒绝)。重定向 URL 只能通过应用管理 API 写入:
72
+
73
+ - 首选:`application/v7/applicationConfig.patch`
74
+ - `security.add.redirect_urls: string[]`
75
+ - 文档:修改重定向 URL 后立即生效。
76
+ - 兜底:`application/v6/application.patch`
77
+ - `security.redirect_urls: string[]`(全量覆盖,需要先读现有列表并做并集)。
78
+ - 名称兜底:`application/v7/applicationBase.patch`(`i18ns[].name`)。
79
+
80
+ **关键风险:**
81
+
82
+ - v7 文档注明:**仅支持开发者后台创建的自建应用,不包含通过机器人助手等其他渠道创建的自建应用**。扫码创建的 PersonalAgent 是否被归入“其他渠道”,需要真实应用验证。
83
+ - 如果 v6/v7 都拒绝,插件没有合规的公开 API 可以替用户写入重定向 URL。
84
+ - 降级方案(推荐):
85
+ - 保存“待写入”的 5 个 URL;
86
+ - 在账号设置里显示“平台未允许自动写入”的提示和“复制全部 URL”按钮;
87
+ - 用户可在飞书开发者后台手动粘贴。
88
+ - 不建议用用户浏览器 Cookie 去调开发者后台私有接口;不稳定且有安全风险。
89
+
90
+ ### 2.4 要写入的常量
91
+
92
+ ```js
93
+ const FEISHU_REDIRECT_URLS = [
94
+ 'http://127.0.0.1:3080/api/dsh-remote-feishu/oauth/callback',
95
+ 'http://localhost:3080/api/dsh-remote-feishu/oauth/callback',
96
+ 'http://127.0.0.1:32123/api/dsh-remote-feishu/oauth/callback',
97
+ 'http://127.0.0.1:8731/api/oauth/callback',
98
+ 'https://www.cubeai.com:6443/cubeaispace-dsh1-1234567890-dsh/api/dsh-remote-feishu/oauth/callback',
99
+ ]
100
+ ```
101
+
102
+ 按用户要求只添加,不校验这些地址是否可访问。
103
+
104
+ ### 2.5 建议的扫码 URL 参数
105
+
106
+ ```text
107
+ name=<频道名称>
108
+ createOnly=true
109
+ source=dsh-feishu-channels
110
+ addons=<gzip+base64url({ scopes: { tenant:['im:message'], user:['im:message.send_as_user'] } })>
111
+ ```
112
+
113
+ 保留当前 `begin` / `poll` 流程;域名仍固定 `accounts.feishu.cn`,Lark 租户继续拒绝。
114
+
115
+ ---
116
+
117
+ ## 3. 1–4、6 的界面与数据方案
118
+
119
+ ### 3.1 去掉设置页
120
+
121
+ - 删除 `ctx.slots.inject('settings.section', ...)` 注册。
122
+ - DSH 设置左侧不再出现“飞书频道”。
123
+ - 频道页承载:
124
+ - 添加账号(BindModal)
125
+ - 账号列表/状态/接收开关
126
+ - 账号设置(AccountInspector)
127
+ - 待批准访问请求
128
+ - 诊断/重连/移除
129
+ - `SettingsPage` 的账号/待批准/设置逻辑拆到频道页;`BindModal`、`AccountInspector` 保留复用。
130
+
131
+ ### 3.2 频道页布局
132
+
133
+ ```text
134
+ [ 添加飞书账号 ] ← 左侧显式按钮;无“飞书频道”标题
135
+ [待批准访问请求 ...] ← 有则显示
136
+
137
+ [飞书 logo] 飞书账号1 [已连接] [接收开关] [设置]
138
+ ├─ 会话标题 A 10:32
139
+ ├─ 会话标题 B 10:28
140
+ └─ 还没有会话,给机器人发一条消息
141
+
142
+ [飞书 logo] 飞书账号2 [已停止] [接收开关] [设置]
143
+ └─ ...
144
+ ```
145
+
146
+ - 页签本身的标签仍叫“飞书频道”(侧边栏 tab),去掉的是**页签内容里的标题行**。
147
+ - 右侧官方树自带的 icon 添加按钮不再出现,因为不再渲染官方树头部。
148
+ - 账号行名称来自 `account.name`(用户输入),不依赖首条消息或 LLM 标题。
149
+ - 每个账号行有“设置”按钮,直接打开 `AccountInspector` 弹窗。
150
+
151
+ ### 3.3 数据来源
152
+
153
+ - `/channels` 返回的 `channels[0].accounts`:账号列表,包含名称/状态/是否接收。
154
+ - `/channels` 返回的 `groups[0].sessions`:会话列表;按 `session.channel === account.id` 归到账号下。
155
+ - 新账号即使没有会话,也从 `accounts` 立即渲染,无需等消息。
156
+ - `channelRefresh` 继续作为账号/会话的刷新 store;`onConnected` 后调用 `invalidate()`。
157
+
158
+ ### 3.4 BindModal 调整
159
+
160
+ - 标题:`添加飞书账号`(不再拼“配置 飞书”)。
161
+ - 删除页签栏与手动配置表单;`tab` 固定为 `qr`。
162
+ - 顶部新增“频道名称”输入:
163
+ - 默认 `飞书账号${现有账号数 + 1}`;
164
+ - 写入 `settings.name`;
165
+ - QR `start` 请求带上该名称;
166
+ - 账号保存后名称即账号/频道名称。
167
+ - 保留现有的工作区/模型/权限/预设/命令权限/扫码/刷新/取消逻辑。
168
+ - 扫码开始前仍要求 workspace/model/permission 有效(与当前一致)。
169
+
170
+ ### 3.5 AccountInspector 增加 App ID / App Secret
171
+
172
+ 在现有字段顶部增加只读区:
173
+
174
+ ```text
175
+ 应用信息
176
+ App ID: cli_xxx
177
+ App Secret: •••••••• [已保存]
178
+ ```
179
+
180
+ - 服务端 `accountView` 增加:
181
+ - `appId: config.appId`
182
+ - `hasAppSecret: Boolean(config.appSecretRef || config.appSecret)`
183
+ - `provisioning?: { checkedAt, nameOk, scopesMissing, redirectMissing }`
184
+ - **不返回真实 Secret**;界面只显示掩码和“已保存”。
185
+ - 如果用户需要“显示/复制 Secret”,需要另开一个受宿主认证保护的高敏接口;不建议默认开放。
186
+
187
+ ### 3.6 名称与账号保存
188
+
189
+ - `settings.name` 由 BindModal 输入,QR `start` 传给 `PairingHub`。
190
+ - `PairingHub` 的 `extra` 保存 settings;成功后 manager `connect` 的 `normalizeAccountSettings` 会持久化 `name`。
191
+ - 账号 `account.name` 直接用于频道行名称;LLM 会话标题仍只影响会话行标题。
192
+ - 如果名称为空,服务端回退到 `飞书账号 N`。
193
+
194
+ ---
195
+
196
+ ## 4. 需要改动的文件/接口
197
+
198
+ ### Host
199
+
200
+ | 文件 | 改动 |
201
+ |---|---|
202
+ | `src/channels/qr/feishu.js` | `beginFeishuQr` 接收 `{name, addons}`;装饰 QR URL(`name`/`addons`/`createOnly`/`source`) |
203
+ | `src/channels/qr/hub.js` | 把 `session.extra` 传给 `begin`;保持现有 poll/refresh/cancel |
204
+ | `src/channels/feishu-provision.js`(新) | 应用管理:v7 config patch(scopes + redirect URLs)、v6 patch 兜底、v7 base 改名兜底、v6 get 校验 |
205
+ | `src/manager.js` | `connectNow` 在保存凭据后调用 provisioning;`accountView` 返回 appId/hasAppSecret/provisioning;错误只记安全码 |
206
+ | `src/channels/meta.js` | 若需要,增加应用配置相关字段(一般不用) |
207
+
208
+ ### Client
209
+
210
+ | 文件/组件 | 改动 |
211
+ |---|---|
212
+ | `client.js` `apply` | 删除 `settings.section` 注册;保留 `sidebar.workspaces`/`sidebar.session.row.leading` |
213
+ | 新 `FeishuChannelPage` | 账号页:添加按钮、账号行、会话、待批准、设置弹窗 |
214
+ | `BindModal` | 标题/单页签/频道名称/旧逻辑保留 |
215
+ | `AccountInspector` | 增加 App ID / App Secret 掩码 / provisioning 警告 |
216
+ | `ChannelOfficialView` | 不再作为主视图;其会话/官方树逻辑可移除或保留为内部组件 |
217
+ | locale | 新增/调整 `bind.title`、`account.name`、`app.id`、`app.secret`、`provisioning.*` 等 |
218
+
219
+ ### 不建议
220
+
221
+ - 不建议修改 `dsh-im-connect`。
222
+ - 不建议用 DOM 注入去改官方开发者后台页面。
223
+ - 不建议把真实 App Secret 返回浏览器。
224
+
225
+ ---
226
+
227
+ ## 5. 实施阶段与工期
228
+
229
+ | 阶段 | 内容 | 预估 |
230
+ |---|---|---|
231
+ | A | 数据/API:accountView appId/secret 掩码/provisioning;QR 参数 | 1.5 天 |
232
+ | B | 应用配置 provisioning(v7/v6、校验、降级提示) | 2–3 天 |
233
+ | C | 频道页重构:去设置页、账号行、会话、待批准、弹窗 | 3–4 天 |
234
+ | D | BindModal 单页签/名称字段 | 1 天 |
235
+ | E | AccountInspector App 信息 | 0.5 天 |
236
+ | F | 测试(Host/Client)+ 真实飞书扫码验证 | 2–3 天 |
237
+ | | **合计** | **10–13 人天** |
238
+
239
+ 第 5 项的真实应用验证是关键路径;如果 v7/v6 都拒绝 redirect URL,需要走降级提示。
240
+
241
+ ---
242
+
243
+ ## 6. 需要你确认的点
244
+
245
+ 1. **App 名称**:接受“扫码确认页预填 + 创建后尝试改名,但用户仍可在飞书确认页修改”吗?平台没有强制锁定能力。
246
+ 2. **重定向 URL**:若飞书拒绝自动写入(很可能只对开发者后台应用开放),接受“账号设置里显示未写入 + 复制全部 URL”作为降级吗?还是要强制要求开发者后台创建的应用?
247
+ 3. **App Secret**:只做掩码展示(推荐),还是需要“显示/复制真实 Secret”?后者要新增高敏接口。
248
+ 4. **频道页形态**:改为自绘账号/会话列表,替代官方任务树头部,因此会失去官方树的“分组方式/按工作区”等富交互;会保留搜索、打开会话、重命名、归档、删除等核心操作。可以接受吗?
249
+
250
+ 确认后我按 A→F 实施,先做 1–4、6,再做第 5 项的 provisioning 和真机验证。
251
+
252
+ ---
253
+
254
+ ## 参考
255
+
256
+ - 本地 SDK 注册能力:`node_modules/@larksuiteoapi/node-sdk/README.zh.md`(`registerApp`、`appPreset`、`addons`)
257
+ - 本地 SDK 类型:`node_modules/@larksuiteoapi/node-sdk/types/index.d.ts`
258
+ - `applicationConfig.patch`(v7,`scope.add_scopes`、`security.add.redirect_urls`)
259
+ - `applicationBase.patch`(v7,`i18ns[].name`)
260
+ - `application.patch`(v6,`security.redirect_urls`)
261
+ - 飞书 v7 配置文档:<https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/application-v7/application-v7/application-config/patch>
262
+ - 扫码注册流程与 `name` 预填:<https://github.com/multica-ai/multica/blob/f41fae6b08fb734afcbd13205c0b3203dd0bc9c6/server/internal/integrations/lark/registration.go>
263
+ - 飞书 CLI 一键建机器人逆向调研:<https://github.com/op7418/codepilot/blob/1ae6d76de6993377dab961eacf04927babd99eb1/docs/research/feishu-cli-one-click-bot.md>
@@ -0,0 +1,120 @@
1
+ # Verification
2
+
3
+ ## Current automated status (2026-11-04)
4
+
5
+ - Package installed from `/home/hls/dev/dsh-remote-feishu/dsh-feishu-channels`
6
+ into the `web` profile as a local link.
7
+ - Host plugin row is active (`Config.listConfigs` reports `feishu-channel`).
8
+ - Host log `~/.dsh/dsh-feishu-channels/gateway.log` shows:
9
+ - `API 已注册 /api/dsh-feishu-channels`
10
+ - `apply 完成`
11
+ - `initEnabled`
12
+ - `attachMappedSessions`
13
+ - `pnpm test` passes 94 tests.
14
+ - `pnpm run check` passes (syntax, isolation, manifest, locale metadata).
15
+ - After the page loaded the new Client module, live slot inspection reports:
16
+ - `settings.section` has **no** `feishu-channel` occupant (the settings entry was removed).
17
+ - `sidebar.session.row.leading` occupant `feishu-channel-session-channel`, order 10, active.
18
+ - `conversation.composer` chain entry should appear after a page refresh; it replaces the
19
+ normal composer for `dfc:` sessions with a read-only Feishu notice.
20
+ - The Client module therefore loaded and executed `apply` without a slot crash.
21
+ - The shared tab bar itself is rendered inside the sidebar wrapper and is not exposed by
22
+ slot inspection; it still needs the visual check below.
23
+ - Code-level coexistence evidence:
24
+ - A harness using the **real `dsh-im-connect@1.0.12` `SessionSwitcher`** with both a
25
+ `channels` tab and our `feishu` tab renders `任务 | 频道 | 飞书频道` and switches
26
+ content for all three tabs.
27
+ - Our own `SessionSwitcher` harness renders `任务 | 飞书频道`, inserts a foreign
28
+ `channels` tab at order 20, and keeps `飞书频道` at order 30.
29
+
30
+ ## Real Feishu provisioning verification (2026-11-04)
31
+
32
+ - Two test accounts were created by QR: `新智能体1号` and `新智能体2号`.
33
+ - Both received the requested name and scopes:
34
+ - `im:message`
35
+ - `im:message.send_as_user`
36
+ - Initial redirect-URL writes failed with Feishu business code `99991672`:
37
+ `application:application:patch` was missing.
38
+ - The app was updated through Feishu's existing-app QR flow to add
39
+ `application:application:patch`.
40
+ - Re-running the v7 config patch returned `code: 0, msg: success`; all five
41
+ redirect URLs were then present in `application.v6.application.get`.
42
+ - Conclusion: keep the redirect-URL write step, with
43
+ `application:application:patch` included in the QR addons for new accounts.
44
+ Existing accounts can use **重新检查应用配置** after adding the permission.
45
+
46
+ ## Visual confirmation
47
+
48
+ The user confirmed the live tab bar on 2026-11-04:
49
+
50
+ - Sidebar: **任务 | 频道 | 飞书频道** (im-connect and this plugin both enabled).
51
+ - The settings entry was removed in the follow-up UI revamp; live slot inspection confirms
52
+ `settings.section` has no `feishu-channel` occupant.
53
+ - Real Feishu QR/message acceptance is still pending a user scan; this is the remaining
54
+ verification item.
55
+
56
+ The Client module is loaded (see the live slot evidence above). Remaining page checks:
57
+
58
+ 1. **Settings nav**: no **飞书频道** entry.
59
+ 2. **Sidebar**: `任务 | 频道 | 飞书频道` when im-connect is also enabled; only
60
+ `任务 | 飞书频道` when it is disabled.
61
+ 3. **Channel tab**: left side has only **添加飞书账号**; no content title, folder group,
62
+ or right-side icon add button.
63
+ 4. **Add dialog**: title **添加飞书账号**; only QR binding; no manual fields or
64
+ **快捷绑定** text; has **频道名称** with default `飞书账号1`.
65
+ 5. **Account row**: after scanning, the entered name appears immediately, with status,
66
+ receive switch, and **设置**.
67
+ 6. **Account settings**: **飞书App信息** shows App ID/App Secret in one row; App ID is
68
+ copyable; App Secret is masked and can be shown/copied. The check result is collapsible.
69
+ 7. **Feishu session composer**: opening a `dfc:` session removes the text input/send
70
+ button and shows the current account model as read-only text; switch models in
71
+ account settings and the display follows. While a turn is running, a **Stop
72
+ generating** button asks for confirmation before cancelling the turn. The current
73
+ session row is highlighted in the channel list.
74
+ 8. **No console error** such as `slot entry crashed` or a blank sidebar.
75
+
76
+ If the sidebar is blank or the tabs are wrong, disable this bundle in Plugin Manager
77
+ first (the official sidebar returns immediately), then report the browser console
78
+ error. Do not uninstall `dsh-im-connect`.
79
+
80
+ ## Slot-level checks (no browser needed)
81
+
82
+ ```text
83
+ cordis_inspect_query client Slots.listSubTree { "root": "settings.section" }
84
+ → occupants must NOT include { id: "feishu-channel" }
85
+
86
+ cordis_inspect_query client Slots.listSubTree { "root": "sidebar.session.row.leading" }
87
+ → occupants should include { id: "feishu-channel-session-channel" }
88
+ ```
89
+
90
+ If these entries do not appear, the page has not loaded the new Client module; refresh
91
+ first.
92
+
93
+ ## Real Feishu acceptance
94
+
95
+ 1. Open the **飞书频道** tab → **添加飞书账号** → enter a name → scan the QR code.
96
+ 2. After the account appears, open **设置** and verify App ID / App Secret show and copy.
97
+ 3. Inspect `config.provisioning` in `~/.dsh/dsh-feishu-channels/channels.json`:
98
+ - If all five redirect URLs are present, keep the redirect write step.
99
+ - If Feishu rejects them, remove the redirect write operation and its UI hint.
100
+ 4. Send a private message to the bot; the first user is the owner.
101
+ 5. Verify: text reply, group @ mention, image, file, approval card, question card,
102
+ slash commands, receive off/on, diagnostics, remove account.
103
+ 6. Add a second Feishu account with a different app; verify sessions do not mix.
104
+ 7. Keep im-connect enabled but with its Feishu account removed/disabled. Do not use the
105
+ same App ID in both plugins.
106
+
107
+ ## Commands
108
+
109
+ ```bash
110
+ cd /home/hls/dev/dsh-remote-feishu/dsh-feishu-channels
111
+ pnpm test
112
+ pnpm run check
113
+ ```
114
+
115
+ ## Known limits
116
+
117
+ - Visual verification requires the user; this environment has no browser control.
118
+ - Registry v1 is private to the im-connect/new-plugin coexistence convention.
119
+ - Codex UI sidebars are not wrapped in v1.
120
+ - Same Feishu App ID in both plugins is a platform-level conflict.
package/icon.svg ADDED
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="DSH Feishu Channel">
2
+ <defs>
3
+ <linearGradient id="dfc-bg" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
4
+ <stop stop-color="#3370ff"/>
5
+ <stop offset="1" stop-color="#00b8a9"/>
6
+ </linearGradient>
7
+ <linearGradient id="dfc-plane" x1="24" y1="22" x2="44" y2="44" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#ffffff"/>
9
+ <stop offset="1" stop-color="#dce9ff"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <rect x="4" y="4" width="56" height="56" rx="16" fill="url(#dfc-bg)"/>
13
+ <path d="M17 22.5c0-3.59 2.91-6.5 6.5-6.5h17c3.59 0 6.5 2.91 6.5 6.5v13c0 3.59-2.91 6.5-6.5 6.5H32l-8.8 7.1c-.98.79-2.45.09-2.45-1.16V42.2A6.5 6.5 0 0 1 17 36z" fill="#ffffff" opacity=".16"/>
14
+ <path d="M17 22.5c0-3.59 2.91-6.5 6.5-6.5h17c3.59 0 6.5 2.91 6.5 6.5v13c0 3.59-2.91 6.5-6.5 6.5H32l-8.8 7.1c-.98.79-2.45.09-2.45-1.16V42.2A6.5 6.5 0 0 1 17 36z" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linejoin="round"/>
15
+ <path d="M24 30.5h16M24 36.5h10" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" opacity=".92"/>
16
+ <path d="M40.5 22.5 27 31.8l6.1 3.1 2.1 7.5 5.3-19.9z" fill="url(#dfc-plane)" stroke="#2b5fdc" stroke-width="1.2" stroke-linejoin="round" opacity=".96"/>
17
+ </svg>
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH Feishu Channel",
4
+ "description": "Connect a Feishu bot to the local DeepSeek Harness."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "DSH飞书频道",
4
+ "description": "将飞书机器人接入本机 DeepSeek Harness。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,78 @@
1
1
  {
2
2
  "name": "@cubeai/dsh-feishu-channels",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "DSH飞书频道 — 将飞书机器人接入本机 DeepSeek Harness。",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./client": "./client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json",
12
+ "./locale/*.json": "./locale/*.json",
13
+ "./icon": "./icon.svg"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "client.js",
18
+ "cordis.patch.yml",
19
+ "locale",
20
+ "icon.svg",
21
+ "README.md",
22
+ "LICENSE",
23
+ "NOTICE",
24
+ "docs"
25
+ ],
26
+ "keywords": [
27
+ "dsh",
28
+ "dsh-plugin",
29
+ "feishu",
30
+ "im",
31
+ "飞书"
32
+ ],
33
+ "author": "huolongshe",
34
+ "license": "Apache-2.0",
35
+ "engines": {
36
+ "node": "^22.19.0 || >=24.0.0"
37
+ },
38
+ "dsh": {
39
+ "bundle": {
40
+ "patch": "./cordis.patch.yml"
41
+ },
42
+ "client": {
43
+ "platform": "web",
44
+ "inject": [
45
+ "@deepseek-ai/dsh-client-locale",
46
+ "@deepseek-ai/dsh-client-ui-slots",
47
+ "@deepseek-ai/dsh-client-ui-primitives"
48
+ ]
49
+ }
50
+ },
51
+ "dependencies": {
52
+ "@deepseek-ai/schemastery": "^3.18.2",
53
+ "@larksuiteoapi/node-sdk": "^1.36.0",
54
+ "qrcode": "^1.5.4"
55
+ },
56
+ "peerDependencies": {
57
+ "@deepseek-ai/cordis": "^4.0.2",
58
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
59
+ "@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
60
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.2",
61
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
62
+ "@deepseek-ai/dsh-host-webserver": "^0.2.1-rc.2",
63
+ "@deepseek-ai/schemastery": "^3.18.2"
64
+ },
65
+ "scripts": {
66
+ "check": "node scripts/check.js",
67
+ "test": "node --test tests/*.test.mjs"
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://openi.pcl.ac.cn/dsh/dsh-feishu-channels.git"
72
+ },
73
+ "homepage": "https://openi.pcl.ac.cn/dsh/dsh-feishu-channels",
74
+ "publishConfig": {
75
+ "access": "public",
76
+ "registry": "https://registry.npmjs.org/"
77
+ }
78
+ }
@@ -0,0 +1,12 @@
1
+ /** Maps the Feishu adapter's status text to the account view state. */
2
+ const readyStates = new Set(['长连接已建立']);
3
+ const connectingStates = new Set(['连接中']);
4
+
5
+ export function connectionState(status) {
6
+ if (readyStates.has(status)) return 'connected';
7
+ if (connectingStates.has(status)) return 'connecting';
8
+ if (status === '已停止') return 'stopped';
9
+ if (status === '未连接' || status === '已断开') return 'disconnected';
10
+ if (status === '连接失败' || status === '连接失败,请查看本机日志' || status === '连接错误') return 'error';
11
+ return 'unknown';
12
+ }
@@ -0,0 +1,77 @@
1
+ export class DiagnosticError extends Error {
2
+ reason;
3
+ httpStatus;
4
+ platformCode;
5
+ constructor(reason, httpStatus, platformCode) {
6
+ super(reason);
7
+ this.reason = reason;
8
+ this.httpStatus = httpStatus;
9
+ this.platformCode = platformCode;
10
+ }
11
+ }
12
+ export async function probe(id, signal, run) {
13
+ const started = Date.now();
14
+ try {
15
+ signal.throwIfAborted();
16
+ await run();
17
+ signal.throwIfAborted();
18
+ return { id, status: 'passed', reason: 'ok', durationMs: Date.now() - started };
19
+ }
20
+ catch (error) {
21
+ const failure = error instanceof DiagnosticError ? error : new DiagnosticError(signal.aborted ? signal.reason?.name === 'TimeoutError' ? 'timeout' : 'cancelled' : 'network');
22
+ return { id, status: ['cancelled', 'missing-context', 'not-connected', 'unsupported', 'changed'].includes(failure.reason) ? 'unverified' : 'failed', reason: failure.reason, durationMs: Date.now() - started,
23
+ ...(failure.httpStatus === undefined ? {} : { httpStatus: failure.httpStatus }), ...(failure.platformCode === undefined ? {} : { platformCode: failure.platformCode }) };
24
+ }
25
+ }
26
+ /** 新请求、不跟随重定向、限制回包大小;调用方负责平台业务码与必要字段校验。 */
27
+ export async function diagnosticJson(url, signal, body, headers, fetcher = fetch) {
28
+ const response = await fetcher(url, { method: body === undefined ? 'GET' : 'POST', headers: { 'content-type': 'application/json', ...headers }, body: body === undefined ? undefined : JSON.stringify(body), signal, redirect: 'error' });
29
+ if (!response.ok) {
30
+ await response.body?.cancel();
31
+ const status = response.status;
32
+ throw new DiagnosticError(status === 401 ? 'auth' : status === 403 ? 'permission' : status === 429 ? 'rate-limit' : status >= 500 ? 'server' : 'rejected', status);
33
+ }
34
+ if (!response.body)
35
+ throw new DiagnosticError('invalid-response');
36
+ const reader = response.body.getReader();
37
+ const chunks = [];
38
+ let size = 0;
39
+ try {
40
+ for (;;) {
41
+ const { done, value } = await reader.read();
42
+ if (done)
43
+ break;
44
+ size += value.length;
45
+ if (size > 65536)
46
+ throw new DiagnosticError('invalid-response');
47
+ chunks.push(value);
48
+ }
49
+ let result;
50
+ try {
51
+ result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
52
+ }
53
+ catch {
54
+ throw new DiagnosticError('invalid-response');
55
+ }
56
+ if (!result || typeof result !== 'object' || Array.isArray(result))
57
+ throw new DiagnosticError('invalid-response');
58
+ return result;
59
+ }
60
+ finally {
61
+ await reader.cancel().catch(() => undefined);
62
+ reader.releaseLock();
63
+ }
64
+ }
65
+ export function requireDiagnostic(value) {
66
+ if (!value)
67
+ throw new DiagnosticError('invalid-response');
68
+ }
69
+ export function platformResult(code, authCodes = []) {
70
+ if (!(typeof code === 'number' || typeof code === 'string' && /^-?\d+$/.test(code)))
71
+ throw new DiagnosticError('invalid-response');
72
+ const numeric = Number(code);
73
+ if (!Number.isFinite(numeric))
74
+ throw new DiagnosticError('invalid-response');
75
+ if (numeric !== 0)
76
+ throw new DiagnosticError(authCodes.includes(numeric) ? 'auth' : numeric === 429 ? 'rate-limit' : 'rejected', undefined, numeric);
77
+ }
@@ -0,0 +1,20 @@
1
+ import { createFeishuChannel } from './feishu.js'
2
+
3
+ /**
4
+ * Only the Feishu platform is supported by this plugin.
5
+ * The manager still passes a platform id; unknown ids are rejected.
6
+ */
7
+ export function createChannelAdapter(id, config, log, _stateDir, options) {
8
+ if (id !== 'feishu') return undefined
9
+ const adapter = createFeishuChannel('feishu', {
10
+ appId: config.appId,
11
+ appSecret: config.appSecret,
12
+ host: options?.host,
13
+ }, log)
14
+ if (!adapter) return undefined
15
+ return {
16
+ ...adapter,
17
+ id: options?.accountId ?? adapter.id,
18
+ label: options?.accountLabel ?? adapter.label,
19
+ }
20
+ }