dsh-im-union 0.4.7 → 0.4.12

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/CHANGELOG.md CHANGED
@@ -2,6 +2,63 @@
2
2
 
3
3
  本项目版本号遵循语义化版本;发布面与宿主兼容矩阵见 `scripts/hosts.mjs`(README 管理块同源)。
4
4
 
5
+ ## 0.4.12(飞书审批卡片:权限 ask 走按钮确认)
6
+
7
+ ### Added
8
+
9
+ - **飞书 `askChoices` 实装(本插件第一个真按钮面渠道)**:宿主审批(T1.9)不只能回「1/2/c」文本,而是直接收到一张带「允许一次 / 拒绝」按钮的卡片,点按钮即定夺。`caps.nativeChoices` 由 false 改 true(此前所有渠道都是 false 或缺面,飞书是第一个把这条分支跑通的)
10
+ - **协议事实(真机实测,写进代码注释)**:飞书 2.0 schema 的 `body.elements` **已不支持 `action` 标签**(`230099 / 200861 unsupported tag action`),按钮卡片必须走 1.0 形态(顶层 `elements` + `action`/`button`);而内联 `interactive` 消息不碰 cardkit 实体面,**不需要 `cardkit:card:write`**(与流式卡片不同一条权限链)
11
+ - **回传走同一条长连接**:`EventDispatcher` 注册 `card.action.trigger`(官方 SDK 1.76 自带该分发 + 按 chatId 去重/加锁/排队),无需另开 webhook;回包给 `toast` 让用户看到「已记录:拒绝」
12
+
13
+ ### Fixed
14
+
15
+ - **按钮期间用户打字会被当成新任务丢给模型**:原 `handleApproval` 的按钮分支是直接 `await adapter.askChoices(...)`,期间**不登记 pending**,所以用户习以为常地回一句「可以」会落进会话队列。现改为**按钮 + 文本双路竞速**(`approvalViaButtons`):卡片在位时「1/2/c」同样定夺,先到为准
16
+ - **按钮面无渠道侧超时**:适配器 `askChoices` 自带 5 分钟上限(与 D5 对齐)+ `signal` abort 即收;`disconnect()` 释放所有在位卡片(空答=取消,fail-closed)
17
+
18
+ ### Changed
19
+
20
+ - `askChoices` 契约加第 6 个可选参 `opts.ownerUserId`:卡片回传带的是点击者身份,需比对发起者才能守 D5(**只有发起者能定夺**,他人点击不改判,只回 toast 说明)
21
+ - 飞书 `multi=true` 明确拒接(卡片无多选原语):抛错后 broker 回落文本菜单,不造假成单选
22
+ - 迟到的重复点按(卡还在、pending 已清)一律忽略;审批卡按 `message_id` 键控,同聊天先后两张卡不会串台
23
+
24
+ ## 0.4.11(回合内「处理中」信号 — 引擎接线 + 飞书表情落地)
25
+
26
+ ### Added
27
+
28
+ - **引擎侧第一次真正调 `setTyping`**:`runTurn` 在 prompt 前发回合开始信号(`caps.typing` 位 + `setTyping` 方法双门控)。此前 Discord/Telegram/Matrix/QQ/微信 六个适配器都实现了 `setTyping`,但引擎侧**零调用点**——它们全是死代码;这次一并激活
29
+ - **飞书「处理中」落地为表情回应**:飞书无 typing API(客户端不暴露),实测 `POST/DELETE /open-apis/im/v1/messages/{id}/reactions` 全链 `code 0`,因此在用户本条入站消息上贴 `OnIt`(官方「需处理/进行中」语义):回合开始贴、回复/卡片落定即删、没删成 2 分钟 TTL 自清;同一聊天只留一个;表情接口缺权/限频/消息被撕一律静默,不阻断回合
30
+ - 飞书 `caps.typing` / `caps.statusReaction` 由 false 改 true(声明=真实现:两个位同一个落地物)
31
+
32
+ ### Changed
33
+
34
+ - 入站接收时记住 `chatId → message_id`(状态位锚点);状态位仅沾在用户那条消息上,不额外发一条「思考中…」占位消息,不污染会话历史
35
+
36
+ ## 0.4.10(多回合接续:同一聊天不得被劈成两条轨迹)
37
+
38
+ ### Fixed
39
+
40
+ - **飞书第二回合另起会话,轨迹分叉(真机 desktop 实证)**:`apply` 期会话后端是 `none`,首回合入站时 `ensureSessionId` 直接返回隐式 id(`im:union:feishu:dm:oc_…`)——宿主按该 id 落了盘(目录 `im~003Aunion…`);惰性后端随后升级 `native`,第二回合就调 `createSession(hint)`,而 desktop 线 `sessionController.create` 面缺位 → 回退 `agents.create`(不收 sessionId)→ 宿主生成 `session-uuid`,`router.bind` 改绑到 UUID。结果同一聊天被劈成两段:首段在 `im:union:` 目录(进「频道」分组),第二段在 `session-uuid` 目录(落「未分组」),jsonl 各写各的
41
+ - **修法(先查后建)**:`ensureSessionId` 在 `createSession` 之前先查宿主会话列表,已有该 `im:union:` id 则直接接续(不 create、不重复应用账号偏好——存量会话不动);命中后进进程内缓存,后续回合不重复查。查询面异常/缺位按「未存在」处理,仍走 create 兜底
42
+ - **可操作的排障日志**:宿主改派成非 `im:` 前缀 id(会落「未分组」)时,除既有「请求 id → 宿主实际 id」info,另打一条 warn 给出 `/session <原隐式 id>` 的接回手段;不再只留一条 id 差异让人猜
43
+
44
+ ## 0.4.9(飞书诊断假红收口 + 可选密钥不再算缺失)
45
+
46
+ ### Fixed
47
+
48
+ - **飞书「凭据无效」假红(真机 desktop 实证)**:`diagnose` 凭据位探的是 `GET /open-apis/bot/v2/info`——该路径不存在(服务端回 `404 page not found` 纯文本,SDK 对非 2xx 直接抛),于是任何正确凭据都落 catch 分支报「凭据或网络无效」。现改探 `POST /open-apis/auth/v3/tenant_access_token/internal`(唯一对 appSecret 做真裁决的面:错 secret → 10014),并把「机器人能力」拆成独立位 `bot-ability`(`GET /open-apis/bot/v3/info`:有 bot → code 0;无 bot → 11205)——bot 面本身不校验 secret(错 secret 同样回 11205),探它只能验能力不能验凭据
49
+ - **「长连接通过」不再被误读为可收发**:`ws-long-connection` 置 passed 时附提示「长连接已建立(不能据此推定机器人能力/消息权限已开通)」——无机器人能力时事件不投递、消息发不出,与长连接握手是两件事
50
+ - **可选密钥被算成「凭据缺失」**:`listPublic` 对 meta 里所有 `secret:true` 字段一律下发 `configured`,设置页据此把未入库的**可选**键(飞书 `dashScopeKey` 语音、微信 `botToken` 扫码自动写)标成红徽标「凭据缺失:…」,观感上等同连接被阻断。现 `secretFields` 随字段下发 `required`,客户端只对 `required!==false` 的缺失出红徽标
51
+
52
+ ### Changed
53
+
54
+ - 诊断 hint 不再一律「凭据或网络无效」:网络类错误回「网络不可达:<message>」,其余回「凭据探测失败:<message>」(只取 error.message,不透传 axios config——那里带着 Authorization 与请求体)
55
+
56
+ ## 0.4.8(宿主枚举补票:0.2.1-rc.3)
57
+
58
+ ### Changed
59
+
60
+ - hosts.mjs 单源追加 `0.2.1-rc.3`(12→13 tag),sync-hosts 下发 package.json engines/6 个 dsh-* peers、dsh.plugin.json、README 双语管理块与 locale/plugin 描述同串。
61
+ - 补票依据:rc.2→rc.3 契约面零断裂(host-matrix 14 列配对探针复核 + source-analysis v0.2.1-rc.3 四件套);本插件不触 `ctx.web` provider 钉选、模型标签槽与 `startSession` 系调用面。
5
62
  ## 0.4.7(/relay 别名解析)
