dsh-im-union 0.0.1 → 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.
- package/CHANGELOG.md +21 -0
- package/README.en.md +10 -2
- package/README.md +10 -2
- package/THIRD_PARTY_NOTICES.md +18 -8
- package/client.js +126 -2
- package/dsh.plugin.json +1 -1
- package/lib/admin/api.d.ts +19 -1
- package/lib/admin/api.js +63 -14
- package/lib/channels/dingtalk.d.ts +186 -0
- package/lib/channels/dingtalk.js +1017 -0
- package/lib/channels/discord.d.ts +45 -3
- package/lib/channels/discord.js +119 -28
- package/lib/channels/feishu.d.ts +160 -8
- package/lib/channels/feishu.js +478 -20
- package/lib/channels/imessage.d.ts +158 -0
- package/lib/channels/imessage.js +547 -0
- package/lib/channels/matrix.d.ts +262 -0
- package/lib/channels/matrix.js +888 -0
- package/lib/channels/meta.js +49 -1
- package/lib/channels/qq.d.ts +214 -0
- package/lib/channels/qq.js +706 -0
- package/lib/channels/registry.js +7 -0
- package/lib/channels/slack.d.ts +232 -0
- package/lib/channels/slack.js +896 -0
- package/lib/channels/telegram.d.ts +23 -1
- package/lib/channels/telegram.js +78 -16
- package/lib/channels/types.d.ts +6 -0
- package/lib/channels/wecom-app.d.ts +138 -0
- package/lib/channels/wecom-app.js +600 -0
- package/lib/channels/wecom.d.ts +11 -3
- package/lib/channels/wecom.js +2 -1
- package/lib/channels/whatsapp.d.ts +198 -0
- package/lib/channels/whatsapp.js +471 -0
- package/lib/client.js +126 -2
- package/lib/engine/account-manager.d.ts +9 -2
- package/lib/engine/account-manager.js +20 -3
- package/lib/engine/commands.d.ts +17 -0
- package/lib/engine/commands.js +78 -0
- package/lib/engine/connector.d.ts +232 -0
- package/lib/engine/connector.js +767 -0
- package/lib/engine/delivery-log.d.ts +62 -0
- package/lib/engine/delivery-log.js +166 -0
- package/lib/engine/files.d.ts +42 -0
- package/lib/engine/files.js +50 -0
- package/lib/engine/fragments.d.ts +55 -0
- package/lib/engine/fragments.js +89 -0
- package/lib/engine/gateway.d.ts +38 -12
- package/lib/engine/gateway.js +388 -15
- package/lib/engine/proactive.d.ts +146 -0
- package/lib/engine/proactive.js +335 -0
- package/lib/engine/quote.d.ts +51 -0
- package/lib/engine/quote.js +82 -0
- package/lib/engine/voice.d.ts +162 -0
- package/lib/engine/voice.js +553 -0
- package/lib/engine/zip.d.ts +14 -0
- package/lib/engine/zip.js +131 -0
- package/lib/host-compat/deliverables-compat.d.ts +49 -0
- package/lib/host-compat/deliverables-compat.js +96 -0
- package/lib/host-compat/services.d.ts +2 -0
- package/lib/host-compat/services.js +2 -0
- package/lib/host-compat/session-api.d.ts +10 -0
- package/lib/host-compat/session-api.js +69 -0
- package/lib/host-compat/session-source.d.ts +9 -0
- package/lib/host-compat/session-source.js +45 -0
- package/lib/host-compat/uploads-compat.d.ts +44 -0
- package/lib/host-compat/uploads-compat.js +66 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +158 -4
- package/package.json +2 -2
- package/src/admin/api.ts +79 -13
- package/src/channels/dingtalk.ts +1126 -0
- package/src/channels/discord.ts +156 -39
- package/src/channels/feishu.ts +557 -26
- package/src/channels/imessage.ts +646 -0
- package/src/channels/matrix.ts +1045 -0
- package/src/channels/meta.ts +49 -1
- package/src/channels/qq.ts +853 -0
- package/src/channels/registry.ts +7 -0
- package/src/channels/slack.ts +1024 -0
- package/src/channels/telegram.ts +93 -18
- package/src/channels/types.ts +6 -0
- package/src/channels/wecom-app.ts +665 -0
- package/src/channels/wecom.ts +13 -4
- package/src/channels/whatsapp.ts +598 -0
- package/src/engine/account-manager.ts +20 -3
- package/src/engine/commands.ts +83 -0
- package/src/engine/connector.ts +901 -0
- package/src/engine/delivery-log.ts +201 -0
- package/src/engine/files.ts +74 -0
- package/src/engine/fragments.ts +113 -0
- package/src/engine/gateway.ts +379 -15
- package/src/engine/proactive.ts +457 -0
- package/src/engine/quote.ts +84 -0
- package/src/engine/voice.ts +673 -0
- package/src/engine/zip.ts +154 -0
- package/src/host-compat/deliverables-compat.ts +141 -0
- package/src/host-compat/services.ts +4 -0
- package/src/host-compat/session-api.ts +79 -0
- package/src/host-compat/session-source.ts +47 -0
- package/src/host-compat/uploads-compat.ts +126 -0
- package/src/index.ts +158 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
本项目版本号遵循语义化版本;发布面与宿主兼容矩阵见 `scripts/hosts.mjs`(README 管理块同源)。
|
|
4
4
|
|
|
5
|
+
## 0.1.0(首个功能版,dist-tag=latest)
|
|
6
|
+
|
|
7
|
+
M1–M3 代码侧全量:14 渠道协议层 + 跨版本窄腰 + 会话引擎 + 管理面。
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 渠道(P0):微信 iLink(官方 ClawBot 协议 + 扫码绑定 UI)、飞书(WS 长连 + 流式卡片/逐步消息双档 + 语音档可选)、Telegram(长轮询 + 编辑流式)、Discord(Gateway v10 + 表格转竖排)、企微智能机器人(长连 + 思考中/工具进度原生显示)
|
|
12
|
+
- 渠道(P1):钉钉 stream + AI Card;QQ 官方 API v2(被动窗口);Slack Socket Mode 零 SDK + thread 模型
|
|
13
|
+
- 渠道(P2):WhatsApp(Baileys,QR 配对 + auth 目录持久化);Matrix(CS API,E2EE 两档诚实档位);iMessage(仅 macOS,非 darwin 硬拒载);企微自建应用(回调验签 + 加解密,webhook 白名单路由)
|
|
14
|
+
- 能力:命令面板 24 命令(含 /fork /rename /delivery /export /speak /mute /goal);审批与交互提问 IM 闭环(waterfall 返回即接管,fail-closed D5);图片/文件输入(宿主元数据判视觉 E4);产物文件回传;投递日志与断线补发(≤3 次,uncertain 绝不重发 D7);超时迟到结果收割;主动投递目标库 + 双向同步;碎片续写(.. / !!);引用上下文增强;会话标题渠道徽章
|
|
15
|
+
- AI Office Connector:查实宿主无公开 connector 协议面 → 离线 Job 队列诚实档(https 强制/租约/审批/并发上限)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Cordis ctx 代理未 inject 属性访问即抛(可选链拦不住)→ 窄腰全 try/catch 守卫
|
|
20
|
+
- 静音判定吞掉 /mute 自身导致的永久静默死锁;/model 与投递链 selections 键不一致导致的视觉判定永命中 unknown
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- publishConfig.tag:next → latest;THIRD_PARTY_NOTICES 运行时依赖清单与实现对齐(纠未采纳的 SDK 列表)
|
|
25
|
+
|
|
5
26
|
## 0.0.1(首发占位,tag=next)
|
|
6
27
|
|
|
7
28
|
预发布占位版本,非推荐安装线;语义为「协议层与跨版本骨架完成、真机验收进行中」。
|
package/README.en.md
CHANGED
|
@@ -8,14 +8,22 @@ A union IM bridge for DeepSeek Harness — a clean-room reimplementation merging
|
|
|
8
8
|
|
|
9
9
|
## Status
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**0.1.0 feature release**: all 14 P0/P1/P2 channel protocol layers, the cross-version narrow waist, session engine, approval/question IM loop and multi-account management are delivered (574 unit tests; host-farm smoke green on 0.1.2 / 0.1.5 / 0.1.7 / 0.2.1 lines). Outstanding real-device acceptance items are registered in CHANGELOG.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Highlights:
|
|
14
|
+
|
|
15
|
+
- **Channels**: WeChat (official iLink QR bind), Feishu/Lark (long-connection, streaming cards, optional voice tier), Telegram, Discord, WeCom intelligent bot, DingTalk (stream + AI Card), QQ official bot, Slack (Socket Mode, zero SDK), WhatsApp (Baileys QR pairing), Matrix, iMessage (macOS only), WeCom self-built app (signed callback webhook), and more; unconfigured channels load nothing (lazy imports).
|
|
16
|
+
- **In-chat loop**: 24 commands (`/help /status /new /stop /steer /model(s) /reasoning /preset(s) /session(s) /workspace(s) /fork /rename /queue /mute /unbind /goal /delivery /export /speak /menu`); tool approvals and agent questions answered in the originating chat (deny-by-default, 5-minute expiry, initiator only).
|
|
17
|
+
- **Cross-version**: settings surface auto-adapts gen1/gen2/gen3; capability probing only, never version branches.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
14
20
|
|
|
15
21
|
```sh
|
|
16
22
|
dsh plugin --profile web add dsh-im-union@latest --registry=https://registry.npmjs.org/
|
|
17
23
|
```
|
|
18
24
|
|
|
25
|
+
Then add accounts in the "IM union bridge" panel: most channels use credential binding (secrets are one-way into the vault and never echoed back); WeChat/WhatsApp use QR pairing; the WeCom self-built app channel requires pointing your callback URL (`/api/dsh-im-union/wecom-app/callback`) in its console. See `SECURITY.md` for the non-negotiable privacy red lines and `THIRD_PARTY_NOTICES.md` for the clean-room provenance.
|
|
26
|
+
|
|
19
27
|
## Development
|
|
20
28
|
|
|
21
29
|
```sh
|
package/README.md
CHANGED
|
@@ -8,14 +8,22 @@ DeepSeek Harness 的 IM 联合桥——净室重写,能力合集来自社区
|
|
|
8
8
|
|
|
9
9
|
## 状态
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**0.1.0 功能版**:P0/P1/P2 十四渠道协议层、跨版本窄腰、会话引擎、审批/提问 IM 闭环、多账号管理面均已交付(单测 574 项;宿主农场 0.1.2/0.1.5/0.1.7/0.2.1 四线冒烟全绿)。真机验收项逐项登记在 CHANGELOG;能力分期见开发仓计划台账。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
能力总览:
|
|
14
|
+
|
|
15
|
+
- **渠道**:微信(官方 iLink 扫码)、飞书(长连/流式卡片/语音档可选)、Telegram、Discord、企微智能机器人(长连)、钉钉(stream+AI Card)、QQ 官方机器人、Slack(Socket Mode)、WhatsApp(Baileys 扫码)、Matrix、iMessage(仅 macOS)、企微自建应用(回调)等;未配置渠道零依赖零加载(懒 import)。
|
|
16
|
+
- **IM 内闭环**:24 命令面板(/help /status /new /stop /steer /model(s) /reasoning /preset(s) /session(s) /workspace(s) /fork /rename /queue /mute /unbind /goal /delivery /export /speak /menu);工具审批与交互提问原聊天回答(默认拒绝、5 分钟超时、发起者限定)。
|
|
17
|
+
- **跨版本**:设置面 gen1/gen2/gen3 三态自适配;能力探测优先,禁版本号分支。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
14
20
|
|
|
15
21
|
```sh
|
|
16
22
|
dsh plugin --profile web add dsh-im-union@latest --registry=https://registry.npmjs.org/
|
|
17
23
|
```
|
|
18
24
|
|
|
25
|
+
装好后在宿主侧栏/设置页的「IM 联合桥」面板添加账号:大部分渠道走凭据绑定(Bot Token/App 凭据,secret 字段只单向入库不回显);微信/WhatsApp 走扫码绑定;企微自建应用需将本插件管理面回调 URL(`/api/dsh-im-union/wecom-app/callback`)配入后台接收服务器。隐私与凭据红线见 `SECURITY.md`;第三方依赖声明见 `THIRD_PARTY_NOTICES.md`(净室重实现,不复制任何上游源码)。
|
|
26
|
+
|
|
19
27
|
## 开发
|
|
20
28
|
|
|
21
29
|
```sh
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -16,12 +16,22 @@ dependencies only.
|
|
|
16
16
|
|
|
17
17
|
## Runtime npm dependencies (consumed as-is, licenses per their packages)
|
|
18
18
|
|
|
19
|
-
- @larksuiteoapi/node-sdk (MIT) — Feishu/Lark bot long-connection SDK
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- @
|
|
23
|
-
|
|
24
|
-
- qrcode (MIT) — QR rendering for
|
|
19
|
+
- @larksuiteoapi/node-sdk (MIT) — Feishu/Lark bot long-connection SDK (its WS
|
|
20
|
+
handshake/signature surface is a private protocol, so the official SDK is used
|
|
21
|
+
instead of a clean-room reimplementation; lazily imported)
|
|
22
|
+
- @whiskeysockets/baileys (MPL-2.0) — WhatsApp Web multi-device protocol library
|
|
23
|
+
(lazily imported; only the WhatsApp channel touches it)
|
|
24
|
+
- qrcode (MIT) — QR PNG data-URL rendering for the WeChat iLink / WhatsApp
|
|
25
|
+
scan-to-bind flows (lazily imported)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
Everything else (Telegram, Discord, Slack, QQ, DingTalk, WeCom bot long-connection,
|
|
28
|
+
WeCom self-built app callback crypto, WeChat iLink transport, Matrix CS API) is
|
|
29
|
+
implemented directly against public protocol documents with ZERO runtime
|
|
30
|
+
dependencies (Node >= 22 global fetch/WebSocket + node:crypto/node:zlib only).
|
|
31
|
+
Earlier drafts of this file listed dingtalk-stream, @wecom/aibot-node-sdk and
|
|
32
|
+
@tencent-connect/qqbot-connector; those SDKs were never adopted — the respective
|
|
33
|
+
channels are zero-dependency clean-room implementations (corrected at 0.1.0).
|
|
34
|
+
|
|
35
|
+
Optional external tools: ffmpeg (system binary, spawned only when the Feishu
|
|
36
|
+
voice tier is explicitly enabled; never bundled, never assumed present — the
|
|
37
|
+
feature degrades honestly when missing).
|
package/client.js
CHANGED
|
@@ -37,6 +37,20 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
'accounts.title': '账号管理',
|
|
38
38
|
'accounts.empty': '暂无账号,在下方添加。',
|
|
39
39
|
'accounts.add': '添加账号',
|
|
40
|
+
'accounts.qr.get': '获取登录二维码',
|
|
41
|
+
'accounts.qr.refresh': '重新获取二维码',
|
|
42
|
+
'accounts.qr.cancel': '取消扫码',
|
|
43
|
+
'accounts.qr.needid': '请先填写账号 ID 再获取二维码',
|
|
44
|
+
'accounts.qr.wait': '等待扫码…(用微信扫上方二维码)',
|
|
45
|
+
'accounts.qr.scaned': '已扫码,请在手机上确认授权',
|
|
46
|
+
'accounts.qr.expired': '二维码已过期,请重新获取',
|
|
47
|
+
'accounts.qr.need_verifycode': '请输入手机上显示的验证码',
|
|
48
|
+
'accounts.qr.verify_code_blocked': '验证码错误次数过多,请重新获取二维码',
|
|
49
|
+
'accounts.qr.binded_redirect': '该 Bot 已绑定其它实例',
|
|
50
|
+
'accounts.qr.scaned_but_redirect': '已扫码,正在重定向等待确认',
|
|
51
|
+
'accounts.qr.code.submit': '提交验证码',
|
|
52
|
+
'accounts.qr.confirmed': '扫码绑定完成,凭据已保存并连接',
|
|
53
|
+
'accounts.saved.bindhint': '已保存(未连接):请用「获取登录二维码」完成扫码授权后自动连接',
|
|
40
54
|
'accounts.save': '保存',
|
|
41
55
|
'accounts.cancel': '取消',
|
|
42
56
|
'accounts.check': '检查连接',
|
|
@@ -73,6 +87,20 @@ window.__ModuleLoader__.load({
|
|
|
73
87
|
'accounts.title': 'Accounts',
|
|
74
88
|
'accounts.empty': 'No accounts yet. Add one below.',
|
|
75
89
|
'accounts.add': 'Add account',
|
|
90
|
+
'accounts.qr.get': 'Get login QR code',
|
|
91
|
+
'accounts.qr.refresh': 'Refresh QR code',
|
|
92
|
+
'accounts.qr.cancel': 'Cancel',
|
|
93
|
+
'accounts.qr.needid': 'Fill the account ID first, then get the QR code',
|
|
94
|
+
'accounts.qr.wait': 'Waiting for scan… (scan with WeChat)',
|
|
95
|
+
'accounts.qr.scaned': 'Scanned — confirm on your phone',
|
|
96
|
+
'accounts.qr.expired': 'QR expired, fetch a new one',
|
|
97
|
+
'accounts.qr.need_verifycode': 'Enter the code shown on your phone',
|
|
98
|
+
'accounts.qr.verify_code_blocked': 'Too many wrong codes, fetch a new QR',
|
|
99
|
+
'accounts.qr.binded_redirect': 'This bot is already bound to another instance',
|
|
100
|
+
'accounts.qr.scaned_but_redirect': 'Scanned — redirecting, waiting for confirm',
|
|
101
|
+
'accounts.qr.code.submit': 'Submit code',
|
|
102
|
+
'accounts.qr.confirmed': 'QR binding done — credentials saved and connected',
|
|
103
|
+
'accounts.saved.bindhint': 'Saved (disconnected): use "Get login QR code" to bind, it connects automatically',
|
|
76
104
|
'accounts.save': 'Save',
|
|
77
105
|
'accounts.cancel': 'Cancel',
|
|
78
106
|
'accounts.check': 'Check connection',
|
|
@@ -176,6 +204,10 @@ window.__ModuleLoader__.load({
|
|
|
176
204
|
var allowHook = useState({});
|
|
177
205
|
var allowDrafts = allowHook[0];
|
|
178
206
|
var setAllowDrafts = allowHook[1];
|
|
207
|
+
// 微信扫码绑定态(T1.2):null | {qrcode, qrDataUrl, accountId, status, verifyCode?}
|
|
208
|
+
var qrHook = useState(null);
|
|
209
|
+
var qr = qrHook[0];
|
|
210
|
+
var setQr = qrHook[1];
|
|
179
211
|
|
|
180
212
|
var load = function () {
|
|
181
213
|
getJSON('/accounts').then(function (result) {
|
|
@@ -202,10 +234,14 @@ window.__ModuleLoader__.load({
|
|
|
202
234
|
}
|
|
203
235
|
|
|
204
236
|
var submitNew = function () {
|
|
237
|
+
// 微信扫码账号:表单存而连——凭据由扫码流程写入,保存时必须跳过
|
|
238
|
+
// 适配器构建(否则 botToken 缺失工厂即抛 → 被当校验错 400,UI 假报)。
|
|
239
|
+
var payload = { record: form, secrets: form.secrets };
|
|
240
|
+
if (form.channel === 'weixin' && (form.secrets.botToken || '') === '') payload.skipConnect = true;
|
|
205
241
|
withBusy(function () {
|
|
206
|
-
return postJSON('/accounts/save',
|
|
242
|
+
return postJSON('/accounts/save', payload).then(function (result) {
|
|
207
243
|
if (result.status === 200) {
|
|
208
|
-
setNotice(t('accounts.saved'));
|
|
244
|
+
setNotice(form.channel === 'weixin' && payload.skipConnect ? t('accounts.saved.bindhint') : t('accounts.saved'));
|
|
209
245
|
setAddOpen(false);
|
|
210
246
|
setForm({ channel: form.channel, accountId: '', alias: '', secrets: {} });
|
|
211
247
|
load();
|
|
@@ -243,6 +279,51 @@ window.__ModuleLoader__.load({
|
|
|
243
279
|
});
|
|
244
280
|
};
|
|
245
281
|
|
|
282
|
+
var startQr = function () {
|
|
283
|
+
if (form.accountId === '') {
|
|
284
|
+
setNotice(t('accounts.qr.needid'));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
withBusy(function () {
|
|
288
|
+
return postJSON('/weixin/qr/start', {}).then(function (result) {
|
|
289
|
+
if (result.status === 200 && result.json && result.json.ok) {
|
|
290
|
+
setQr({ qrcode: result.json.qrcode, qrDataUrl: result.json.qrDataUrl, accountId: form.accountId, status: 'wait' });
|
|
291
|
+
} else {
|
|
292
|
+
setNotice((result.json && result.json.error) || 'HTTP ' + result.status);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// 状态驱动串行轮询:每次 setQr 产生新对象触发 effect 重跑下一拍;
|
|
299
|
+
// 服务端每请求 hold 最长 ~35s,前端不并发叠轮(cleanup 置活旗防串台)。
|
|
300
|
+
useEffect(function () {
|
|
301
|
+
if (qr === null) return undefined;
|
|
302
|
+
if (qr.status === 'confirmed' || qr.status === 'expired' || qr.status === 'need_verifycode' || qr.status === 'verify_code_blocked') return undefined;
|
|
303
|
+
var alive = true;
|
|
304
|
+
var payload = { qrcode: qr.qrcode, accountId: qr.accountId };
|
|
305
|
+
if (typeof qr.verifyCode === 'string' && qr.verifyCode !== '') payload.verifyCode = qr.verifyCode;
|
|
306
|
+
postJSON('/weixin/qr/status', payload).then(function (result) {
|
|
307
|
+
if (!alive) return;
|
|
308
|
+
if (result.status === 200 && result.json && result.json.ok) {
|
|
309
|
+
var status = result.json.status;
|
|
310
|
+
if (status === 'confirmed') {
|
|
311
|
+
setQr(Object.assign({}, qr, { status: 'confirmed' }));
|
|
312
|
+
setNotice(t('accounts.qr.confirmed'));
|
|
313
|
+
setAddOpen(false);
|
|
314
|
+
load();
|
|
315
|
+
} else {
|
|
316
|
+
setQr(Object.assign({}, qr, { status: status }));
|
|
317
|
+
}
|
|
318
|
+
} else if (alive) {
|
|
319
|
+
setQr(Object.assign({}, qr, { status: 'expired' })); // 请求异常保守收轮:引导重新取码而非假进度
|
|
320
|
+
}
|
|
321
|
+
}).catch(function () {
|
|
322
|
+
if (alive) setQr(Object.assign({}, qr, { status: 'expired' }));
|
|
323
|
+
});
|
|
324
|
+
return function () { alive = false; };
|
|
325
|
+
}, [qr]);
|
|
326
|
+
|
|
246
327
|
var body = [];
|
|
247
328
|
body.push(createElement('h4', null, t('accounts.title')));
|
|
248
329
|
if (notice !== '') body.push(createElement('p', { style: { color: '#a33' } }, notice));
|
|
@@ -350,6 +431,48 @@ window.__ModuleLoader__.load({
|
|
|
350
431
|
)
|
|
351
432
|
);
|
|
352
433
|
});
|
|
434
|
+
var qrBlock = null;
|
|
435
|
+
if (form.channel === 'weixin') {
|
|
436
|
+
if (qr === null || qr.status === 'expired') {
|
|
437
|
+
qrBlock = createElement(
|
|
438
|
+
'div',
|
|
439
|
+
{ key: 'qr-idle', style: { marginTop: 8 } },
|
|
440
|
+
createElement('button', { onClick: startQr, disabled: busy }, t(qr === null ? 'accounts.qr.get' : 'accounts.qr.refresh'))
|
|
441
|
+
);
|
|
442
|
+
} else if (qr.status === 'confirmed') {
|
|
443
|
+
qrBlock = createElement('p', { key: 'qr-done', style: { color: '#2a6' } }, t('accounts.qr.confirmed'));
|
|
444
|
+
} else {
|
|
445
|
+
var qrChildren = [
|
|
446
|
+
createElement('img', { key: 'img', src: qr.qrDataUrl, width: 200, height: 200, alt: 'login qr', style: { display: 'block' } }),
|
|
447
|
+
createElement('p', { key: 'st', style: { marginTop: 4 } }, t('accounts.qr.' + qr.status)),
|
|
448
|
+
];
|
|
449
|
+
if (qr.status === 'need_verifycode') {
|
|
450
|
+
qrChildren.push(
|
|
451
|
+
createElement(
|
|
452
|
+
'div',
|
|
453
|
+
{ key: 'vc' },
|
|
454
|
+
createElement('input', {
|
|
455
|
+
value: qr.verifyCode || '',
|
|
456
|
+
onChange: function (event) { setQr(Object.assign({}, qr, { verifyCode: event.target.value })); },
|
|
457
|
+
}),
|
|
458
|
+
createElement(
|
|
459
|
+
'button',
|
|
460
|
+
{ style: { marginLeft: 6 }, onClick: function () { setQr(Object.assign({}, qr, { status: 'wait' })); } },
|
|
461
|
+
t('accounts.qr.code.submit')
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
qrChildren.push(
|
|
467
|
+
createElement(
|
|
468
|
+
'button',
|
|
469
|
+
{ key: 'cancel', style: { marginTop: 4 }, onClick: function () { setQr(null); } },
|
|
470
|
+
t('accounts.qr.cancel')
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
qrBlock = createElement('div', { key: 'qr-live', style: { marginTop: 8, padding: 8, border: '1px solid #ddd', borderRadius: 8 } }, qrChildren);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
353
476
|
body.push(
|
|
354
477
|
createElement(
|
|
355
478
|
'div',
|
|
@@ -393,6 +516,7 @@ window.__ModuleLoader__.load({
|
|
|
393
516
|
})
|
|
394
517
|
),
|
|
395
518
|
fieldInputs,
|
|
519
|
+
qrBlock,
|
|
396
520
|
createElement(
|
|
397
521
|
'div',
|
|
398
522
|
{ style: { marginTop: 8 } },
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-im-union",
|
|
3
3
|
"name": "dsh-im-union",
|
|
4
|
-
"version": "0.0
|
|
4
|
+
"version": "0.1.0",
|
|
5
5
|
"description": {
|
|
6
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-alpha.3).",
|
|
7
7
|
"zh": "DeepSeek Harness 的 IM 联合桥:多平台多账号机器人、每个聊天绑定独立会话,跨宿主版本兼容(0.1.2-rc.1 至 0.2.1-alpha.3)。"
|
package/lib/admin/api.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface AdminRequest {
|
|
|
22
22
|
url: URL;
|
|
23
23
|
body: unknown;
|
|
24
24
|
respond: AdminResponder;
|
|
25
|
+
/** raw 路由(第三方回调)专用:原文 body 文本 + 查询参数表 + 非 JSON 应答面。 */
|
|
26
|
+
rawBody?: string;
|
|
27
|
+
query?: Record<string, string>;
|
|
28
|
+
respondText?: (status: number, text: string) => void;
|
|
25
29
|
}
|
|
26
30
|
export type AdminHandler = (request: AdminRequest) => void | Promise<void>;
|
|
27
31
|
/** 写请求前置检查;返回错误状态码或 undefined。 */
|
|
@@ -29,9 +33,23 @@ export declare function validateMutation(req: IncomingMessage): number | undefin
|
|
|
29
33
|
export interface AdminApiHandle {
|
|
30
34
|
dispose(): void;
|
|
31
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 第三方回调路由白名单(T3.4 企微自建应用等 inbound webhook)。
|
|
38
|
+
* 口径登记:D2/D3 的 requestRejection/JSON 闸适用于**管理面**(浏览器/本机调用);
|
|
39
|
+
* 平台回调来自厂商服务器、无登录 Cookie,其认证就是签名验证(SHA1+AES,
|
|
40
|
+
* fail-closed)——因此命中的路径跳过 requestRejection 与 JSON 前置闸,
|
|
41
|
+
* 改由 handler 自身验签;仍走同一前缀注册、no-store、体积上限与错误零回显纪律。
|
|
42
|
+
*/
|
|
43
|
+
export interface RawRoute {
|
|
44
|
+
path: string;
|
|
45
|
+
/** 允许 GET 回明文(echostr 验证面原样输出,不包 JSON)。 */
|
|
46
|
+
plainText?: boolean;
|
|
47
|
+
}
|
|
32
48
|
/**
|
|
33
49
|
* 注册管理面。routes 用精确路径匹配(M0 只挂 /ping 与 /state 两个只读探针,
|
|
34
50
|
* 账号 CRUD 随 M1 渠道层落地)。webServer 缺失(非 web profile)→ 不注册,
|
|
35
51
|
* 记日志不抛错——CLI/TUI 宿主上插件其余部分照常。
|
|
36
52
|
*/
|
|
37
|
-
export declare function registerAdminApi(ctx: CordisLike, routes: Record<string, AdminHandler
|
|
53
|
+
export declare function registerAdminApi(ctx: CordisLike, routes: Record<string, AdminHandler>, opts?: {
|
|
54
|
+
rawRoutes?: RawRoute[];
|
|
55
|
+
}): AdminApiHandle;
|
package/lib/admin/api.js
CHANGED
|
@@ -43,7 +43,8 @@ export function validateMutation(req) {
|
|
|
43
43
|
* 账号 CRUD 随 M1 渠道层落地)。webServer 缺失(非 web profile)→ 不注册,
|
|
44
44
|
* 记日志不抛错——CLI/TUI 宿主上插件其余部分照常。
|
|
45
45
|
*/
|
|
46
|
-
export function registerAdminApi(ctx, routes) {
|
|
46
|
+
export function registerAdminApi(ctx, routes, opts) {
|
|
47
|
+
const rawPaths = new Map((opts?.rawRoutes ?? []).map((r) => [r.path, r]));
|
|
47
48
|
const log = ctx.logger('dsh-im-union');
|
|
48
49
|
const webServer = optionalService(ctx, 'webServer');
|
|
49
50
|
if (webServer === undefined || typeof webServer.register !== 'function') {
|
|
@@ -55,7 +56,67 @@ export function registerAdminApi(ctx, routes) {
|
|
|
55
56
|
path: BASE_PATH,
|
|
56
57
|
handler: async (req, res) => {
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
60
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
61
|
+
if (url.pathname !== BASE_PATH && !url.pathname.startsWith(BASE_PATH + '/')) {
|
|
62
|
+
send(res, 404, { ok: false, error: 'not found' });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const path = url.pathname.slice(BASE_PATH.length) || '/';
|
|
66
|
+
const handler = routes[path];
|
|
67
|
+
if (handler === undefined) {
|
|
68
|
+
send(res, 404, { ok: false, error: 'not found' });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const rawRoute = rawPaths.get(path);
|
|
72
|
+
if (rawRoute !== undefined) {
|
|
73
|
+
// 第三方回调面:验签即认证,读原文 body(≤1 MiB)后交 handler 处置。
|
|
74
|
+
let rawBody = '';
|
|
75
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
76
|
+
const chunks = [];
|
|
77
|
+
let total = 0;
|
|
78
|
+
for await (const chunk of req) {
|
|
79
|
+
const buf = chunk;
|
|
80
|
+
total += buf.length;
|
|
81
|
+
if (total > MAX_BODY_BYTES) {
|
|
82
|
+
send(res, 413, { ok: false, error: '请求体超限' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
chunks.push(buf);
|
|
86
|
+
}
|
|
87
|
+
rawBody = Buffer.concat(chunks).toString('utf8');
|
|
88
|
+
}
|
|
89
|
+
const query = {};
|
|
90
|
+
url.searchParams.forEach((value, key) => {
|
|
91
|
+
query[key] = value;
|
|
92
|
+
});
|
|
93
|
+
let settled = false;
|
|
94
|
+
await handler({
|
|
95
|
+
method,
|
|
96
|
+
path,
|
|
97
|
+
url,
|
|
98
|
+
body: query,
|
|
99
|
+
rawBody,
|
|
100
|
+
query,
|
|
101
|
+
respond: (status, payload) => {
|
|
102
|
+
settled = true;
|
|
103
|
+
send(res, status, payload);
|
|
104
|
+
},
|
|
105
|
+
respondText: rawRoute.plainText
|
|
106
|
+
? (status, text) => {
|
|
107
|
+
settled = true;
|
|
108
|
+
res.statusCode = status;
|
|
109
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
110
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
111
|
+
res.end(text);
|
|
112
|
+
}
|
|
113
|
+
: undefined,
|
|
114
|
+
});
|
|
115
|
+
if (!settled)
|
|
116
|
+
send(res, 200, { ok: true });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// 以下 requestRejection/JSON 闸只约束管理面(动态读取,不缓存认证实例)。
|
|
59
120
|
const connection = optionalService(ctx, 'connection');
|
|
60
121
|
if (typeof connection?.requestRejection !== 'function') {
|
|
61
122
|
send(res, 503, { ok: false, error: '宿主认证服务未就绪或版本不支持;请重启 DSH 或升级宿主。' });
|
|
@@ -74,19 +135,7 @@ export function registerAdminApi(ctx, routes) {
|
|
|
74
135
|
send(res, mutationError, { ok: false, error: '写请求需 application/json 与客户端标识头' });
|
|
75
136
|
return;
|
|
76
137
|
}
|
|
77
|
-
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
78
|
-
if (url.pathname !== BASE_PATH && !url.pathname.startsWith(BASE_PATH + '/')) {
|
|
79
|
-
send(res, 404, { ok: false, error: 'not found' });
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
const path = url.pathname.slice(BASE_PATH.length) || '/';
|
|
83
|
-
const handler = routes[path];
|
|
84
|
-
if (handler === undefined) {
|
|
85
|
-
send(res, 404, { ok: false, error: 'not found' });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
138
|
let body = {};
|
|
89
|
-
const method = (req.method ?? 'GET').toUpperCase();
|
|
90
139
|
if (method !== 'GET' && method !== 'HEAD') {
|
|
91
140
|
try {
|
|
92
141
|
body = await readJson(req);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { ChatAddress } from '../engine/router.js';
|
|
2
|
+
import type { ChannelAdapter, ChannelCaps, ChannelDiagnosisCheck, MessageListener, OutboundFile, Receipt, ReplyStream, SemanticMessage } from './types.js';
|
|
3
|
+
interface WSLike {
|
|
4
|
+
send(data: string): void;
|
|
5
|
+
close(): void;
|
|
6
|
+
addEventListener(type: string, cb: (event: any) => void): void;
|
|
7
|
+
readyState: number;
|
|
8
|
+
}
|
|
9
|
+
type WSFactory = (url: string) => WSLike;
|
|
10
|
+
type FetchResponseLike = {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
status: number;
|
|
13
|
+
json(): Promise<unknown>;
|
|
14
|
+
arrayBuffer?(): Promise<ArrayBuffer>;
|
|
15
|
+
};
|
|
16
|
+
type FetchLike = (url: string, init?: {
|
|
17
|
+
method?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
body?: string | Uint8Array;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}) => Promise<FetchResponseLike>;
|
|
22
|
+
export interface DingtalkAdapterOptions {
|
|
23
|
+
clientId: string;
|
|
24
|
+
clientSecret: string;
|
|
25
|
+
/** 覆盖 REST 基址(测试注入/专有云代理)。 */
|
|
26
|
+
apiBase?: string;
|
|
27
|
+
/** 覆盖 oapi 基址(素材上传面,测试注入用)。 */
|
|
28
|
+
oapiBase?: string;
|
|
29
|
+
/** AI 卡片模板 ID;缺位=不启卡片档(文本档)。 */
|
|
30
|
+
cardTemplateId?: string;
|
|
31
|
+
/** 卡片流式变量名(以模板定义为准,默认 content)。 */
|
|
32
|
+
cardTemplateKey?: string;
|
|
33
|
+
/** 流式档位:card=AI Card 优先(默认,需 cardTemplateId)/text=纯文本。 */
|
|
34
|
+
cardMode?: 'card' | 'text';
|
|
35
|
+
/** 机器人编码;文档口径企业内部应用即 AppKey(=clientId)。 */
|
|
36
|
+
robotCode?: string;
|
|
37
|
+
WebSocketImpl?: WSFactory;
|
|
38
|
+
fetchImpl?: FetchLike;
|
|
39
|
+
requestTimeoutMs?: number;
|
|
40
|
+
idleReconnectMs?: number;
|
|
41
|
+
/** 卡片流式帧间隔(默认 1100ms,避开群 webhook ~20 条/分钟量级;测试可置 0)。 */
|
|
42
|
+
streamThrottleMs?: number;
|
|
43
|
+
logger?: (msg: string) => void;
|
|
44
|
+
}
|
|
45
|
+
/** HTTP 4xx / errcode·code 明确拒绝 = 确定性失败(服务端未投递,可安全告知)。 */
|
|
46
|
+
export declare class DingTalkApiError extends Error {
|
|
47
|
+
/** 平台错误码或 HTTP 状态字符串(判型/日志用,不进用户可见文案)。 */
|
|
48
|
+
readonly code: string;
|
|
49
|
+
constructor(message: string,
|
|
50
|
+
/** 平台错误码或 HTTP 状态字符串(判型/日志用,不进用户可见文案)。 */
|
|
51
|
+
code: string);
|
|
52
|
+
}
|
|
53
|
+
/** 结果不明(网络错/超时/5xx/断连)——D7:禁自动重发。 */
|
|
54
|
+
export declare class DingTalkUncertainError extends Error {
|
|
55
|
+
}
|
|
56
|
+
export declare class DingtalkAdapter implements ChannelAdapter {
|
|
57
|
+
readonly id = "dingtalk";
|
|
58
|
+
readonly label = "\u9489\u9489";
|
|
59
|
+
readonly caps: ChannelCaps;
|
|
60
|
+
connected: boolean;
|
|
61
|
+
/** 卡片面确定性失败后置位:本实例不再尝试卡片(内部退化文本档)。 */
|
|
62
|
+
cardDisabled: boolean;
|
|
63
|
+
private readonly clientId;
|
|
64
|
+
private readonly clientSecret;
|
|
65
|
+
private readonly apiBase;
|
|
66
|
+
private readonly oapiBase;
|
|
67
|
+
private readonly cardTemplateId;
|
|
68
|
+
private readonly cardTemplateKey;
|
|
69
|
+
private readonly cardMode;
|
|
70
|
+
private readonly robotCodeConfigured;
|
|
71
|
+
private readonly wsFactory;
|
|
72
|
+
private readonly fetchImpl;
|
|
73
|
+
private readonly requestTimeoutMs;
|
|
74
|
+
private readonly idleReconnectMs;
|
|
75
|
+
private readonly streamThrottleMs;
|
|
76
|
+
private readonly log;
|
|
77
|
+
private ws;
|
|
78
|
+
private listeners;
|
|
79
|
+
private tokenCache;
|
|
80
|
+
private observedRobotCode;
|
|
81
|
+
/** msgId 排重(事件面可能重放)。 */
|
|
82
|
+
private readonly seenMsgIds;
|
|
83
|
+
/** kind:chatId → 回调自带 sessionWebhook(回复首选面,带过期时间)。 */
|
|
84
|
+
private readonly webhookByChat;
|
|
85
|
+
/** kind:chatId → 发送者 userId(batchSend 主动推送用)。 */
|
|
86
|
+
private readonly userIdByChat;
|
|
87
|
+
/** kind:chatId → conversationId(群侧推送 openConversationId 候选)。 */
|
|
88
|
+
private readonly conversationIdByChat;
|
|
89
|
+
private idleTimer;
|
|
90
|
+
private reconnectTimer;
|
|
91
|
+
private lastInboundAt;
|
|
92
|
+
private backoffMs;
|
|
93
|
+
private stopping;
|
|
94
|
+
constructor(options: DingtalkAdapterOptions);
|
|
95
|
+
/** 卡片档是否可用(未配模板/显式 text 档/已确定性失败 → 否)。 */
|
|
96
|
+
private get cardEnabled();
|
|
97
|
+
private get robotCode();
|
|
98
|
+
connect(signal?: AbortSignal): Promise<void>;
|
|
99
|
+
/** 注册 ticket→建连。ticket 90s 单次有效,故每次建连都重新注册。 */
|
|
100
|
+
private openOnce;
|
|
101
|
+
/** POST /v1.0/gateway/connections/open → {endpoint, ticket}。 */
|
|
102
|
+
private openGatewayTicket;
|
|
103
|
+
disconnect(): Promise<void>;
|
|
104
|
+
/** close 后自动重连(指数退避);主动 disconnect/拆除期不重连。 */
|
|
105
|
+
private scheduleReconnect;
|
|
106
|
+
private startIdleWatchdog;
|
|
107
|
+
private stopIdleWatchdog;
|
|
108
|
+
onMessage(listener: MessageListener): () => void;
|
|
109
|
+
private handleFrame;
|
|
110
|
+
/** 回一帧 ACK(messageId 必须与推送一致;发送失败只记日志,不改判业务)。 */
|
|
111
|
+
private ack;
|
|
112
|
+
private parseData;
|
|
113
|
+
/** 回调 payload → 上下文登记 + 排重 + 归一(导出 normalize 供单测直验)。 */
|
|
114
|
+
private handleCallback;
|
|
115
|
+
/**
|
|
116
|
+
* 机器人回调 payload → SemanticMessage。
|
|
117
|
+
* conversationType '2'→group(chatId=conversationId)/'1'→dm(chatId=senderStaffId);
|
|
118
|
+
* 机器人自发回声丢弃;非文本类型给占位文案不丢事件(与飞书同纪律)。
|
|
119
|
+
*/
|
|
120
|
+
normalize(payload: Record<string, any>): SemanticMessage | undefined;
|
|
121
|
+
/** 富文本片段 → 线性文本(图片位以占位标注,不丢事件)。 */
|
|
122
|
+
private richTextToText;
|
|
123
|
+
/**
|
|
124
|
+
* 回复:回调自带 sessionWebhook 优先(无需 token);无上下文/已过期 →
|
|
125
|
+
* 机器人主动推送面(batchSend / groupMessages.send)。
|
|
126
|
+
* markdown 优先,格式类拒绝回退纯文本(回退链同 telegram 思路)。
|
|
127
|
+
*/
|
|
128
|
+
reply(address: ChatAddress, text: string, signal?: AbortSignal): Promise<Receipt>;
|
|
129
|
+
private replyViaWebhook;
|
|
130
|
+
/** webhook POST:响应 {errcode,errmsg};errcode=0 即成功。 */
|
|
131
|
+
private postWebhook;
|
|
132
|
+
/** 主动推送面(v1.0 机器人接口,需 accessToken)。 */
|
|
133
|
+
private replyViaRobotSend;
|
|
134
|
+
private robotSend;
|
|
135
|
+
/**
|
|
136
|
+
* 出站文件(公开面两步,均已核实):
|
|
137
|
+
* 1) POST {oapiBase}/media/upload?access_token=…&type=file,multipart/form-data
|
|
138
|
+
* 字段名固定 media → { media_id }(普通文件 ≤20MB、3 天有效)
|
|
139
|
+
* 2) 机器人推送 msgKey='sampleFile',msgParam=JSON {mediaId, fileName, fileType}
|
|
140
|
+
* ——文档:机器人发送消息类型 types-of-messages-sent-by-robots
|
|
141
|
+
* 注:协议只支持企业内部应用/第三方企业应用机器人(本适配器即此档);
|
|
142
|
+
* webhook 自定义机器人无文件外发公开面。
|
|
143
|
+
* 判型沿用文本面:errcode≠0/4xx = 确定性 DingTalkApiError;超时/传输失败 = DingTalkUncertainError(D7)。
|
|
144
|
+
*/
|
|
145
|
+
sendFile(address: ChatAddress, file: OutboundFile, signal?: AbortSignal): Promise<Receipt>;
|
|
146
|
+
/** 素材上传 multipart:oapi 口径 errcode≠0 / 非 2xx = 确定性;传输失败 = uncertain(不重传)。 */
|
|
147
|
+
private uploadMedia;
|
|
148
|
+
/**
|
|
149
|
+
* 下载码 → 临时链接 → 取字节。
|
|
150
|
+
* 文档:下载机器人接收消息的文件内容
|
|
151
|
+
* https://open.dingtalk.com/document/development/download-the-file-content-of-the-robot-receiving-message
|
|
152
|
+
* 任何失败均降级:保留占位文案加说明,不抛不丢消息(入站面无 Receipt 语义)。
|
|
153
|
+
*/
|
|
154
|
+
private fillFile;
|
|
155
|
+
/**
|
|
156
|
+
* AI Card 流式:createAndDeliver 起卡 → PUT /v1.0/card/streaming 逐帧全量
|
|
157
|
+
* (isFull=true,markdown 变量硬性要求;guid 每帧唯一做幂等标志)→
|
|
158
|
+
* finish 帧 isFinalize=true(卡片状态机自动切「完成」档)。
|
|
159
|
+
*/
|
|
160
|
+
beginStream(address: ChatAddress, signal?: AbortSignal): Promise<ReplyStream>;
|
|
161
|
+
/** 文本档流式:中间快照无可编辑承载面,只缓存;收轮一次性 reply。 */
|
|
162
|
+
private textStream;
|
|
163
|
+
private createAndDeliverCard;
|
|
164
|
+
private streamUpdate;
|
|
165
|
+
/** accessToken 自管(expireIn 7200s,提前 60s 刷新;文档要求按应用维度缓存)。 */
|
|
166
|
+
private accessToken;
|
|
167
|
+
/** v1.0 接口调用:token 头 + 错误分级(4xx/code 确定性,5xx/网络 uncertain)。 */
|
|
168
|
+
private restV1;
|
|
169
|
+
/**
|
|
170
|
+
* POST JSON 底座:返回 {status, body}。
|
|
171
|
+
* 网络异常/超时/HTTP 5xx → DingTalkUncertainError(D7 禁重发);
|
|
172
|
+
* HTTP 4xx 与业务码由调用方判确定性拒绝。
|
|
173
|
+
*/
|
|
174
|
+
private send;
|
|
175
|
+
/** 渠道内不裁决准入:白名单在网关(红线 D6),恒交全局。 */
|
|
176
|
+
authorize(): boolean | undefined;
|
|
177
|
+
/** 诊断三态:credentials 实探(换 token);roundtrip 恒 unverified;零 secret 回显。 */
|
|
178
|
+
diagnose(signal?: AbortSignal): Promise<ChannelDiagnosisCheck[]>;
|
|
179
|
+
}
|
|
180
|
+
/** 工厂挂 registry(secrets 取 meta 里的钉钉字段名 clientId/clientSecret;config 可含 apiBase 覆盖)。 */
|
|
181
|
+
export declare function createDingtalkAdapter(options: {
|
|
182
|
+
accountId: string;
|
|
183
|
+
config: Record<string, string>;
|
|
184
|
+
secrets: Record<string, string | undefined>;
|
|
185
|
+
}): DingtalkAdapter;
|
|
186
|
+
export {};
|