@xmanrui/dsh-im 2.5.0 → 3.0.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/README.en.md +4 -4
- package/README.md +4 -4
- package/bin/dsh-im.mjs +1 -1
- package/lib/client.js +1410 -1409
- package/lib/index.js +206 -206
- package/package.json +2 -2
- package/plugin-src/client/channels/dingtalk/index.js +0 -17
- package/plugin-src/client/channels/dingtalk/styles.js +2 -2
- package/plugin-src/client/channels/feishu/index.js +0 -29
- package/plugin-src/client/channels/feishu/styles.js +2 -2
- package/plugin-src/client/channels/qq/index.js +0 -9
- package/plugin-src/client/channels/wecom/index.js +0 -9
- package/plugin-src/client/channels/weixin/index.js +0 -19
- package/plugin-src/client/channels/weixin/styles.js +2 -2
- package/plugin-src/client/index.js +4 -4
- package/scripts/verify-package.mjs +48 -11
- package/src/channels/dingtalk/dingtalk-api.mjs +101 -5
- package/src/channels/dingtalk/dingtalk-bridge.mjs +198 -22
- package/src/channels/discord/discord-api.mjs +35 -0
- package/src/channels/discord/discord-bridge.mjs +1 -0
- package/src/channels/discord/discord-runtime.mjs +26 -0
- package/src/channels/feishu/bridge.mjs +21 -28
- package/src/channels/feishu/message-utils.mjs +1 -1
- package/src/channels/shared/connection-test.mjs +1 -1
- package/src/channels/shared/i18n-en/feishu.mjs +6 -6
- package/src/channels/shared/i18n-en/shared-c.mjs +2 -2
- package/src/channels/shared/status-reaction.mjs +107 -0
- package/src/channels/shared/text-harness-bridge.mjs +58 -6
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +28 -0
- package/src/channels/slack/slack-bridge.mjs +5 -0
- package/src/channels/slack/slack-runtime.mjs +24 -0
- package/src/channels/telegram/telegram-api.mjs +9 -0
- package/src/channels/telegram/telegram-bridge.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +20 -0
- package/src/channels/whatsapp/whatsapp-bridge.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "node plugin-src/client/build.mjs && node plugin-src/host/build.mjs",
|
|
85
85
|
"test": "node --test test/*.test.mjs test/channels/*/*.test.mjs",
|
|
86
|
-
"check": "npm
|
|
86
|
+
"check": "npm run build && npm test && node scripts/verify-package.mjs"
|
|
87
87
|
},
|
|
88
88
|
"engines": {
|
|
89
89
|
"node": ">=22.19"
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
} from '../../channel-card-meta.js';
|
|
17
17
|
import {
|
|
18
18
|
DINGTALK_ENDPOINTS,
|
|
19
|
-
DINGTALK_RPC_CHANNEL,
|
|
20
19
|
connectionTestFeedback,
|
|
21
20
|
formatRemaining,
|
|
22
21
|
normalizeProvisioning,
|
|
@@ -29,9 +28,6 @@ import { installDingtalkStyles } from './styles.js';
|
|
|
29
28
|
|
|
30
29
|
const ACTIVE_PROVISION_STATES = new Set(['pending', 'scanned', 'authorizing', 'creating', 'connecting']);
|
|
31
30
|
|
|
32
|
-
export const name = 'dingtalk-settings';
|
|
33
|
-
export const inject = ['slots', 'connection'];
|
|
34
|
-
|
|
35
31
|
function DingtalkIcon({ size = 28 }) {
|
|
36
32
|
return h('svg', {
|
|
37
33
|
width: size,
|
|
@@ -864,16 +860,3 @@ export function DingtalkSettingsTab({ rpcCall }) {
|
|
|
864
860
|
})
|
|
865
861
|
: null)));
|
|
866
862
|
}
|
|
867
|
-
|
|
868
|
-
export function apply(ctx) {
|
|
869
|
-
ctx.effect(() => installDingtalkStyles(), 'dingtalk-settings: install client styles');
|
|
870
|
-
const rpcCall = (endpoint, payload, signal) =>
|
|
871
|
-
ctx.connection.rpc.call(DINGTALK_RPC_CHANNEL, endpoint, payload, signal);
|
|
872
|
-
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
873
|
-
name: 'settings.plugins.tab',
|
|
874
|
-
id: 'dingtalk',
|
|
875
|
-
order: 40,
|
|
876
|
-
label: '钉钉',
|
|
877
|
-
inject: () => ({ rpcCall }),
|
|
878
|
-
}, DingtalkSettingsTab));
|
|
879
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DINGTALK_STYLE_ID = 'xmanrui-dsh-dingtalk-settings';
|
|
1
|
+
export const DINGTALK_STYLE_ID = 'xmanrui-dsh-im-dingtalk-settings';
|
|
2
2
|
|
|
3
3
|
const CSS = String.raw`
|
|
4
4
|
.ddt-page {
|
|
@@ -126,7 +126,7 @@ export function installDingtalkStyles() {
|
|
|
126
126
|
const existing = document.querySelector(`style[data-plugin-css="${DINGTALK_STYLE_ID}"]`);
|
|
127
127
|
if (existing) return () => {};
|
|
128
128
|
const style = document.createElement('style');
|
|
129
|
-
style.dataset.plugin = '@xmanrui/dsh-
|
|
129
|
+
style.dataset.plugin = '@xmanrui/dsh-im';
|
|
130
130
|
style.dataset.pluginCss = DINGTALK_STYLE_ID;
|
|
131
131
|
style.textContent = CSS;
|
|
132
132
|
document.head.appendChild(style);
|
|
@@ -6,7 +6,6 @@ import { h } from "../../i18n.js";
|
|
|
6
6
|
import {
|
|
7
7
|
FEISHU_ENDPOINTS,
|
|
8
8
|
FEISHU_REGISTRATION_OPERATIONS,
|
|
9
|
-
FEISHU_RPC_CHANNEL,
|
|
10
9
|
formatRemaining,
|
|
11
10
|
normalizeBotsSnapshot,
|
|
12
11
|
normalizeGroupResponseMode,
|
|
@@ -28,11 +27,6 @@ import {
|
|
|
28
27
|
ChannelListHeading,
|
|
29
28
|
LastMessageErrorSummary,
|
|
30
29
|
} from "../../channel-card-meta.js";
|
|
31
|
-
import { installFeishuStyles } from "./styles.js";
|
|
32
|
-
|
|
33
|
-
export const name = "feishu-settings";
|
|
34
|
-
export const inject = ["slots", "connection"];
|
|
35
|
-
|
|
36
30
|
const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
|
37
31
|
const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
|
38
32
|
|
|
@@ -1540,26 +1534,3 @@ export function FeishuSettingsTab({ rpcCall }) {
|
|
|
1540
1534
|
),
|
|
1541
1535
|
));
|
|
1542
1536
|
}
|
|
1543
|
-
|
|
1544
|
-
export function apply(ctx) {
|
|
1545
|
-
ctx.effect(
|
|
1546
|
-
() => installFeishuStyles(),
|
|
1547
|
-
"feishu-settings: install client styles",
|
|
1548
|
-
);
|
|
1549
|
-
|
|
1550
|
-
const rpcCall = (endpoint, payload, signal) =>
|
|
1551
|
-
ctx.connection.rpc.call(FEISHU_RPC_CHANNEL, endpoint, payload, signal);
|
|
1552
|
-
|
|
1553
|
-
ctx.slots.inject("settings.plugins.tab", () =>
|
|
1554
|
-
ctx.slots.register(
|
|
1555
|
-
{
|
|
1556
|
-
name: "settings.plugins.tab",
|
|
1557
|
-
id: "feishu",
|
|
1558
|
-
order: 20,
|
|
1559
|
-
label: "飞书",
|
|
1560
|
-
inject: () => ({ rpcCall }),
|
|
1561
|
-
},
|
|
1562
|
-
FeishuSettingsTab,
|
|
1563
|
-
),
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const FEISHU_STYLE_ID = "
|
|
1
|
+
export const FEISHU_STYLE_ID = "xmanrui-dsh-im-feishu-settings";
|
|
2
2
|
|
|
3
3
|
const CSS = String.raw`
|
|
4
4
|
.bxf-page {
|
|
@@ -562,7 +562,7 @@ export function installFeishuStyles() {
|
|
|
562
562
|
}
|
|
563
563
|
|
|
564
564
|
const style = document.createElement("style");
|
|
565
|
-
style.dataset.plugin = "@xmanrui/dsh-
|
|
565
|
+
style.dataset.plugin = "@xmanrui/dsh-im";
|
|
566
566
|
style.dataset.pluginCss = FEISHU_STYLE_ID;
|
|
567
567
|
style.textContent = CSS;
|
|
568
568
|
document.head.appendChild(style);
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
19
19
|
import {
|
|
20
20
|
QQ_ENDPOINTS,
|
|
21
|
-
QQ_RPC_CHANNEL,
|
|
22
21
|
connectionTestFeedback,
|
|
23
22
|
formatRemaining,
|
|
24
23
|
normalizeProvisioning,
|
|
@@ -514,11 +513,3 @@ export function QqSettingsTab({ rpcCall }) {
|
|
|
514
513
|
? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
|
515
514
|
botList)));
|
|
516
515
|
}
|
|
517
|
-
|
|
518
|
-
export function apply(ctx) {
|
|
519
|
-
ctx.effect(() => installQqStyles(), 'qq-settings: install client styles');
|
|
520
|
-
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(QQ_RPC_CHANNEL, endpoint, payload, signal);
|
|
521
|
-
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
522
|
-
name: 'settings.plugins.tab', id: 'qq', order: 50, label: 'QQ', inject: () => ({ rpcCall }),
|
|
523
|
-
}, QqSettingsTab));
|
|
524
|
-
}
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import { installDingtalkStyles } from '../dingtalk/styles.js';
|
|
19
19
|
import {
|
|
20
20
|
WECOM_ENDPOINTS,
|
|
21
|
-
WECOM_RPC_CHANNEL,
|
|
22
21
|
formatRemaining,
|
|
23
22
|
normalizeProvisioning,
|
|
24
23
|
normalizeSnapshot,
|
|
@@ -546,11 +545,3 @@ export function WecomSettingsTab({ rpcCall }) {
|
|
|
546
545
|
? h(EmptyView, { busy, onStart: () => void startProvisioning() }) : null,
|
|
547
546
|
botList)));
|
|
548
547
|
}
|
|
549
|
-
|
|
550
|
-
export function apply(ctx) {
|
|
551
|
-
ctx.effect(() => installWecomStyles(), 'wecom-settings: install client styles');
|
|
552
|
-
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(WECOM_RPC_CHANNEL, endpoint, payload, signal);
|
|
553
|
-
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
554
|
-
name: 'settings.plugins.tab', id: 'wecom', order: 45, label: '企业微信', inject: () => ({ rpcCall }),
|
|
555
|
-
}, WecomSettingsTab));
|
|
556
|
-
}
|
|
@@ -5,7 +5,6 @@ import { QrActionIcon } from '../../credential-binding.js';
|
|
|
5
5
|
import { h } from '../../i18n.js';
|
|
6
6
|
import {
|
|
7
7
|
WEIXIN_ENDPOINTS,
|
|
8
|
-
WEIXIN_RPC_CHANNEL,
|
|
9
8
|
formatRemaining,
|
|
10
9
|
normalizeProvisioning,
|
|
11
10
|
normalizeSnapshot,
|
|
@@ -27,11 +26,6 @@ import {
|
|
|
27
26
|
ChannelListHeading,
|
|
28
27
|
LastMessageErrorSummary,
|
|
29
28
|
} from '../../channel-card-meta.js';
|
|
30
|
-
import { installWeixinStyles } from './styles.js';
|
|
31
|
-
|
|
32
|
-
export const name = 'weixin-settings';
|
|
33
|
-
export const inject = ['slots', 'connection'];
|
|
34
|
-
|
|
35
29
|
const Button = React.forwardRef(function Button(
|
|
36
30
|
{ children, kind = 'secondary', className = '', ...props },
|
|
37
31
|
ref,
|
|
@@ -717,16 +711,3 @@ export function WeixinSettingsTab({ rpcCall }) {
|
|
|
717
711
|
: null),
|
|
718
712
|
));
|
|
719
713
|
}
|
|
720
|
-
|
|
721
|
-
export function apply(ctx) {
|
|
722
|
-
ctx.effect(() => installWeixinStyles(), 'weixin-settings: install client styles');
|
|
723
|
-
const rpcCall = (endpoint, payload, signal) =>
|
|
724
|
-
ctx.connection.rpc.call(WEIXIN_RPC_CHANNEL, endpoint, payload, signal);
|
|
725
|
-
ctx.slots.inject('settings.plugins.tab', () => ctx.slots.register({
|
|
726
|
-
name: 'settings.plugins.tab',
|
|
727
|
-
id: 'weixin',
|
|
728
|
-
order: 30,
|
|
729
|
-
label: '微信',
|
|
730
|
-
inject: () => ({ rpcCall }),
|
|
731
|
-
}, WeixinSettingsTab));
|
|
732
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const WEIXIN_STYLE_ID = 'xmanrui-dsh-weixin-settings';
|
|
1
|
+
export const WEIXIN_STYLE_ID = 'xmanrui-dsh-im-weixin-settings';
|
|
2
2
|
|
|
3
3
|
const CSS = String.raw`
|
|
4
4
|
.dxw-page {
|
|
@@ -107,7 +107,7 @@ export function installWeixinStyles() {
|
|
|
107
107
|
const existing = document.querySelector(`style[data-plugin-css="${WEIXIN_STYLE_ID}"]`);
|
|
108
108
|
if (existing) return () => {};
|
|
109
109
|
const style = document.createElement('style');
|
|
110
|
-
style.dataset.plugin = '@xmanrui/dsh-
|
|
110
|
+
style.dataset.plugin = '@xmanrui/dsh-im';
|
|
111
111
|
style.dataset.pluginCss = WEIXIN_STYLE_ID;
|
|
112
112
|
style.textContent = CSS;
|
|
113
113
|
document.head.appendChild(style);
|
|
@@ -333,10 +333,10 @@ export function apply(ctx) {
|
|
|
333
333
|
pickDirectory: () => ctx.workspaces.pickDirectory(),
|
|
334
334
|
});
|
|
335
335
|
|
|
336
|
-
ctx.slots.inject('settings.
|
|
337
|
-
name: 'settings.
|
|
338
|
-
id: 'im',
|
|
339
|
-
order:
|
|
336
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
337
|
+
name: 'settings.section',
|
|
338
|
+
id: 'xmanrui-dsh-im',
|
|
339
|
+
order: 21,
|
|
340
340
|
label: () => t('IM机器人'),
|
|
341
341
|
locale: IM_LOCALE_NAMESPACE,
|
|
342
342
|
inject: () => ({
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { access, readFile, stat } from 'node:fs/promises';
|
|
1
|
+
import { access, readFile, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
5
|
const root = resolve(import.meta.dirname, '..');
|
|
6
|
+
|
|
7
|
+
async function readSourceTree(directory) {
|
|
8
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
9
|
+
const chunks = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
const path = resolve(directory, entry.name);
|
|
12
|
+
if (entry.isDirectory()) {
|
|
13
|
+
chunks.push(await readSourceTree(path));
|
|
14
|
+
} else if (entry.isFile() && /\.m?js$/u.test(entry.name)) {
|
|
15
|
+
chunks.push(await readFile(path, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return chunks.join('\n');
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
const required = [
|
|
7
22
|
'lib/index.js',
|
|
8
23
|
'lib/client.js',
|
|
@@ -35,7 +50,17 @@ const required = [
|
|
|
35
50
|
];
|
|
36
51
|
await Promise.all(required.map((path) => access(resolve(root, path))));
|
|
37
52
|
|
|
38
|
-
const [
|
|
53
|
+
const [
|
|
54
|
+
client,
|
|
55
|
+
host,
|
|
56
|
+
patch,
|
|
57
|
+
manifestText,
|
|
58
|
+
lockText,
|
|
59
|
+
hostSource,
|
|
60
|
+
clientEntrySource,
|
|
61
|
+
clientSources,
|
|
62
|
+
executable,
|
|
63
|
+
] = await Promise.all([
|
|
39
64
|
readFile(resolve(root, 'lib/client.js'), 'utf8'),
|
|
40
65
|
readFile(resolve(root, 'lib/index.js'), 'utf8'),
|
|
41
66
|
readFile(resolve(root, 'cordis.patch.yml'), 'utf8'),
|
|
@@ -43,6 +68,7 @@ const [client, host, patch, manifestText, lockText, hostSource, clientSource, ex
|
|
|
43
68
|
readFile(resolve(root, 'package-lock.json'), 'utf8'),
|
|
44
69
|
readFile(resolve(root, 'plugin-src/host/index.mjs'), 'utf8'),
|
|
45
70
|
readFile(resolve(root, 'plugin-src/client/index.js'), 'utf8'),
|
|
71
|
+
readSourceTree(resolve(root, 'plugin-src/client')),
|
|
46
72
|
stat(resolve(root, 'bin/dsh-im.mjs')),
|
|
47
73
|
]);
|
|
48
74
|
const manifest = JSON.parse(manifestText);
|
|
@@ -82,17 +108,28 @@ if (forbiddenDshLockPaths.length > 0) {
|
|
|
82
108
|
);
|
|
83
109
|
}
|
|
84
110
|
|
|
85
|
-
if (
|
|
111
|
+
if (!/\bid\s*:\s*["']@xmanrui\/dsh-im["']/u.test(client)) {
|
|
86
112
|
throw new Error('client bundle does not register the dsh-im loader id');
|
|
87
113
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
const sourceSectionMarkers = [
|
|
115
|
+
/ctx\.slots\.inject\(\s*["']settings\.section["']/u,
|
|
116
|
+
/name\s*:\s*["']settings\.section["']/u,
|
|
117
|
+
/id\s*:\s*["']xmanrui-dsh-im["']/u,
|
|
118
|
+
/order\s*:\s*21\b/u,
|
|
119
|
+
/label\s*:\s*\(\)\s*=>\s*t\(\s*["']IM机器人["']\s*\)/u,
|
|
120
|
+
/locale\s*:\s*IM_LOCALE_NAMESPACE\b/u,
|
|
121
|
+
];
|
|
122
|
+
const bundleSectionPattern = /name\s*:\s*["']settings\.section["']\s*,\s*id\s*:\s*["']xmanrui-dsh-im["']\s*,\s*order\s*:\s*21\s*,\s*label\s*:\s*\(\)\s*=>\s*[$A-Z_a-z][$\w]*\(\s*["']IM(?:机器人|\\u673A\\u5668\\u4EBA)["']\s*\)\s*,\s*locale\s*:\s*(?:[$A-Z_a-z][$\w]*|["']dsh-im["'])/u;
|
|
123
|
+
if (sourceSectionMarkers.some((pattern) => !pattern.test(clientEntrySource))
|
|
124
|
+
|| !/IM_LOCALE_NAMESPACE\s*=\s*["']dsh-im["']/u.test(clientSources)
|
|
125
|
+
|| !bundleSectionPattern.test(client)) {
|
|
126
|
+
throw new Error('client bundle does not register the localized top-level IM settings section');
|
|
127
|
+
}
|
|
128
|
+
if ((client.match(/\.slots\.inject\(\s*["']settings\.section["']/gu) ?? []).length !== 1) {
|
|
129
|
+
throw new Error('client bundle must register exactly one top-level settings section');
|
|
93
130
|
}
|
|
94
|
-
if (
|
|
95
|
-
throw new Error('client bundle
|
|
131
|
+
if (client.includes('settings.plugins.tab') || clientSources.includes('settings.plugins.tab')) {
|
|
132
|
+
throw new Error('client source or bundle still contains the legacy Plugins-tab settings entry');
|
|
96
133
|
}
|
|
97
134
|
if (/role:\s*["']switch|type:\s*["']checkbox/.test(client)) {
|
|
98
135
|
throw new Error('client bundle contains a channel enable switch');
|
|
@@ -115,7 +152,7 @@ if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(host)) {
|
|
|
115
152
|
throw new Error('host bundle still imports an external channel plugin');
|
|
116
153
|
}
|
|
117
154
|
if (/@xmanrui\/dsh-(?:feishu|weixin|dingtalk)/.test(
|
|
118
|
-
manifestText + lockText + hostSource +
|
|
155
|
+
manifestText + lockText + hostSource + clientSources,
|
|
119
156
|
)) {
|
|
120
157
|
throw new Error('source or package metadata still depends on an external channel plugin');
|
|
121
158
|
}
|
|
@@ -8,8 +8,14 @@ export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
|
|
|
8
8
|
export const DINGTALK_API_BASE_URL = 'https://api.dingtalk.com/';
|
|
9
9
|
export const DINGTALK_REGISTRATION_SOURCE = 'DING_DWS_CLAW';
|
|
10
10
|
export const DINGTALK_AI_CARD_TEMPLATE_ID = '02fcf2f4-5e02-4a85-b672-46d1f715543e.schema';
|
|
11
|
+
export const DINGTALK_THINKING_REACTION_NAME = '🤔思考中';
|
|
12
|
+
export const DINGTALK_DONE_REACTION_NAME = '✅已完成';
|
|
13
|
+
export const DINGTALK_ERROR_REACTION_NAME = '❌处理失败';
|
|
11
14
|
|
|
12
15
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
16
|
+
const REACTION_TIMEOUT_MS = 5_000;
|
|
17
|
+
const TEXT_REACTION_ID = '2659900';
|
|
18
|
+
const TEXT_REACTION_BACKGROUND_ID = 'im_bg_1';
|
|
13
19
|
const REGISTRATION_STATUSES = new Set(['WAITING', 'SUCCESS', 'FAIL', 'EXPIRED']);
|
|
14
20
|
|
|
15
21
|
export class DingtalkApiError extends Error {
|
|
@@ -406,18 +412,20 @@ export function createDingtalkApi({
|
|
|
406
412
|
if (!source) throw new TypeError('registrationSource is required');
|
|
407
413
|
const tokenCache = new Map();
|
|
408
414
|
const tokenRequests = new Map();
|
|
415
|
+
const reactionTokenRequests = new Map();
|
|
409
416
|
let cardSlotTail = Promise.resolve();
|
|
410
417
|
let nextCardRequestAt = 0;
|
|
411
418
|
|
|
412
419
|
const endpoint = (base, pathname) => new URL(pathname.replace(/^\//, ''), base);
|
|
413
420
|
|
|
414
|
-
async function accessToken({ clientId, clientSecret, signal }) {
|
|
421
|
+
async function accessToken({ clientId, clientSecret, signal, requestKind = 'normal' }) {
|
|
415
422
|
const appKey = nonEmptyString(clientId);
|
|
416
423
|
const appSecret = nonEmptyString(clientSecret);
|
|
417
424
|
if (!appKey || !appSecret) throw new TypeError('clientId and clientSecret are required');
|
|
418
425
|
const cached = tokenCache.get(appKey);
|
|
419
426
|
if (cached && cached.expiresAt > now()) return cached.token;
|
|
420
|
-
|
|
427
|
+
const requests = requestKind === 'reaction' ? reactionTokenRequests : tokenRequests;
|
|
428
|
+
if (requests.has(appKey)) return requests.get(appKey);
|
|
421
429
|
|
|
422
430
|
const request = (async () => {
|
|
423
431
|
const value = await requestJson(fetchImpl, endpoint(apiBase, 'v1.0/oauth2/accessToken'), {
|
|
@@ -431,9 +439,75 @@ export function createDingtalkApi({
|
|
|
431
439
|
const refreshAfterMs = Math.max(1_000, (expiresInSeconds - 60) * 1_000);
|
|
432
440
|
tokenCache.set(appKey, { token, expiresAt: now() + refreshAfterMs });
|
|
433
441
|
return token;
|
|
434
|
-
})()
|
|
435
|
-
|
|
436
|
-
|
|
442
|
+
})();
|
|
443
|
+
const shared = request.finally(() => requests.delete(appKey));
|
|
444
|
+
requests.set(appKey, shared);
|
|
445
|
+
return shared;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function changeReaction({
|
|
449
|
+
clientId,
|
|
450
|
+
clientSecret,
|
|
451
|
+
robotCode,
|
|
452
|
+
messageId,
|
|
453
|
+
conversationId,
|
|
454
|
+
reactionName = DINGTALK_THINKING_REACTION_NAME,
|
|
455
|
+
signal,
|
|
456
|
+
}, action) {
|
|
457
|
+
const appKey = nonEmptyString(clientId);
|
|
458
|
+
const appSecret = nonEmptyString(clientSecret);
|
|
459
|
+
const botCode = nonEmptyString(robotCode) ?? appKey;
|
|
460
|
+
const openMsgId = nonEmptyString(messageId);
|
|
461
|
+
const openConversationId = nonEmptyString(conversationId);
|
|
462
|
+
const emotionName = nonEmptyString(reactionName);
|
|
463
|
+
if (!appKey || !appSecret) throw new TypeError('clientId and clientSecret are required');
|
|
464
|
+
if (!botCode) throw new TypeError('robotCode is required');
|
|
465
|
+
if (!openMsgId || !openConversationId) {
|
|
466
|
+
throw new TypeError('messageId and conversationId are required');
|
|
467
|
+
}
|
|
468
|
+
if (!emotionName) throw new TypeError('reactionName is required');
|
|
469
|
+
// Reactions deduplicate with each other but never own the normal reply's
|
|
470
|
+
// in-flight token request. Both paths may still reuse a cached token.
|
|
471
|
+
const token = await accessToken({
|
|
472
|
+
clientId: appKey,
|
|
473
|
+
clientSecret: appSecret,
|
|
474
|
+
signal,
|
|
475
|
+
requestKind: 'reaction',
|
|
476
|
+
});
|
|
477
|
+
const response = await requestJson(
|
|
478
|
+
fetchImpl,
|
|
479
|
+
endpoint(apiBase, `v1.0/robot/emotion/${action}`),
|
|
480
|
+
{
|
|
481
|
+
body: {
|
|
482
|
+
robotCode: botCode,
|
|
483
|
+
openMsgId,
|
|
484
|
+
openConversationId,
|
|
485
|
+
emotionType: 2,
|
|
486
|
+
emotionName,
|
|
487
|
+
textEmotion: {
|
|
488
|
+
emotionId: TEXT_REACTION_ID,
|
|
489
|
+
emotionName,
|
|
490
|
+
text: emotionName,
|
|
491
|
+
backgroundId: TEXT_REACTION_BACKGROUND_ID,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
495
|
+
signal,
|
|
496
|
+
timeoutMs: REACTION_TIMEOUT_MS,
|
|
497
|
+
action: action === 'reply' ? '消息状态添加' : '消息状态撤回',
|
|
498
|
+
},
|
|
499
|
+
);
|
|
500
|
+
const rejection = response?.success === false
|
|
501
|
+
? safeProviderCode(response?.code ?? response?.errcode) ?? 'rejected'
|
|
502
|
+
: rejectedProviderResponse(response);
|
|
503
|
+
if (rejection) {
|
|
504
|
+
throw new DingtalkApiError(
|
|
505
|
+
'reaction-rejected',
|
|
506
|
+
'钉钉服务拒绝了消息状态请求。',
|
|
507
|
+
{ providerCode: rejection },
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
437
511
|
}
|
|
438
512
|
|
|
439
513
|
async function messageFileDownloadUrl({
|
|
@@ -690,6 +764,28 @@ export function createDingtalkApi({
|
|
|
690
764
|
|
|
691
765
|
accessToken,
|
|
692
766
|
|
|
767
|
+
async addReaction(request) {
|
|
768
|
+
return changeReaction(request, 'reply');
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
async recallReaction(request) {
|
|
772
|
+
return changeReaction(request, 'recall');
|
|
773
|
+
},
|
|
774
|
+
|
|
775
|
+
async addThinkingReaction(request) {
|
|
776
|
+
return changeReaction({
|
|
777
|
+
...request,
|
|
778
|
+
reactionName: DINGTALK_THINKING_REACTION_NAME,
|
|
779
|
+
}, 'reply');
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
async recallThinkingReaction(request) {
|
|
783
|
+
return changeReaction({
|
|
784
|
+
...request,
|
|
785
|
+
reactionName: DINGTALK_THINKING_REACTION_NAME,
|
|
786
|
+
}, 'recall');
|
|
787
|
+
},
|
|
788
|
+
|
|
693
789
|
async downloadImage({
|
|
694
790
|
clientId,
|
|
695
791
|
clientSecret,
|