6
63
 
7
64
  ### Added
package/README.en.md CHANGED
@@ -3,7 +3,7 @@
3
3
  A union IM bridge for DeepSeek Harness — a clean-room reimplementation merging the capability sets of xmanrui/dsh-im and MichengAI/dsh-im-connect, with no source copied from either.
4
4
 
5
5
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
6
- - **Supported DSH hosts:** `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-alpha.2` `0.2.1-alpha.3` `0.2.1-rc.1` `0.2.1-rc.2` (12 rc/alpha releases across lines 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1)
6
+ - **Supported DSH hosts:** `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-alpha.2` `0.2.1-alpha.3` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3` (13 rc/alpha releases across lines 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1)
7
7
  <!-- host-compat:end -->
8
8
 
9
9
  ## Status
@@ -31,6 +31,78 @@ Then add accounts in the "IM union bridge" panel: most channels use credential b
31
31
  - The Feishu voice tier requires system `ffmpeg` on PATH (the transcoding binary is not shipped; it is only invoked when the voice tier is enabled).
32
32
  - The iMessage channel is macOS only.
33
33
 
34
+ ## Usage
35
+
36
+ After installing, manage accounts and preferences from the **IM Union Bridge** section of the host's settings page; day-to-day operation happens entirely inside chat (`/help` lists the commands).
37
+
38
+ ### Adding an account
39
+
40
+ Three bind shapes; the panel renders each form from channel metadata:
41
+
42
+ | Bind | Channels | Notes |
43
+ |---|---|---|
44
+ | Credentials (9) | telegram / discord / slack / matrix / imessage / wecom-app / email / dingtalk-notify / qq-notify | Bot token or app credentials; secret fields are written one-way and never returned by any response |
45
+ | Credentials or QR (4) | feishu / wecom / dingtalk / qq | Either fill app credentials, or create the bot by scanning from the panel |
46
+ | QR (2) | weixin / whatsapp | WeChat via the official iLink QR bind; WhatsApp via Baileys pairing |
47
+
48
+ Account IDs are generated (`<channel>-<ts36><rand>`) — **you never fill one in**. The *alias* is the human-facing name and is editable in the panel.
49
+
50
+ ### Command panel (25)
51
+
52
+ | Group | Commands |
53
+ |---|---|
54
+ | Session | `/help` `/menu` `/status` `/new` `/stop` `/steer <text>` `/sessions [all] [page]` `/session <index\|id-fragment>` `/fork` `/rename <title>` `/unbind` `/goal <text\|->` `/queue` `/mute` |
55
+ | Models & presets | `/models` `/model <id>` `/reasoning <tier>` `/presets` `/preset <name>` `/workspaces` `/workspace <path\|->` |
56
+ | Delivery & relay | `/delivery [retry <n>]` `/export` `/relay <account\|->` `/speak <text>` |
57
+
58
+ Commands are gated by the per-account allowlist; in groups only explicit @-mentions trigger them. Tool approvals and agent questions are answered in the originating chat (deny-by-default, initiator only, 5-minute expiry).
59
+
60
+ ### Final-answer relay (`/relay`)
61
+
62
+ Mirror every turn's final answer of the **current session** to a send-only notify account (DingTalk group webhook / QQ active push):
63
+
64
+ ```text
65
+ /relay list this session's binding (prints usage when unbound)
66
+ /relay - unbind
67
+ /relay <account> bind; <account> is the surrogate ID or the alias
68
+ /relay <account>:<targetId> bind and set the push target explicitly
69
+ ```
70
+
71
+ - **Resolution order**: surrogate ID first, then a **unique** alias match. An ambiguous alias is refused outright with a hint to use the account ID — the plugin never guesses.
72
+ - **targetId per channel**: DingTalk's webhook already pins the destination group, so it may be omitted; QQ uses `chatId` as openid/group_openid and **requires** it.
73
+ - **Semantics**: session-scoped, re-binding overwrites, many sessions may point at one account; the mirrored text carries a `[source-channel/session]` prefix plus the same turn-end suffix; an uncertain delivery is **never re-sent** (failures land in `plugin.log`).
74
+ - `/new` rotation does not inherit the binding (re-run `/relay`), and one session targets one notify account at a time.
75
+
76
+ ### Tasks / Channels sidebar
77
+
78
+ The plugin wraps the host sidebar into two self-owned tabs: **Tasks** renders the official session tree with IM sessions filtered out (they never surface as "ungrouped" rows), **Channels** shows IM sessions grouped by messenger with search, a running indicator and click-to-open. When another plugin already owns the slot the plugin **stands down entirely** and the channel list remains available as a card inside the settings panel.
79
+
80
+ ### Model allowlist
81
+
82
+ Enter one `provider/model` per line in the settings textarea (empty = no restriction). When set, `/models` lists only allowlisted models and `/model` rejects anything else while echoing the usable set; account-level model preferences are exempt. Purpose: when a model misbehaves, an IM-side user still has a vetted fallback to switch to.
83
+
84
+ ### Charts (svg → image)
85
+
86
+ Ask for a diagram and the assistant emits a single `svg` code fence (complete inline SVG, white background, dark foreground, width ≤ 1000); the plugin renders it to PNG and sends it as an image. With no renderer available, or on channels that cannot take images, it degrades to text instead of blocking the answer.
87
+
88
+ ### IM-concise agent preset
89
+
90
+ The plugin registers an `im-concise` agent preset with the host and uses it for IM sessions by default: a complete-mode persona that keeps rich-structure injections (GenUI fences, raw HTML) out of text-only chats, plus short plain-text discipline. Override it per account under *session preferences → Agent preset*; reply sanitisation stays on as a backstop either way.
91
+
92
+ ### Administration API
93
+
94
+ `/api/dsh-im-union/*` reuses the host login session (every request passes the Host/Origin/Cookie checks) and returns zero credentials and zero internal error detail:
95
+
96
+ | Group | Endpoints |
97
+ |---|---|
98
+ | Probes | `/ping` `/state` |
99
+ | Accounts | `/accounts` `/accounts/save` `/accounts/pause` `/accounts/remove` `/accounts/allowlist` `/accounts/check` |
100
+ | Catalogs | `/session-catalog` `/channels` |
101
+ | QR bind | `/weixin/qr/start` `/weixin/qr/status` |
102
+ | Proactive delivery | `/proactive/send` `/proactive/targets` `/proactive/targets/remove` `/proactive/syncback` |
103
+ | Relay & allowlist | `/relay-bindings` `/relay-bindings/bind` `/relay-bindings/unbind` `/model-whitelist` |
104
+ | Misc | `/connector/jobs` `/connector/jobs/cancel` `/wecom-app/callback` (third-party webhook; signature verification *is* the authentication) |
105
+
34
106
  ## Development
35
107
 
36
108
  ```sh
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  DeepSeek Harness 的 IM 联合桥——净室重写,能力合集来自社区两仓(xmanrui/dsh-im 与 MichengAI/dsh-im-connect),不复制其源码。
4
4
 
5
5
  <!-- host-compat:begin — generated by scripts/sync-hosts.mjs from scripts/hosts.mjs; DO NOT EDIT -->
6
- - **支持的 DSH 宿主:** `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-alpha.2` `0.2.1-alpha.3` `0.2.1-rc.1` `0.2.1-rc.2`(覆盖 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 共 12 个 rc/alpha)
6
+ - **支持的 DSH 宿主:** `0.1.2-rc.1` `0.1.5-rc.1` `0.1.5-rc.2` `0.1.5-rc.3` `0.1.7-rc.1` `0.1.7-rc.2` `0.2.0-rc.1` `0.2.0-rc.2` `0.2.1-alpha.2` `0.2.1-alpha.3` `0.2.1-rc.1` `0.2.1-rc.2` `0.2.1-rc.3`(覆盖 0.1.2 / 0.1.5 / 0.1.7 / 0.2.0 / 0.2.1 共 13 个 rc/alpha)
7
7
  <!-- host-compat:end -->
8
8
 
9
9
  ## 状态
@@ -31,6 +31,78 @@ dsh plugin --profile web add dsh-im-union@latest --registry=https://registry.npm
31
31
  - 飞书语音档需要系统 `ffmpeg` 在 PATH 中(转码二进制不随包分发,仅启用语音档时才会调用)。
32
32
  - iMessage 渠道仅 macOS 可用。
33
33
 
34
+ ## 使用说明
35
+
36
+ 装好后在宿主设置页/侧栏的「IM 联合桥」面板管理账号与偏好;日常操作全部可在聊天内完成(发 `/help` 看命令)。
37
+
38
+ ### 添加账号
39
+
40
+ 三类绑定形态,面板按渠道元数据自动出表单:
41
+
42
+ | 形态 | 渠道 | 说明 |
43
+ |---|---|---|
44
+ | 凭据表单(9) | telegram / discord / slack / matrix / imessage / wecom-app / email / dingtalk-notify / qq-notify | 填 Bot Token 或 App 凭据;secret 字段只单向入库,任何回包零凭据 |
45
+ | 凭据或扫码(4) | feishu / wecom / dingtalk / qq | 二选一:填应用凭据,或走面板扫码建机器人 |
46
+ | 扫码(2) | weixin / whatsapp | 微信走官方 iLink 扫码;WhatsApp 走 Baileys 配对 |
47
+
48
+ 账号 ID 由系统自动生成(`<channel>-<ts36><rand>`),**无需也不应手填**;「别名」才是给人看的名字,可在面板设置。
49
+
50
+ ### 命令面板(25 条)
51
+
52
+ | 组 | 命令 |
53
+ |---|---|
54
+ | 会话 | `/help` `/menu` `/status` `/new` `/stop` `/steer <文本>` `/sessions [all] [页]` `/session <序号\|id片段>` `/fork` `/rename <标题>` `/unbind` `/goal <文本\|->` `/queue` `/mute` |
55
+ | 模型与预设 | `/models` `/model <id>` `/reasoning <档位>` `/presets` `/preset <名称>` `/workspaces` `/workspace <路径\|->` |
56
+ | 投递与抄送 | `/delivery [retry <序号>]` `/export` `/relay <账号\|->` `/speak <文本>` |
57
+
58
+ 命令默认只对白名单用户开放;群聊只认显式 @ 本机器人。工具审批与交互提问在原聊天回答(默认拒绝、发起者限定、5 分钟超时)。
59
+
60
+ ### 通知抄送(`/relay`)
61
+
62
+ 把**当前会话**每回合的最终回答镜像到一个纯出站通知账号(钉钉群 webhook / QQ 主动推送):
63
+
64
+ ```text
65
+ /relay 列出本会话绑定(未绑定时回用法提示)
66
+ /relay - 解绑
67
+ /relay <账号> 绑定;<账号> 可为代理键 ID 或别名
68
+ /relay <账号>:<targetId> 绑定并指定推送目标
69
+ ```
70
+
71
+ - **解析顺序**:先按账号 ID 直查,未命中再按别名**唯一**匹配;别名歧义时直接拒绝并提示改用账号 ID,绝不猜。
72
+ - **targetId 按渠道**:钉钉的 webhook 已锁定目标群,可省;QQ 用 `chatId` 作 openid/group_openid,**必填**。
73
+ - **语义**:会话级绑定、重复绑定即换目标(覆盖)、多会话可指向同一账号;抄送文本带 `[源渠道/会话短id]` 前缀与同款收轮后缀;投递结果不明时**绝不自动重发**,失败落 `plugin.log`。
74
+ - `/new` 轮换后新会话不继承旧绑定(需重新 `/relay`);一个会话同时只指向一个通知账号。
75
+
76
+ ### 侧栏「任务 / 频道」
77
+
78
+ 插件把宿主侧栏包成两个自持页签:**任务**=官方会话树(已过滤 IM 会话,不会冒出「未分组」条目)、**频道**=按渠道分组的 IM 会话列表(搜索、运行状态点、点击打开)。检测到侧栏已被其它插件接管时**完全不插手**,此时渠道列表退化为设置面板内的卡片。
79
+
80
+ ### 模型白名单
81
+
82
+ 设置面板「模型白名单」框一行一个 `provider/model`(留空=不限)。非空时 `/models` 只列白名单内、`/model` 越界被拒并回列可用项;账号级模型偏好不受约束。用途:模型异常时 IM 远端仍有可切的兜底集合。
83
+
84
+ ### 图表(svg → 图片)
85
+
86
+ 需要图示时让助手输出一个 `svg` 代码围栏(完整内联 SVG、白底深色前景、宽 ≤1000),插件会渲染成 PNG 作为图片消息发出;渲染依赖不在位或渠道不支持图片时降级为纯文字,不阻断回答。
87
+
88
+ ### IM 简洁预设
89
+
90
+ 插件向宿主注册 `im-concise` 预设并作为 IM 会话默认:persona 完整模式(从源头阻断 GenUI 围栏、裸 HTML 等富结构注入)+ 简短纯文字纪律。账号「会话偏好 → Agent 预设」可改用其它预设;出站净化作为兜底安全网始终生效。
91
+
92
+ ### 管理 API
93
+
94
+ `/api/dsh-im-union/*` 复用宿主登录态(每请求过 Host/Origin/Cookie 三查),回包零凭据零内部异常细节:
95
+
96
+ | 组 | 端点 |
97
+ |---|---|
98
+ | 探针 | `/ping` `/state` |
99
+ | 账号 | `/accounts` `/accounts/save` `/accounts/pause` `/accounts/remove` `/accounts/allowlist` `/accounts/check` |
100
+ | 目录 | `/session-catalog` `/channels` |
101
+ | 扫码 | `/weixin/qr/start` `/weixin/qr/status` |
102
+ | 主动投递 | `/proactive/send` `/proactive/targets` `/proactive/targets/remove` `/proactive/syncback` |
103
+ | 抄送与白名单 | `/relay-bindings` `/relay-bindings/bind` `/relay-bindings/unbind` `/model-whitelist` |
104
+ | 其它 | `/connector/jobs` `/connector/jobs/cancel` `/wecom-app/callback`(第三方回调,验签即认证) |
105
+
34
106
  ## 开发
35
107
 
36
108
  ```sh
package/client.js CHANGED
@@ -785,7 +785,8 @@ select.diu-input { appearance: auto; }
785
785
 
786
786
  var missingSecrets = [];
787
787
  (row.secretFields || []).forEach(function (sf) {
788
- if (!sf.configured) missingSecrets.push(sf.key);
788
+ // 可选密钥(语音 DashScope Key / 扫码 botToken)未入库不算缺失:不进红徽标。
789
+ if (!sf.configured && sf.required !== false) missingSecrets.push(sf.key);
789
790
  });
790
791
  var badge = row.paused
791
792
  ? h(Tag, { tone: 'Muted' }, t('accounts.state.paused'))
package/dsh.plugin.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "id": "dsh-im-union",
3
3
  "name": "dsh-im-union",
4
- "version": "0.4.7",
4
+ "version": "0.4.12",
5
5
  "description": {
6
- "en": "Union IM bridge for DeepSeek Harness: multi-account bots across messaging platforms, each chat bound to its own session, with cross-host-version compatibility (0.1.2-rc.1 .. 0.2.1-rc.2).",
7
- "zh": "DeepSeek Harness 的 IM 联合桥:多平台多账号机器人、每个聊天绑定独立会话,跨宿主版本兼容(0.1.2-rc.1 至 0.2.1-rc.2)。"
6
+ "en": "Union IM bridge for DeepSeek Harness: multi-account bots across messaging platforms, each chat bound to its own session, with cross-host-version compatibility (0.1.2-rc.1 .. 0.2.1-rc.3).",
7
+ "zh": "DeepSeek Harness 的 IM 联合桥:多平台多账号机器人、每个聊天绑定独立会话,跨宿主版本兼容(0.1.2-rc.1 至 0.2.1-rc.3)。"
8
8
  },
9
9
  "license": "MIT",
10
10
  "engines": {
11
- "dsh": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-rc.1 || 0.2.1-rc.2"
11
+ "dsh": "0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-alpha.3 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
12
12
  },
13
13
  "main": "./lib/index.js",
14
14
  "client": {
@@ -63,6 +63,25 @@ interface FeishuUploadResult {
63
63
  interface FeishuResourceResult {
64
64
  getReadableStream?: () => AsyncIterable<Uint8Array>;
65
65
  }
66
+ /** 卡片回传事件(card.action.trigger)归一后的形——SDK 长连接与 webhook 两形态共用。 */
67
+ export interface FeishuCardAction {
68
+ messageId: string;
69
+ chatId: string;
70
+ operator: {
71
+ openId?: string;
72
+ userId?: string;
73
+ };
74
+ action: {
75
+ value?: Record<string, unknown>;
76
+ };
77
+ }
78
+ /**
79
+ * 从 card.action.trigger 负载取关键字段。两种形态都要接:
80
+ * - 长连接/webhook 原始体:`{schema:'2.0', header:{event_type}, event:{context, operator, action}}`;
81
+ * - SDK 安全管道 normalize 后的形:`{messageId, chatId, operator:{openId}, action:{value}}`。
82
+ * 缺关键字段回 undefined(调用方丢弃,不猜)。
83
+ */
84
+ export declare function extractCardAction(payload: any): FeishuCardAction | undefined;
66
85
  export interface LarkLike {
67
86
  WSClient: new (options: {
68
87
  appId: string;
@@ -73,7 +92,7 @@ export interface LarkLike {
73
92
  }) => Promise<void> | void;
74
93
  };
75
94
  EventDispatcher: new (options: Record<string, unknown>) => {
76
- register: (handlers: Record<string, (data: FeishuEventPayload) => Promise<void> | void>) => unknown;
95
+ register: (handlers: Record<string, (data: any) => unknown>) => unknown;
77
96
  };
78
97
  Client: new (options: {
79
98
  appId: string;
@@ -202,13 +221,22 @@ export declare class FeishuAdapter implements ChannelAdapter {
202
221
  /** 语音档位(未开启=undefined:入站只留占位,零外部调用)。 */
203
222
  private readonly voice;
204
223
  private tenantToken;
224
+ /** chatId → 最近一条入站消息 id(typing 状态位挂在用户那条消息上)。 */
225
+ private readonly lastInboundMessageId;
226
+ /** chatId → 在位「处理中」表情(回复落地/超时回收)。 */
227
+ private readonly typingReactions;
205
228
  private listeners;
206
229
  private client;
207
230
  private wsClient;
208
231
  private lark;
209
232
  private readonly log;
210
233
  /** 事件处理器表(测试注入口:直接驱动 receive handler)。 */
211
- handlers: Record<string, (data: FeishuEventPayload) => Promise<void> | void> | undefined;
234
+ handlers: Record<string, (data: any) => unknown> | undefined;
235
+ /**
236
+ * 在位的选项卡:message_id → 待答。
237
+ * 键用 message_id 而非 chatId:一个聊天可能先后出多张卡,旧卡迟到点击不得误判新卡。
238
+ */
239
+ private readonly pendingChoices;
212
240
  constructor(options: FeishuAdapterOptions);
213
241
  onMessage(listener: MessageListener): () => void;
214
242
  private requireLark;
@@ -219,6 +247,33 @@ export declare class FeishuAdapter implements ChannelAdapter {
219
247
  /** 权限类错误码 → 可读引导(不透传内部错误体)。 */
220
248
  private translateApiError;
221
249
  reply(address: ChatAddress, text: string): Promise<Receipt>;
250
+ /**
251
+ * 回合开始「处理中」信号(引擎在 prompt 前调用)。飞书无 typing API,落地为
252
+ * 给用户本条入站消息加 OnIt 表情;同一聊天只留一个,回复落地/2 分钟超时自清。
253
+ * 任一环节失败一律静默——这是增强项,不得阻断回合。
254
+ */
255
+ setTyping(address: ChatAddress): Promise<void>;
256
+ /** 回收「处理中」表情(重复调用幂等)。 */
257
+ private clearTyping;
258
+ /**
259
+ * 审批/选项确认卡片(T1.9 按钮面)。
260
+ * 协议事实(真机实测):2.0 schema 的 `body.elements` 已不支持 `action` 标签
261
+ * (230099 / 200861 unsupported tag action),按钮只能走 1.0 形态(顶层 elements);
262
+ * 内联 interactive 消息不碰 cardkit 实体面,因此不需要卡片实体权限。
263
+ * multi=true 不接(卡片无多选原语):抛错由 broker 回落文本菜单,不造假。
264
+ */
265
+ askChoices(address: ChatAddress, question: string, options: readonly string[], multi: boolean, signal?: AbortSignal, opts?: {
266
+ ownerUserId?: string;
267
+ }): Promise<{
268
+ answer: string;
269
+ uncertain: boolean;
270
+ }>;
271
+ /**
272
+ * 卡片按钮点击回传(card.action.trigger,长连接同一条通道)。
273
+ * D5 纪律:只有发起者能定夺,他人点击不改判;超时/pending 已清则忽略。
274
+ * 回包给 toast(飞书卡片回调响应),让用户看到「已记录」。
275
+ */
276
+ private handleCardAction;
222
277
  /**
223
278
  * 出站文件(T2.4):im.v1.file.create(file_type=stream)→ im.v1.message.create
224
279
  * (msg_type=file, content.file_key)。限额:引擎 FILE_BUDGET 在上游已裁(20 MiB ≤ 4),
@@ -257,12 +312,26 @@ export declare class FeishuAdapter implements ChannelAdapter {
257
312
  speak(address: ChatAddress, text: string, _signal?: AbortSignal): Promise<Receipt>;
258
313
  /** tenant_access_token 自管(提前 60s 刷新;公开协议 auth/v3/internal)。 */
259
314
  private token;
315
+ /** 通用 REST(带 tenant token):「处理中」表情等非卡片面共用。 */
316
+ private rest;
260
317
  private cardApi;
261
318
  /** 开流:建卡片实体(streaming_mode 开)→ interactive 消息引用 card_id。 */
262
319
  beginStream(address: ChatAddress): Promise<import('./types.js').ReplyStream>;
263
320
  private createCardMessage;
264
321
  authorize(): boolean | undefined;
265
322
  diagnose(): Promise<ChannelDiagnosisCheck[]>;
323
+ /**
324
+ * 凭据探测:auth/v3/tenant_access_token 换 token(唯一对 appSecret 做真裁决的面)。
325
+ * 曾探 `/open-apis/bot/v2/info`——该路径不存在(服务端 404 纯文本、SDK 直接抛),
326
+ * 任何凭据都落「凭据或网络无效」;且 bot 面本身不校验 secret(错 secret 同样回
327
+ * 11205),探它只能验能力不能验凭据——故拆成凭据位与机器人能力位两条。
328
+ */
329
+ private credentialsCheck;
330
+ /**
331
+ * 机器人能力探测:GET /open-apis/bot/v3/info(有 bot → code 0;无 bot → 11205)。
332
+ * 这是「长连接通了却收不到消息」的判定位:无机器人能力时事件不投递、消息发不出。
333
+ */
334
+ private botAbilityCheck;
266
335
  /**
267
336
  * 语音档位诊断(T3.5 三态):
268
337
  * - 未启用(voiceEnabled 非 'true')= unverified——能力存在但未开通,不谎报;
@@ -271,8 +340,8 @@ export declare class FeishuAdapter implements ChannelAdapter {
271
340
  * 未注入探测面时不真起进程(禁真实副作用),诚实报 unverified。
272
341
  */
273
342
  private voiceCheck;
274
- /** 测试/诊断驱动:模拟一条 receive 事件(与 SDK handler 同一入口)。 */
275
- injectEvent(eventName: string, payload: FeishuEventPayload): Promise<void>;
343
+ /** 测试/诊断驱动:模拟一条事件(与 SDK handler 同一入口);回传 handler 的响应体。 */
344
+ injectEvent(eventName: string, payload: unknown): Promise<unknown>;
276
345
  }
277
346
  /**
278
347
  * 工厂挂 registry(懒加载动态 import SDK 面)。语音档位全配置驱动、默认关: