@xmanrui/dsh-im 0.1.0 → 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.
- package/README.md +32 -12
- package/THIRD_PARTY_NOTICES.md +35 -3
- package/lib/client.js +772 -68
- package/lib/index.js +255 -13339
- package/package.json +5 -4
- package/plugin-src/client/channel-logos.js +13 -0
- package/plugin-src/client/channels/shared/token-channel.js +32 -15
- package/plugin-src/client/channels/whatsapp/api.js +123 -0
- package/plugin-src/client/channels/whatsapp/index.js +433 -0
- package/plugin-src/client/channels/whatsapp/styles.js +19 -0
- package/plugin-src/client/index.js +20 -2
- package/plugin-src/client/styles.js +3 -1
- package/plugin-src/host/build.mjs +22 -2
- package/plugin-src/host/channels/whatsapp/index.mjs +35 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +121 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +140 -0
- package/plugin-src/host/index.mjs +3 -0
- package/scripts/verify-package.mjs +28 -2
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/whatsapp/config-store.mjs +165 -0
- package/src/channels/whatsapp/harness-client.mjs +3 -0
- package/src/channels/whatsapp/state-store.mjs +3 -0
- package/src/channels/whatsapp/whatsapp-bridge.mjs +15 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +388 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +299 -0
- package/src/channels/whatsapp/whatsapp-web-session.mjs +212 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "通过扫码或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Telegram和
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "通过扫码或机器人凭据把IM机器人接入DeepSeek Harness(支持飞书、微信、钉钉、企业微信、QQ、Telegram、Discord和WhatsApp)。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"dsh-im": "
|
|
17
|
+
"dsh-im": "bin/dsh-im.mjs"
|
|
18
18
|
},
|
|
19
19
|
"main": "./lib/index.js",
|
|
20
20
|
"exports": {
|
|
@@ -57,7 +57,6 @@
|
|
|
57
57
|
"node": ">=22.19"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@larksuiteoapi/node-sdk": "1.73.0",
|
|
61
60
|
"@tencent-connect/qqbot-connector": "1.2.0",
|
|
62
61
|
"@tencent-connect/qqbot-nodejs": "1.0.4",
|
|
63
62
|
"@wecom/aibot-node-sdk": "1.0.7",
|
|
@@ -65,6 +64,8 @@
|
|
|
65
64
|
"qrcode": "1.5.4"
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
67
|
+
"@larksuiteoapi/node-sdk": "1.73.0",
|
|
68
|
+
"@whiskeysockets/baileys": "7.0.0-rc14",
|
|
68
69
|
"esbuild": "0.25.9",
|
|
69
70
|
"react": "18.3.1",
|
|
70
71
|
"react-dom": "18.3.1",
|
|
@@ -105,3 +105,16 @@ export function DiscordLogoGlyph({ size } = {}) {
|
|
|
105
105
|
d: 'M20.32 4.37a19.8 19.8 0 0 0-4.89-1.51c-.21.38-.46.89-.63 1.29a18.4 18.4 0 0 0-5.59 0 13 13 0 0 0-.64-1.29c-1.71.29-3.36.8-4.89 1.52C.59 9.09-.25 13.68.17 18.2a19.9 19.9 0 0 0 6 3.04c.48-.66.91-1.36 1.28-2.1-.7-.26-1.37-.58-2-.96.17-.12.33-.25.49-.38 3.86 1.79 8.04 1.79 11.86 0 .16.13.32.26.49.38-.64.38-1.31.7-2.01.97.37.73.8 1.44 1.28 2.09a19.8 19.8 0 0 0 6-3.04c.49-5.24-.84-9.79-3.24-13.83ZM8.02 15.42c-1.16 0-2.11-1.07-2.11-2.38s.93-2.38 2.11-2.38c1.18 0 2.13 1.08 2.11 2.38 0 1.31-.93 2.38-2.11 2.38Zm7.95 0c-1.16 0-2.11-1.07-2.11-2.38s.93-2.38 2.11-2.38c1.18 0 2.13 1.08 2.11 2.38 0 1.31-.93 2.38-2.11 2.38Z',
|
|
106
106
|
}));
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
export function WhatsappLogoGlyph({ size } = {}) {
|
|
110
|
+
return h('svg', {
|
|
111
|
+
...dimensions(size),
|
|
112
|
+
viewBox: '0 0 24 24',
|
|
113
|
+
focusable: 'false',
|
|
114
|
+
'aria-hidden': 'true',
|
|
115
|
+
'data-im-channel-logo': 'whatsapp',
|
|
116
|
+
}, h('path', {
|
|
117
|
+
fill: 'currentColor',
|
|
118
|
+
d: 'M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.521.149-.173.198-.297.298-.495.099-.198.05-.372-.025-.521-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51a12.8 12.8 0 0 0-.57-.01c-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479s1.065 2.875 1.213 3.074c.149.198 2.095 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.29.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.981.999-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.83 9.83 0 0 1 2.893 6.991c-.003 5.45-4.437 9.884-9.886 9.884m8.413-18.297A11.8 11.8 0 0 0 12.055 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.9 11.9 0 0 0 5.688 1.448h.005c6.557 0 11.892-5.335 11.895-11.893a11.82 11.82 0 0 0-3.486-8.413',
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
@@ -43,6 +43,13 @@ export function createTokenChannelSettings(definition) {
|
|
|
43
43
|
emptyTitle,
|
|
44
44
|
emptyDescription,
|
|
45
45
|
platformLabel,
|
|
46
|
+
CredentialPanel = null,
|
|
47
|
+
credentialPayload = ({ secret }) => ({ token: secret }),
|
|
48
|
+
credentialAriaLabel = `使用 Bot Token 接入 ${channel} 机器人`,
|
|
49
|
+
credentialOpenLabel = '手动接入',
|
|
50
|
+
credentialCloseLabel = '收起凭据',
|
|
51
|
+
credentialNoun = 'Bot Token',
|
|
52
|
+
emptyActionLabel = '填写 Bot Token',
|
|
46
53
|
} = definition;
|
|
47
54
|
|
|
48
55
|
function AccountCard({ account, busy, removing, onReconnect, onRequestRemove, onConfirmRemove, onCancelRemove }) {
|
|
@@ -83,7 +90,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
83
90
|
}, '移除接入')))),
|
|
84
91
|
removing ? h('div', { className: 'ddt-confirm dim-confirm', role: 'alertdialog' },
|
|
85
92
|
h('strong', null, `从 DeepSeek Harness 移除“${account.bot.name}”?`),
|
|
86
|
-
h('p', null, `这会停止消息连接,并删除本机保存的
|
|
93
|
+
h('p', null, `这会停止消息连接,并删除本机保存的 ${credentialNoun}、机器人配置及会话映射。${platformLabel}中的机器人不会被自动删除。`),
|
|
87
94
|
h('div', { className: 'ddt-actions dim-viewActions' },
|
|
88
95
|
h(Button, { onClick: onCancelRemove, disabled: Boolean(busy) }, '保留机器人'),
|
|
89
96
|
h(Button, { kind: 'danger', onClick: onConfirmRemove, disabled: Boolean(busy) },
|
|
@@ -153,11 +160,14 @@ export function createTokenChannelSettings(definition) {
|
|
|
153
160
|
};
|
|
154
161
|
}, [loadStatus, model.phase]);
|
|
155
162
|
|
|
156
|
-
const bindCredentials = React.useCallback(async (
|
|
163
|
+
const bindCredentials = React.useCallback(async (values) => {
|
|
157
164
|
setBusy(true);
|
|
158
165
|
setCredentialError(null);
|
|
159
166
|
try {
|
|
160
|
-
const snapshot = api.normalizeSnapshot(await invoke(
|
|
167
|
+
const snapshot = api.normalizeSnapshot(await invoke(
|
|
168
|
+
endpoints.bindCredentials,
|
|
169
|
+
credentialPayload(values),
|
|
170
|
+
));
|
|
161
171
|
if (!mounted.current) return;
|
|
162
172
|
setModel({ phase: 'ready', bots: snapshot.bots, totals: snapshot.totals, error: null });
|
|
163
173
|
setCredentialOpen(false);
|
|
@@ -224,8 +234,8 @@ export function createTokenChannelSettings(definition) {
|
|
|
224
234
|
onClick: () => { setCredentialOpen((value) => !value); setCredentialError(null); },
|
|
225
235
|
disabled: busy,
|
|
226
236
|
'aria-pressed': credentialOpen,
|
|
227
|
-
'aria-label':
|
|
228
|
-
}, h(CredentialActionIcon), credentialOpen ?
|
|
237
|
+
'aria-label': credentialAriaLabel,
|
|
238
|
+
}, h(CredentialActionIcon), credentialOpen ? credentialCloseLabel : credentialOpenLabel)),
|
|
229
239
|
model.totals.configured > 0
|
|
230
240
|
? h('div', { className: 'ddt-badge dim-onlineBadge' },
|
|
231
241
|
h('span', null, `${model.totals.connected} / ${model.totals.configured} 在线`))
|
|
@@ -242,15 +252,22 @@ export function createTokenChannelSettings(definition) {
|
|
|
242
252
|
h('p', null, model.error?.message),
|
|
243
253
|
h(Button, { onClick: () => void loadStatus() }, '重新读取')))
|
|
244
254
|
: h(React.Fragment, null,
|
|
245
|
-
credentialOpen ?
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
255
|
+
credentialOpen ? (CredentialPanel
|
|
256
|
+
? h(CredentialPanel, {
|
|
257
|
+
busy,
|
|
258
|
+
error: credentialError,
|
|
259
|
+
onSubmit: bindCredentials,
|
|
260
|
+
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
|
261
|
+
})
|
|
262
|
+
: h(CredentialBindingPanel, {
|
|
263
|
+
channel,
|
|
264
|
+
secretLabel: 'Bot Token',
|
|
265
|
+
secretPlaceholder: tokenPlaceholder,
|
|
266
|
+
busy,
|
|
267
|
+
error: credentialError,
|
|
268
|
+
onSubmit: bindCredentials,
|
|
269
|
+
onCancel: () => { setCredentialOpen(false); setCredentialError(null); },
|
|
270
|
+
})) : null,
|
|
254
271
|
model.bots.length === 0 && !credentialOpen
|
|
255
272
|
? h('div', { className: 'ddt-card dim-surfaceCard' },
|
|
256
273
|
h('div', { className: 'ddt-cardBody ddt-empty dim-surfaceBody dim-emptyView' },
|
|
@@ -264,7 +281,7 @@ export function createTokenChannelSettings(definition) {
|
|
|
264
281
|
h(Button, {
|
|
265
282
|
kind: 'primary',
|
|
266
283
|
onClick: () => setCredentialOpen(true),
|
|
267
|
-
},
|
|
284
|
+
}, emptyActionLabel))),
|
|
268
285
|
h('div', {
|
|
269
286
|
className: `ddt-brandMark dim-emptyBrand ${avatarClass}`,
|
|
270
287
|
'aria-hidden': 'true',
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
|
2
|
+
|
|
3
|
+
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
4
|
+
status: 'connection.status',
|
|
5
|
+
beginProvisioning: 'provision.begin',
|
|
6
|
+
pollProvisioning: 'provision.poll',
|
|
7
|
+
cancelProvisioning: 'provision.cancel',
|
|
8
|
+
reconnectBot: 'bot.reconnect',
|
|
9
|
+
deleteBot: 'bot.delete',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const PROVISION_STATES = new Set(['starting', 'pending', 'connecting', 'connected', 'failed', 'cancelled']);
|
|
13
|
+
const BOT_STATES = new Set(['connected', 'connecting', 'offline', 'error']);
|
|
14
|
+
const QR_DATA_URL = /^data:image\/(?:png|webp);base64,[a-z\d+/]+={0,2}$/i;
|
|
15
|
+
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function text(value, fallback, max = 240) {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, max) : fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function id(value) {
|
|
25
|
+
const result = text(value, '', 128);
|
|
26
|
+
return /^[a-z\d_-]+$/i.test(result) ? result : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function timestamp(value) {
|
|
30
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
31
|
+
const parsed = typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
32
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function unwrapRpcResult(result) {
|
|
36
|
+
if (!isRecord(result) || typeof result.ok !== 'boolean') {
|
|
37
|
+
throw new Error('WhatsApp 服务返回了无法识别的响应');
|
|
38
|
+
}
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
const error = new Error(text(result.error?.message, 'WhatsApp 操作失败'));
|
|
41
|
+
error.code = text(result.error?.code, 'WHATSAPP_RPC_ERROR', 80);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
return result.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function safeQrSource(value) {
|
|
48
|
+
return typeof value === 'string' && value.length <= 2 * 1024 * 1024 && QR_DATA_URL.test(value)
|
|
49
|
+
? value : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeProvisioning(value, now = Date.now()) {
|
|
53
|
+
const source = isRecord(value?.provisioning) ? value.provisioning : value;
|
|
54
|
+
if (!isRecord(source)) throw new Error('WhatsApp 服务没有返回扫码进度');
|
|
55
|
+
const attemptId = id(source.attemptId);
|
|
56
|
+
if (!attemptId) throw new Error('WhatsApp 服务没有返回有效的扫码任务');
|
|
57
|
+
const reported = text(source.status, 'failed', 32);
|
|
58
|
+
const result = {
|
|
59
|
+
attemptId,
|
|
60
|
+
status: PROVISION_STATES.has(reported) ? reported : 'failed',
|
|
61
|
+
expiresAt: timestamp(source.expiresAt) ?? now + 60_000,
|
|
62
|
+
pollIntervalMs: Math.min(5_000, Math.max(500, Number(source.pollIntervalMs) || 1_000)),
|
|
63
|
+
qrRevision: Number.isSafeInteger(source.qrRevision) ? source.qrRevision : 0,
|
|
64
|
+
};
|
|
65
|
+
const qrCodeDataUrl = safeQrSource(source.qrCodeDataUrl);
|
|
66
|
+
if (qrCodeDataUrl) result.qrCodeDataUrl = qrCodeDataUrl;
|
|
67
|
+
if (id(source.botId)) result.botId = id(source.botId);
|
|
68
|
+
if (isRecord(source.error)) result.error = {
|
|
69
|
+
code: text(source.error.code, 'WHATSAPP_PROVISION_FAILED', 80),
|
|
70
|
+
message: text(source.error.message, 'WhatsApp 没有接入完成'),
|
|
71
|
+
};
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeBot(value) {
|
|
76
|
+
if (!isRecord(value) || !id(value.botId)) return undefined;
|
|
77
|
+
const connected = value.connected === true;
|
|
78
|
+
const state = BOT_STATES.has(value.state) ? value.state : 'offline';
|
|
79
|
+
return {
|
|
80
|
+
botId: id(value.botId),
|
|
81
|
+
connected,
|
|
82
|
+
state: connected ? 'connected' : state,
|
|
83
|
+
bot: {
|
|
84
|
+
name: text(value.bot?.name, 'WhatsApp机器人', 100),
|
|
85
|
+
idMasked: text(value.bot?.idMasked, 'WhatsApp账号', 140),
|
|
86
|
+
},
|
|
87
|
+
health: {
|
|
88
|
+
summary: text(value.health?.summary, connected
|
|
89
|
+
? 'WhatsApp Web 关联设备运行正常' : 'WhatsApp 连接尚未就绪'),
|
|
90
|
+
lastCheckedAt: timestamp(value.health?.lastCheckedAt),
|
|
91
|
+
},
|
|
92
|
+
error: isRecord(value.error) ? {
|
|
93
|
+
code: text(value.error.code, 'WHATSAPP_ACCOUNT_ERROR', 80),
|
|
94
|
+
message: text(value.error.message, 'WhatsApp 连接尚未就绪'),
|
|
95
|
+
} : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function normalizeSnapshot(value) {
|
|
100
|
+
const source = isRecord(value?.snapshot) ? value.snapshot : value;
|
|
101
|
+
if (!isRecord(source) || !Array.isArray(source.bots)) {
|
|
102
|
+
throw new Error('WhatsApp 服务没有返回有效的机器人列表');
|
|
103
|
+
}
|
|
104
|
+
const bots = source.bots.map(normalizeBot).filter(Boolean);
|
|
105
|
+
return {
|
|
106
|
+
revision: Number.isSafeInteger(source.revision) ? source.revision : 0,
|
|
107
|
+
bots,
|
|
108
|
+
totals: { configured: bots.length, connected: bots.filter((bot) => bot.connected).length },
|
|
109
|
+
provisioning: source.provisioning ? normalizeProvisioning(source.provisioning) : null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function presentError(error) {
|
|
114
|
+
return {
|
|
115
|
+
code: text(error?.code, 'WHATSAPP_ERROR', 80),
|
|
116
|
+
message: text(error?.message, 'WhatsApp 操作失败,请稍后重试'),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatRemaining(milliseconds) {
|
|
121
|
+
const seconds = Math.max(0, Math.ceil(Number(milliseconds) / 1_000) || 0);
|
|
122
|
+
return `${String(Math.floor(seconds / 60)).padStart(2, '0')}:${String(seconds % 60).padStart(2, '0')}`;
|
|
123
|
+
}
|