dsh-remote-web 0.7.11-beta.3 → 0.7.12-beta.2

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.
@@ -0,0 +1,177 @@
1
+ # 飞书机器人通道 —— 模块 API / 契约 / 真实联调步骤(2026-11-08)
2
+
3
+ > 实现:`clients/dsh-remote/feishu-bot.mjs`(bridge 侧,懒加载官方 SDK)
4
+ > 调研依据:`research/2026-11-strategy/06-multiagent/feishu-long-connection.md`
5
+ > 依赖策略:`research/2026-11-strategy/06-multiagent/feishu-lazy-sdk-plan.md`
6
+
7
+ ## 0. 一句话
8
+
9
+ 飞书「企业自建应用」长连接机器人:**收消息 / 发通知 / 卡片拍板**,与企微、钉钉、微信并列的第四条通道;
10
+ **官方 SDK 不随安装包分发**(约 41MB),用户在面板点「下载依赖」后才懒加载;`package.json` 里**没有**这个依赖。
11
+
12
+ ---
13
+
14
+ ## 1. 依赖策略(业主明确要求)
15
+
16
+ | 项 | 做法 |
17
+ |---|---|
18
+ | `package.json` | **不声明** `@larksuiteoapi/node-sdk`(安装体积不变) |
19
+ | 下载位置 | `<relayDir>/im-deps/node_modules/@larksuiteoapi/node-sdk`(npm `--prefix` 本地安装;relayDir 默认 `~/.dsh-remote`) |
20
+ | 下载命令(插件/安装器执行) | `npm install --prefix <relayDir>/im-deps --no-save --no-audit --no-fund @larksuiteoapi/node-sdk` |
21
+ | 进度/失败 | 面板显示下载/解压/完成/失败;失败给人话(网络/磁盘/权限),可重试 |
22
+ | 运行时 | `loadFeishuSdk()` 动态 `import()` 入口(从 `package.json` 的 `main` 解析)+ 缓存;**没启用飞书就不加载** |
23
+ | 未下载 | 状态 `feishu.state = "sdk_missing"`、`deps.feishu.installed=false`,**不崩、不阻塞其它通道**;面板显示「下载依赖(约 41MB)」 |
24
+ | 卸载 | 「彻底卸载」默认一并删除 `im-deps/`(与调研方案一致) |
25
+
26
+ > 可选环境变量:`DSH_IM_DEPS_DIR` 覆盖依赖根目录(自建/测试用)。
27
+
28
+ ---
29
+
30
+ ## 2. 配置文件契约:`im-bots.json`(插件写、桥只读,0600)
31
+
32
+ ```jsonc
33
+ {
34
+ "version": 1,
35
+ "wecom": { "...": "不变" },
36
+ "dingtalk": { "...": "不变" },
37
+ "feishu": {
38
+ "enabled": false,
39
+ "appId": "", // cli_ 开头;SDK 有严格正则(cli_+16 hex),不匹配会"保存成功但连不上"
40
+ "appSecret": "",
41
+ "domain": "feishu", // feishu=open.feishu.cn | lark=open.larksuite.com(国际版)
42
+ "receiveId": "", // 主动推送目标(open_id 或 chat_id);空 = 用最近一次入站的 chat_id
43
+ "receiveIdType": "", // "open_id" | "chat_id"(空 = 按有无绑定推导)
44
+ "boundChatId": "", // 桥写回:最近一次收到消息的 chat_id(面板只读展示)
45
+ "allowFrom": [], // open_id 白名单(空 = 不限制;建议填写)
46
+ "cardApproval": true, // 审批用卡片按钮(false = 纯文本 + 回电脑拍板)
47
+ "sdkVersion": "", // 下载成功后回写(面板展示)
48
+ "sessionId": "", // 可选:固定转发目标会话(空 = 跟随当前活跃会话)
49
+ "sessionTitle": ""
50
+ }
51
+ }
52
+ ```
53
+
54
+ **不加** `verifyToken` / `encryptKey`:长连接模式下事件/回调由 SDK 内部解密分发,那两个字段只属于 HTTP 回调模式。
55
+
56
+ ## 3. 状态契约:`im-bot-status.json`(桥写,原子替换 + 0600)
57
+
58
+ ```jsonc
59
+ {
60
+ "version": 1, "updatedAt": 0,
61
+ "wecom": { "...": "不变" },
62
+ "dingtalk": { "...": "不变" },
63
+ "feishu": {
64
+ "enabled": true, "state": "idle|connecting|connected|reconnecting|failed|sdk_missing",
65
+ "connected": true, "authenticated": true,
66
+ "lastError": "", "lastConnectedAt": 0, "lastMessageAt": 0, "boundChatId": "",
67
+ "appId": "cli_9f42***b1", "domain": "feishu", "receiveIdType": "open_id",
68
+ "cardApproval": true, "sdkVersion": "1.76.0",
69
+ "tier": "free", "caps": ["..."], "limits": { "messages_per_month": 20 },
70
+ "usage": { "month": "2026-11", "messages": 0 },
71
+ "ackCount": 0, "decisionCount": 0
72
+ },
73
+ "deps": {
74
+ "feishu": { "installed": true, "version": "1.76.0", "path": "<...>/node_modules/@larksuiteoapi/node-sdk/index.js", "lastError": "" }
75
+ }
76
+ }
77
+ ```
78
+
79
+ - 与企微/钉钉字段对齐;飞书**多一个 `state`**(5 态便于排查)。
80
+ - **只加不改**:写状态时先读旧文件再合并,绝不覆盖 `wecom` / `dingtalk` 段。
81
+ - `secret` 不出现在状态文件与日志里。
82
+
83
+ ---
84
+
85
+ ## 4. 模块 API
86
+
87
+ ```js
88
+ import {
89
+ feishuSdkStatus, loadFeishuSdk, // 依赖
90
+ validateAppId, normalizeFeishuConfig, readFeishuConfig,
91
+ buildApprovalCard, buildDecidedCard, parseCardAction,
92
+ formatFeishuText, extractFeishuMessageText, normalizeInboundEvent,
93
+ feishuErrorHint, mapConnectError,
94
+ createFeishuSender, createFeishuBot, createFeishuBotController,
95
+ FEISHU_SETUP_GUIDE, FEISHU_ERROR_HINTS
96
+ } from "./feishu-bot.mjs";
97
+ ```
98
+
99
+ | 导出 | 用途 |
100
+ |---|---|
101
+ | `feishuSdkStatus({relayDir, env})` | 只读探测依赖:`{installed,version,path,depsDir,lastError}`(面板用;不发网络) |
102
+ | `loadFeishuSdk({relayDir, env, sdkFactory, importImpl})` | 懒加载 + 缓存;`sdkFactory` 注入供测试 |
103
+ | `validateAppId(appId)` | `{ok,strict,warning?}`:宽松只拦明显错;不通过 SDK 严格正则时给 warning |
104
+ | `normalizeFeishuConfig / readFeishuConfig` | 配置归一 / 读 `im-bots.json` |
105
+ | `buildApprovalCard / buildDecidedCard` | 卡片 JSON 2.0(按钮 `behaviors:[{type:"callback",value}]`) |
106
+ | `parseCardAction` | 解析 `card.action.trigger`(operator/context/expiresAt) |
107
+ | `createFeishuSender` | tenant_access_token(缓存+失效刷新一次)+ 发消息 + PATCH 卡片 |
108
+ | `createFeishuBot` | `{start,stop,sendText,sendCard,patchCard,status,on}`;事件 `message/cardAction/ready/status` |
109
+ | `createFeishuBotController` | bridge 只接这个:`{start,stop,status}`;读配置 + 2s 热更新 + 状态原子写 + 会员闸 + 通知 |
110
+ | `FEISHU_SETUP_GUIDE` | 官方链接 + **8 步向导文案** + 顺序坑提示(插件半直接渲染) |
111
+
112
+ ### 4.1 卡片回执时序(3 秒铁律)
113
+
114
+ ```
115
+ 用户点按钮 → card.action.trigger(长连接)
116
+ ├─① 同步返回 {toast:{type:"success"}, card:{type:"raw",data:已决定卡片}} ← 必须在 3s 内
117
+ ├─② 幂等:同一 requestId+action 第二次点只回 toast(10 分钟 TTL)
118
+ ├─③ 过期(expiresAt 已过)→ toast「已过期(未回复按拒绝处理)」,**不交决策**
119
+ └─④ 异步把决策交给 onApprovalDecision(requestId, "allow"|"deny")
120
+ ↓ bridge 注入的是 applyApprovalDecision(与手机拍板/企微卡片同一条路)
121
+ ```
122
+
123
+ `allow` 只对应「允许一次」,不会变成永久放行;无决策来源时只 ACK 不生效(并记日志)。
124
+
125
+ ---
126
+
127
+ ## 5. 配置页 8 步向导(插件半照抄 `FEISHU_SETUP_GUIDE`)
128
+
129
+ 顶部固定两条官方链接(**业主明确要求**):
130
+ - 使用长连接接收事件:<https://open.feishu.cn/document/server-docs/event-subscription-guide/event-subscription-configure-/request-url-configuration-case>
131
+ - 注册/创建应用:<https://open.feishu.cn/app>
132
+
133
+ | # | 步骤 |
134
+ |---|---|
135
+ | 1 | 打开飞书开放平台 → 创建**企业自建应用**(商店应用不支持长连接) |
136
+ | 2 | 「凭证与基础信息」→ 复制 **App ID / App Secret** |
137
+ | 3 | 「添加应用能力」→ 开启**机器人** |
138
+ | 4 | 「权限管理」→ 申请收消息(`im:message.p2p_msg:readonly`、`im:message.group_at_msg:readonly`)与发消息(`im:message:send_as_bot`) |
139
+ | 5 | 「事件与回调 → 事件配置」→ 订阅方式选**使用长连接接收事件** → 添加「接收消息 v2.0」 |
140
+ | 6 | 「事件与回调 → 回调配置」→ 订阅方式选**使用长连接接收回调** → 添加 `card.action.trigger` |
141
+ | 7 | 「版本管理与发布」→ 创建版本并**发布**(不发布不生效);「可用范围」包含自己/目标成员 |
142
+ | 8 | 回到面板:填 App ID / Secret → **下载依赖** → **测试连接** → 在飞书里给机器人发一句话完成绑定 |
143
+
144
+ ⚠️ **顺序坑**:保存事件/回调配置时,**必须已经有一个长连接在线**(否则提示「应用未建立长连接」)。
145
+ 所以先在本机填凭据让连接起来,再去后台保存配置。
146
+ ⚠️ **一个应用只挂一台电脑**(官方集群随机分发,多台会互抢事件;单应用上限 50 连接)。
147
+
148
+ ---
149
+
150
+ ## 6. 真实联调步骤(需要业主:建应用 + 发版)
151
+
152
+ 前置:Windows/macOS 任一真机、能访问公网(**长连接不支持代理**)、企业管理员权限(发版)。
153
+
154
+ 1. **建应用并配置**(按上面 8 步,先只做 1–4 与 7 的权限/能力部分,事件/回调等第 4 步做完再回来配)。
155
+ 2. **本机下载依赖**(面板按钮等价命令):
156
+ `npm install --prefix ~/.dsh-remote/im-deps --no-save --no-audit --no-fund @larksuiteoapi/node-sdk`
157
+ 3. **写配置**(面板写入或手工):`im-bots.json` 的 `feishu.{enabled:true,appId,appSecret,allowFrom:[你的 open_id]}`。
158
+ 4. **看状态**:bridge 日志出现 `[feishu] 长连接已发起`;`im-bot-status.json` 的 `feishu.state → connected`。
159
+ 5. **回去配事件/回调**(第 5、6 步)→ 发布版本(第 7 步)。
160
+ 6. **绑定**:在飞书里给机器人发一句话 → 状态 `boundChatId` 出现 → 面板显示已绑定。
161
+ 7. **验收**:
162
+ - 出站:电脑上让 agent 跑一个任务 → 飞书收到「任务完成/出错」通知;
163
+ - 入站:飞书发一句话 → 电脑端当前会话收到(免费档每月 20 条,超出有升级提示);
164
+ - 拍板:触发一次需要审批的操作 → 飞书收到卡片 → 点「允许一次」→ 卡片原地变「已允许」+ 电脑端继续;点「拒绝」→ 电脑端拒绝;**不回** → 90s 后按拒绝处理。
165
+ 8. **排障对照**(状态文件 `feishu.lastError`):
166
+ - 凭据错 / 应用未发版 / 权限未申请 / 订阅方式没切长连接 / 可用范围不含你 / 机器人不在群 —— 都有对应人话(`FEISHU_ERROR_HINTS` / `mapConnectError`)。
167
+ - 企业网络走代理 → 明确提示「长连接不支持代理」。
168
+
169
+ ---
170
+
171
+ ## 7. 已知限制 / 未核实
172
+
173
+ 1. **本模块未做真机联调**:SDK/网络全部注入假实现单测(42 条);真实建连、卡片回调、发版生效需业主按 §6 验一次。
174
+ 2. **卡片回调能否走长连接**:官方文档说可以;SDK README 有一句相反表述(调研 R8)。真机点一次按钮确认。
175
+ 3. `patchCard`(原地更新)用 REST PATCH,官方权限/版本限制未核实;失败只记日志(不影响 ACK)。
176
+ 4. 流式回复:飞书没有该能力(调研 §4.3),本模块不提供;通知/审批不需要。
177
+ 5. 群聊只处理 @机器人 的消息由飞书侧决定(我们按收到的消息处理);多设备抢事件需「一个应用一台电脑」。