@xmanrui/dsh-im 1.4.0 → 2.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 +18 -6
- package/README.md +18 -6
- package/lib/index.js +206 -176
- package/package.json +1 -1
- package/plugin-src/host/index.mjs +2 -0
- package/src/channels/dingtalk/config-store.mjs +5 -3
- package/src/channels/dingtalk/dingtalk-api.mjs +105 -82
- package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
- package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
- package/src/channels/dingtalk/state-store.mjs +3 -1
- package/src/channels/discord/config-store.mjs +2 -1
- package/src/channels/discord/discord-api.mjs +25 -1
- package/src/channels/discord/discord-runtime.mjs +237 -11
- package/src/channels/feishu/bridge.mjs +169 -180
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +57 -21
- package/src/channels/feishu/feishu-runtime.mjs +10 -9
- package/src/channels/feishu/message-utils.mjs +4 -3
- package/src/channels/office/office-job-executor.mjs +15 -14
- package/src/channels/office/office-runtime.mjs +6 -5
- package/src/channels/qq/config-store.mjs +3 -1
- package/src/channels/qq/markdown-reply.mjs +4 -2
- package/src/channels/qq/qq-bridge.mjs +149 -130
- package/src/channels/qq/qq-controller.mjs +17 -16
- package/src/channels/qq/qq-runtime.mjs +3 -2
- package/src/channels/shared/agent-preset.mjs +3 -1
- package/src/channels/shared/compact-command.mjs +15 -14
- package/src/channels/shared/connection-test.mjs +6 -4
- package/src/channels/shared/control-command.mjs +13 -11
- package/src/channels/shared/editable-message-stream.mjs +4 -2
- package/src/channels/shared/harness-approval.mjs +23 -21
- package/src/channels/shared/harness-client.mjs +3 -2
- package/src/channels/shared/harness-question.mjs +8 -6
- package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
- package/src/channels/shared/i18n-en/discord.mjs +8 -0
- package/src/channels/shared/i18n-en/feishu.mjs +217 -0
- package/src/channels/shared/i18n-en/office.mjs +23 -0
- package/src/channels/shared/i18n-en/qq.mjs +54 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
- package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
- package/src/channels/shared/i18n-en/slack.mjs +27 -0
- package/src/channels/shared/i18n-en/telegram.mjs +21 -0
- package/src/channels/shared/i18n-en/wecom.mjs +38 -0
- package/src/channels/shared/i18n-en/weixin.mjs +80 -0
- package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
- package/src/channels/shared/i18n-en.mjs +35 -0
- package/src/channels/shared/i18n.mjs +35 -0
- package/src/channels/shared/image-prompt.mjs +17 -15
- package/src/channels/shared/inbound-file.mjs +5 -3
- package/src/channels/shared/model-command.mjs +35 -31
- package/src/channels/shared/preset-command.mjs +41 -38
- package/src/channels/shared/semantic/artifact-delivery.mjs +170 -0
- package/src/channels/shared/semantic/artifact.mjs +2 -2
- package/src/channels/shared/semantic/delivery.mjs +40 -0
- package/src/channels/shared/text-harness-bridge.mjs +154 -136
- package/src/channels/shared/token-bot-controller.mjs +32 -11
- package/src/channels/shared/workspace-command.mjs +69 -58
- package/src/channels/slack/config-store.mjs +3 -1
- package/src/channels/slack/slack-api.mjs +6 -5
- package/src/channels/slack/slack-controller.mjs +10 -9
- package/src/channels/slack/slack-runtime.mjs +16 -4
- package/src/channels/telegram/config-store.mjs +2 -1
- package/src/channels/telegram/telegram-api.mjs +111 -19
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +383 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +154 -153
- package/src/channels/wecom/wecom-controller.mjs +20 -19
- package/src/channels/wecom/wecom-runtime.mjs +2 -1
- package/src/channels/weixin/config-store.mjs +2 -1
- package/src/channels/weixin/weixin-api.mjs +140 -107
- package/src/channels/weixin/weixin-bridge.mjs +86 -123
- package/src/channels/weixin/weixin-controller.mjs +22 -21
- package/src/channels/weixin/weixin-runtime.mjs +3 -2
- package/src/channels/whatsapp/config-store.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
- package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
connectionTestMessage,
|
|
11
11
|
connectionTestTargetUnavailable,
|
|
12
12
|
} from '../shared/connection-test.mjs';
|
|
13
|
+
import { t } from '../shared/i18n.mjs';
|
|
13
14
|
|
|
14
15
|
const ACTIVE_ATTEMPT_STATES = new Set([
|
|
15
16
|
'starting',
|
|
@@ -98,8 +99,8 @@ function publicProvisioningError(error) {
|
|
|
98
99
|
if (error instanceof WeixinApiError) return safeAccountError(error.code, error.message);
|
|
99
100
|
const message = ACTIVATION_ERROR_MESSAGES[error?.code];
|
|
100
101
|
return message
|
|
101
|
-
? safeAccountError(error.code, message)
|
|
102
|
-
: safeAccountError('activation-unknown-failed', '微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。');
|
|
102
|
+
? safeAccountError(error.code, t(message))
|
|
103
|
+
: safeAccountError('activation-unknown-failed', t('微信已授权,但激活过程中发生未知错误。请查看 dsh web 日志。'));
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
function preserveActivationError(error, fallbackCode) {
|
|
@@ -167,7 +168,7 @@ export class WeixinController {
|
|
|
167
168
|
if (!token) {
|
|
168
169
|
this.#errors.set(
|
|
169
170
|
latest.botId,
|
|
170
|
-
safeAccountError('missing-token', '登录凭据缺失,请移除账号后重新扫码。'),
|
|
171
|
+
safeAccountError('missing-token', t('登录凭据缺失,请移除账号后重新扫码。')),
|
|
171
172
|
);
|
|
172
173
|
return;
|
|
173
174
|
}
|
|
@@ -176,7 +177,7 @@ export class WeixinController {
|
|
|
176
177
|
} catch (error) {
|
|
177
178
|
this.#errors.set(
|
|
178
179
|
latest.botId,
|
|
179
|
-
safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。'),
|
|
180
|
+
safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。')),
|
|
180
181
|
);
|
|
181
182
|
this.#logger.warn?.(`[dsh-weixin] account ${latest.botId} failed to initialize:`, error);
|
|
182
183
|
} finally {
|
|
@@ -227,12 +228,12 @@ export class WeixinController {
|
|
|
227
228
|
} catch (error) {
|
|
228
229
|
if (record.controller.signal.aborted) {
|
|
229
230
|
record.state = 'cancelled';
|
|
230
|
-
record.error = safeAccountError('cancelled', '扫码绑定已取消。');
|
|
231
|
+
record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
|
|
231
232
|
} else {
|
|
232
233
|
record.state = 'failed';
|
|
233
234
|
record.error = safeAccountError(
|
|
234
235
|
error instanceof WeixinApiError ? error.code : 'qr-start-failed',
|
|
235
|
-
error instanceof WeixinApiError ? error.message : '无法生成微信二维码,请稍后重试。',
|
|
236
|
+
error instanceof WeixinApiError ? error.message : t('无法生成微信二维码,请稍后重试。'),
|
|
236
237
|
);
|
|
237
238
|
}
|
|
238
239
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
@@ -271,7 +272,7 @@ export class WeixinController {
|
|
|
271
272
|
record.verifyResolve = null;
|
|
272
273
|
await record.task?.catch(() => undefined);
|
|
273
274
|
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
|
|
274
|
-
record.error ??= safeAccountError('cancelled', '扫码绑定已取消。');
|
|
275
|
+
record.error ??= safeAccountError('cancelled', t('扫码绑定已取消。'));
|
|
275
276
|
}
|
|
276
277
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
277
278
|
this.#touch();
|
|
@@ -288,7 +289,7 @@ export class WeixinController {
|
|
|
288
289
|
await this.#startRuntime(config, token);
|
|
289
290
|
this.#errors.delete(botId);
|
|
290
291
|
} catch (error) {
|
|
291
|
-
this.#errors.set(botId, safeAccountError('connection-failed', '微信连接仍未就绪,请稍后重试。'));
|
|
292
|
+
this.#errors.set(botId, safeAccountError('connection-failed', t('微信连接仍未就绪,请稍后重试。')));
|
|
292
293
|
throw error;
|
|
293
294
|
} finally {
|
|
294
295
|
this.#touch();
|
|
@@ -303,10 +304,10 @@ export class WeixinController {
|
|
|
303
304
|
return this.#withBotTransition(botId, async () => {
|
|
304
305
|
const runtime = this.#runtimes.get(botId);
|
|
305
306
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
306
|
-
throw connectionTestTargetUnavailable('微信机器人');
|
|
307
|
+
throw connectionTestTargetUnavailable(t('微信机器人'));
|
|
307
308
|
}
|
|
308
309
|
return runtime.sendConnectionTest(connectionTestMessage(
|
|
309
|
-
|
|
310
|
+
t('微信机器人({name})', { name: maskWeixinAccountId(config.accountId) }),
|
|
310
311
|
));
|
|
311
312
|
});
|
|
312
313
|
}
|
|
@@ -352,7 +353,7 @@ export class WeixinController {
|
|
|
352
353
|
? 'error'
|
|
353
354
|
: 'offline';
|
|
354
355
|
const error = this.#errors.get(config.botId) ?? (state === 'error'
|
|
355
|
-
? safeAccountError('connection-failed', '微信连接未就绪,插件会自动重试。')
|
|
356
|
+
? safeAccountError('connection-failed', t('微信连接未就绪,插件会自动重试。'))
|
|
356
357
|
: null);
|
|
357
358
|
return {
|
|
358
359
|
botId: config.botId,
|
|
@@ -360,16 +361,16 @@ export class WeixinController {
|
|
|
360
361
|
connected,
|
|
361
362
|
configured: true,
|
|
362
363
|
bot: {
|
|
363
|
-
name: '微信机器人',
|
|
364
|
+
name: t('微信机器人'),
|
|
364
365
|
accountIdMasked: maskWeixinAccountId(config.accountId),
|
|
365
366
|
},
|
|
366
367
|
health: {
|
|
367
368
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
368
369
|
summary: connected
|
|
369
|
-
? '微信消息长轮询运行正常'
|
|
370
|
+
? t('微信消息长轮询运行正常')
|
|
370
371
|
: state === 'error'
|
|
371
|
-
? '微信连接未就绪,插件会自动重试'
|
|
372
|
-
: '微信连接当前离线',
|
|
372
|
+
? t('微信连接未就绪,插件会自动重试')
|
|
373
|
+
: t('微信连接当前离线'),
|
|
373
374
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
374
375
|
},
|
|
375
376
|
stats: {
|
|
@@ -438,11 +439,11 @@ export class WeixinController {
|
|
|
438
439
|
record.state = 'needs_verification';
|
|
439
440
|
} else if (response.status === 'verify_code_blocked') {
|
|
440
441
|
record.state = 'failed';
|
|
441
|
-
record.error = safeAccountError('verification-blocked', '配对码多次错误,请重新生成二维码。');
|
|
442
|
+
record.error = safeAccountError('verification-blocked', t('配对码多次错误,请重新生成二维码。'));
|
|
442
443
|
break;
|
|
443
444
|
} else if (response.status === 'expired') {
|
|
444
445
|
record.state = 'expired';
|
|
445
|
-
record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
|
|
446
|
+
record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
|
|
446
447
|
break;
|
|
447
448
|
} else if (response.status === 'scaned_but_redirect') {
|
|
448
449
|
record.currentBaseUrl = apiBaseFromServer(response.redirect_host, record.currentBaseUrl);
|
|
@@ -453,7 +454,7 @@ export class WeixinController {
|
|
|
453
454
|
) ?? this.#configStore.list()[0];
|
|
454
455
|
if (!existing) {
|
|
455
456
|
record.state = 'failed';
|
|
456
|
-
record.error = safeAccountError('already-bound', '该微信账号已绑定,但本机没有可恢复的凭据。');
|
|
457
|
+
record.error = safeAccountError('already-bound', t('该微信账号已绑定,但本机没有可恢复的凭据。'));
|
|
457
458
|
} else {
|
|
458
459
|
record.state = 'connected';
|
|
459
460
|
record.botId = existing.botId;
|
|
@@ -465,7 +466,7 @@ export class WeixinController {
|
|
|
465
466
|
const accountId = cleanString(response.ilink_bot_id);
|
|
466
467
|
const ownerUserId = cleanString(response.ilink_user_id);
|
|
467
468
|
if (!token || !accountId || !ownerUserId) {
|
|
468
|
-
throw new WeixinApiError('incomplete-login', '微信授权成功,但返回的账号凭据不完整。');
|
|
469
|
+
throw new WeixinApiError('incomplete-login', t('微信授权成功,但返回的账号凭据不完整。'));
|
|
469
470
|
}
|
|
470
471
|
record.state = 'connecting';
|
|
471
472
|
this.#touch();
|
|
@@ -485,12 +486,12 @@ export class WeixinController {
|
|
|
485
486
|
if (!record.controller.signal.aborted && Date.now() >= record.expiresAt
|
|
486
487
|
&& !TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
|
487
488
|
record.state = 'expired';
|
|
488
|
-
record.error = safeAccountError('expired', '二维码已过期,请重新生成。');
|
|
489
|
+
record.error = safeAccountError('expired', t('二维码已过期,请重新生成。'));
|
|
489
490
|
}
|
|
490
491
|
} catch (error) {
|
|
491
492
|
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
|
492
493
|
record.state = 'cancelled';
|
|
493
|
-
record.error = safeAccountError('cancelled', '扫码绑定已取消。');
|
|
494
|
+
record.error = safeAccountError('cancelled', t('扫码绑定已取消。'));
|
|
494
495
|
} else {
|
|
495
496
|
record.state = 'failed';
|
|
496
497
|
record.error = publicProvisioningError(error);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
connectionTestTarget,
|
|
5
5
|
connectionTestTargetUnavailable,
|
|
6
6
|
} from '../shared/connection-test.mjs';
|
|
7
|
+
import { t } from '../shared/i18n.mjs';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_START_RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000]);
|
|
9
10
|
const HARNESS_HEALTH_ERROR_CODES = new Set([
|
|
@@ -224,7 +225,7 @@ export class WeixinRuntime {
|
|
|
224
225
|
const code = response.errcode ?? response.ret;
|
|
225
226
|
throw new WeixinApiError(
|
|
226
227
|
code === -14 ? 'stale-token' : 'updates-rejected',
|
|
227
|
-
code === -14 ? '微信登录凭据已失效,请移除账号后重新扫码。' : '微信消息同步请求被拒绝。',
|
|
228
|
+
code === -14 ? t('微信登录凭据已失效,请移除账号后重新扫码。') : t('微信消息同步请求被拒绝。'),
|
|
228
229
|
);
|
|
229
230
|
}
|
|
230
231
|
consecutiveFailures = 0;
|
|
@@ -293,7 +294,7 @@ export class WeixinRuntime {
|
|
|
293
294
|
: typeof this.#config.ownerUserId === 'string' && this.#config.ownerUserId.trim()
|
|
294
295
|
? this.#config.ownerUserId.trim()
|
|
295
296
|
: null;
|
|
296
|
-
if (!toUserId) throw connectionTestTargetUnavailable('微信机器人');
|
|
297
|
+
if (!toUserId) throw connectionTestTargetUnavailable(t('微信机器人'));
|
|
297
298
|
if (!this.#status.ready || !this.#abortController) {
|
|
298
299
|
throw new Error('Weixin runtime is not connected');
|
|
299
300
|
}
|
|
@@ -2,6 +2,8 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
|
|
5
|
+
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
|
|
5
7
|
const EMPTY_DOCUMENT = Object.freeze({ version: 2, bots: Object.freeze([]) });
|
|
6
8
|
const BOT_ID_PATTERN = /^whatsapp_[a-f0-9]{24}$/;
|
|
7
9
|
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
|
@@ -30,7 +32,7 @@ export function deriveWhatsappBotId(accountJid) {
|
|
|
30
32
|
|
|
31
33
|
export function maskWhatsappAccount(accountJid) {
|
|
32
34
|
const digits = normalizeWhatsappAccountJid(accountJid)?.split('@')[0] ?? '';
|
|
33
|
-
if (!digits) return 'WhatsApp账号';
|
|
35
|
+
if (!digits) return t('WhatsApp账号');
|
|
34
36
|
if (digits.length <= 7) return `${digits.slice(0, 2)}•••${digits.slice(-2)}`;
|
|
35
37
|
return `${digits.slice(0, 4)}••••${digits.slice(-4)}`;
|
|
36
38
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { t } from '../shared/i18n.mjs';
|
|
1
2
|
import { createTextBridgeStatus, TextHarnessBridge } from '../shared/text-harness-bridge.mjs';
|
|
2
3
|
|
|
3
4
|
export const WHATSAPP_DESCRIPTOR = Object.freeze({
|
|
4
5
|
key: 'whatsapp',
|
|
5
6
|
label: 'WhatsApp',
|
|
6
|
-
|
|
7
|
+
// Translated lazily: t() must run after setImHostLanguage, not at import time.
|
|
8
|
+
get connectionLabel() { return t(' Web 关联设备'); },
|
|
7
9
|
});
|
|
8
10
|
|
|
9
11
|
export class WhatsappHarnessBridge extends TextHarnessBridge {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
|
|
3
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
4
|
+
import { t } from '../shared/i18n.mjs';
|
|
4
5
|
import {
|
|
5
6
|
deriveWhatsappBotId,
|
|
6
7
|
maskWhatsappAccount,
|
|
@@ -83,8 +84,8 @@ export class WhatsappController {
|
|
|
83
84
|
this.#errors.set(config.botId, safeError(
|
|
84
85
|
error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
|
|
85
86
|
error?.code === 'relink-required'
|
|
86
|
-
? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
|
|
87
|
-
: 'WhatsApp 连接未就绪,插件会自动重试。',
|
|
87
|
+
? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
|
|
88
|
+
: t('WhatsApp 连接未就绪,插件会自动重试。'),
|
|
88
89
|
));
|
|
89
90
|
this.#logger.warn?.(`[dsh-im:whatsapp] bot ${config.botId} failed to initialize`);
|
|
90
91
|
} finally {
|
|
@@ -172,7 +173,7 @@ export class WhatsappController {
|
|
|
172
173
|
await record.task?.catch(() => undefined);
|
|
173
174
|
if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
|
|
174
175
|
record.state = 'cancelled';
|
|
175
|
-
record.error = safeError('cancelled', '扫码接入已取消。');
|
|
176
|
+
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
176
177
|
}
|
|
177
178
|
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
|
178
179
|
}
|
|
@@ -192,8 +193,8 @@ export class WhatsappController {
|
|
|
192
193
|
this.#errors.set(botId, safeError(
|
|
193
194
|
error?.code === 'relink-required' ? 'relink-required' : 'connection-failed',
|
|
194
195
|
error?.code === 'relink-required'
|
|
195
|
-
? 'WhatsApp 关联设备已失效,请移除后重新扫码。'
|
|
196
|
-
: 'WhatsApp 连接仍未就绪,请稍后重试。',
|
|
196
|
+
? t('WhatsApp 关联设备已失效,请移除后重新扫码。')
|
|
197
|
+
: t('WhatsApp 连接仍未就绪,请稍后重试。'),
|
|
197
198
|
));
|
|
198
199
|
throw error;
|
|
199
200
|
} finally {
|
|
@@ -209,14 +210,14 @@ export class WhatsappController {
|
|
|
209
210
|
return this.#withBotTransition(botId, async () => {
|
|
210
211
|
const runtime = this.#runtimes.get(botId);
|
|
211
212
|
if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
|
|
212
|
-
const error = new Error('WhatsApp机器人尚未连接');
|
|
213
|
+
const error = new Error(t('WhatsApp机器人尚未连接'));
|
|
213
214
|
error.code = 'test-target-unavailable';
|
|
214
215
|
throw error;
|
|
215
216
|
}
|
|
216
217
|
return runtime.sendConnectionTest(
|
|
217
218
|
connectionTestMessage(
|
|
218
219
|
`${config.name}(${maskWhatsappAccount(config.accountJid)})`,
|
|
219
|
-
'WhatsApp机器人',
|
|
220
|
+
t('WhatsApp机器人'),
|
|
220
221
|
),
|
|
221
222
|
);
|
|
222
223
|
});
|
|
@@ -275,8 +276,8 @@ export class WhatsappController {
|
|
|
275
276
|
bot: { name: config.name, idMasked: maskWhatsappAccount(config.accountJid) },
|
|
276
277
|
health: {
|
|
277
278
|
status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
|
|
278
|
-
summary: connected ? 'WhatsApp Web 关联设备运行正常'
|
|
279
|
-
: state === 'error' ? 'WhatsApp 连接未就绪' : 'WhatsApp 连接当前离线',
|
|
279
|
+
summary: connected ? t('WhatsApp Web 关联设备运行正常')
|
|
280
|
+
: state === 'error' ? t('WhatsApp 连接未就绪') : t('WhatsApp 连接当前离线'),
|
|
280
281
|
lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
|
|
281
282
|
lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
|
|
282
283
|
},
|
|
@@ -340,7 +341,7 @@ export class WhatsappController {
|
|
|
340
341
|
await this.#startRuntime(config);
|
|
341
342
|
this.#errors.delete(botId);
|
|
342
343
|
} catch (error) {
|
|
343
|
-
this.#errors.set(botId, safeError('connection-failed', 'WhatsApp 已绑定,消息连接暂未就绪。'));
|
|
344
|
+
this.#errors.set(botId, safeError('connection-failed', t('WhatsApp 已绑定,消息连接暂未就绪。')));
|
|
344
345
|
this.#logger.warn?.(`[dsh-im:whatsapp] bot ${botId} did not reconnect after QR binding`);
|
|
345
346
|
}
|
|
346
347
|
if (previous?.authDirectory && previous.authDirectory !== config.authDirectory) {
|
|
@@ -363,11 +364,11 @@ export class WhatsappController {
|
|
|
363
364
|
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
|
364
365
|
if (previous) await this.#startRuntime(previous).catch(() => undefined);
|
|
365
366
|
record.state = 'cancelled';
|
|
366
|
-
record.error = safeError('cancelled', '扫码接入已取消。');
|
|
367
|
+
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
367
368
|
} else {
|
|
368
369
|
await this.#deleteAuth(record.authDirectory).catch(() => undefined);
|
|
369
370
|
record.state = 'failed';
|
|
370
|
-
record.error = safeError('activation-failed', 'WhatsApp 已扫码,但无法保存关联设备。');
|
|
371
|
+
record.error = safeError('activation-failed', t('WhatsApp 已扫码,但无法保存关联设备。'));
|
|
371
372
|
this.#logger.error?.('[dsh-im:whatsapp] unable to persist linked-device session');
|
|
372
373
|
}
|
|
373
374
|
} finally {
|
|
@@ -380,10 +381,10 @@ export class WhatsappController {
|
|
|
380
381
|
if (TERMINAL_ATTEMPT_STATES.has(record.state)) return;
|
|
381
382
|
if (record.controller.signal.aborted || error?.name === 'AbortError') {
|
|
382
383
|
record.state = 'cancelled';
|
|
383
|
-
record.error = safeError('cancelled', '扫码接入已取消。');
|
|
384
|
+
record.error = safeError('cancelled', t('扫码接入已取消。'));
|
|
384
385
|
} else {
|
|
385
386
|
record.state = 'failed';
|
|
386
|
-
record.error = safeError('qr-connect-failed', '无法连接 WhatsApp,请重新生成二维码。');
|
|
387
|
+
record.error = safeError('qr-connect-failed', t('无法连接 WhatsApp,请重新生成二维码。'));
|
|
387
388
|
}
|
|
388
389
|
if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
|
|
389
390
|
await record.session?.close().catch(() => undefined);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@whiskeysockets/baileys';
|
|
8
8
|
|
|
9
9
|
import { splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
10
|
+
import { t } from '../shared/i18n.mjs';
|
|
10
11
|
import { ImagePromptError } from '../shared/image-prompt.mjs';
|
|
11
12
|
import { trackOutboundArtifactProviderPromise } from '../shared/semantic/artifact.mjs';
|
|
12
13
|
import { createWhatsappBridgeStatus, WhatsappHarnessBridge } from './whatsapp-bridge.mjs';
|
|
@@ -107,7 +108,7 @@ async function downloadWhatsappImage(message, download, {
|
|
|
107
108
|
throw new ImagePromptError(
|
|
108
109
|
'image-download-failed',
|
|
109
110
|
`WhatsApp image download timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
|
110
|
-
'图片下载失败,请重新发送后再试。',
|
|
111
|
+
t('图片下载失败,请重新发送后再试。'),
|
|
111
112
|
);
|
|
112
113
|
}
|
|
113
114
|
throw error;
|
|
@@ -126,7 +127,7 @@ async function downloadWhatsappImage(message, download, {
|
|
|
126
127
|
throw new ImagePromptError(
|
|
127
128
|
'image-too-large',
|
|
128
129
|
`WhatsApp image exceeded ${maxBytes} bytes`,
|
|
129
|
-
'图片超过 5 MB,请压缩后重试。',
|
|
130
|
+
t('图片超过 5 MB,请压缩后重试。'),
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
chunks.push(data);
|
|
@@ -137,7 +138,7 @@ async function downloadWhatsappImage(message, download, {
|
|
|
137
138
|
throw new ImagePromptError(
|
|
138
139
|
'image-download-failed',
|
|
139
140
|
`WhatsApp image stream timed out after ${IMAGE_DOWNLOAD_TIMEOUT_MS} ms`,
|
|
140
|
-
'图片下载失败,请重新发送后再试。',
|
|
141
|
+
t('图片下载失败,请重新发送后再试。'),
|
|
141
142
|
);
|
|
142
143
|
}
|
|
143
144
|
throw error;
|
|
@@ -313,12 +314,31 @@ function waitWithSignal(promise, signal) {
|
|
|
313
314
|
|
|
314
315
|
function uncertainArtifactDelivery(error) {
|
|
315
316
|
if (error?.code === 'artifact-delivery-uncertain') return error;
|
|
316
|
-
const uncertain = new Error('WhatsApp could not confirm
|
|
317
|
+
const uncertain = new Error('WhatsApp could not confirm artifact delivery.');
|
|
317
318
|
uncertain.code = 'artifact-delivery-uncertain';
|
|
318
319
|
uncertain.cause = error;
|
|
319
320
|
return uncertain;
|
|
320
321
|
}
|
|
321
322
|
|
|
323
|
+
function whatsappArtifactError(error) {
|
|
324
|
+
if (error?.code?.startsWith?.('artifact-')) return error;
|
|
325
|
+
const status = error?.output?.statusCode
|
|
326
|
+
?? error?.data?.statusCode
|
|
327
|
+
?? error?.statusCode;
|
|
328
|
+
let code;
|
|
329
|
+
if (status === 401 || status === 403) code = 'artifact-permission-required';
|
|
330
|
+
else if (status === 413) code = 'artifact-too-large';
|
|
331
|
+
else if (status === 429) code = 'artifact-rate-limited';
|
|
332
|
+
else if ([400, 404, 405, 406, 410, 415, 422].includes(status)) {
|
|
333
|
+
code = 'artifact-provider-rejected';
|
|
334
|
+
}
|
|
335
|
+
if (!code) return uncertainArtifactDelivery(error);
|
|
336
|
+
const wrapped = new Error('WhatsApp rejected artifact delivery.');
|
|
337
|
+
wrapped.code = code;
|
|
338
|
+
wrapped.cause = error;
|
|
339
|
+
return wrapped;
|
|
340
|
+
}
|
|
341
|
+
|
|
322
342
|
export class WhatsappBotClient {
|
|
323
343
|
#socket;
|
|
324
344
|
#outboundIds;
|
|
@@ -354,14 +374,32 @@ export class WhatsappBotClient {
|
|
|
354
374
|
}
|
|
355
375
|
|
|
356
376
|
async sendFile(target, file) {
|
|
377
|
+
return this.#sendArtifact(target, file, {
|
|
378
|
+
document: file.bytes,
|
|
379
|
+
mimetype: file.mediaType ?? 'application/octet-stream',
|
|
380
|
+
fileName: file.fileName,
|
|
381
|
+
}, 'file');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async sendImage(target, file) {
|
|
385
|
+
return this.#sendArtifact(target, file, {
|
|
386
|
+
image: file.bytes,
|
|
387
|
+
mimetype: file.mediaType ?? 'image/jpeg',
|
|
388
|
+
}, 'image');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async #sendArtifact(target, file, content, presentation) {
|
|
357
392
|
this.#signal?.throwIfAborted();
|
|
358
393
|
await this.#stopTyping(target.jid);
|
|
359
394
|
this.#signal?.throwIfAborted();
|
|
360
395
|
const deliverySeed = typeof file.deliveryKey === 'string' && file.deliveryKey
|
|
361
396
|
? file.deliveryKey
|
|
362
397
|
: file.artifactId;
|
|
363
|
-
const
|
|
364
|
-
?
|
|
398
|
+
const messageIdSeed = presentation === 'image'
|
|
399
|
+
? `${deliverySeed}:image`
|
|
400
|
+
: deliverySeed;
|
|
401
|
+
const messageId = typeof messageIdSeed === 'string' && messageIdSeed
|
|
402
|
+
? createHash('sha256').update(messageIdSeed).digest('hex').slice(0, 20).toUpperCase()
|
|
365
403
|
: undefined;
|
|
366
404
|
const options = {
|
|
367
405
|
...(target.quoted ? { quoted: target.quoted } : {}),
|
|
@@ -375,11 +413,7 @@ export class WhatsappBotClient {
|
|
|
375
413
|
try {
|
|
376
414
|
const pending = this.#socket.sendMessage(
|
|
377
415
|
target.jid,
|
|
378
|
-
|
|
379
|
-
document: file.bytes,
|
|
380
|
-
mimetype: file.mediaType ?? 'application/octet-stream',
|
|
381
|
-
fileName: file.fileName,
|
|
382
|
-
},
|
|
416
|
+
content,
|
|
383
417
|
options,
|
|
384
418
|
);
|
|
385
419
|
trackOutboundArtifactProviderPromise(file, pending);
|
|
@@ -390,7 +424,7 @@ export class WhatsappBotClient {
|
|
|
390
424
|
result = await waitWithSignal(pending, waitSignal);
|
|
391
425
|
} catch (error) {
|
|
392
426
|
if (this.#signal?.aborted) throw abortReason(this.#signal);
|
|
393
|
-
throw
|
|
427
|
+
throw whatsappArtifactError(error);
|
|
394
428
|
}
|
|
395
429
|
this.#signal?.throwIfAborted();
|
|
396
430
|
this.#outboundIds.remember(result?.key?.id);
|
|
@@ -607,7 +641,7 @@ export class WhatsappRuntime {
|
|
|
607
641
|
|
|
608
642
|
async sendConnectionTest(text) {
|
|
609
643
|
if (!this.#status.ready || !this.#client) {
|
|
610
|
-
const error = new Error('WhatsApp机器人尚未连接');
|
|
644
|
+
const error = new Error(t('WhatsApp机器人尚未连接'));
|
|
611
645
|
error.code = 'test-target-unavailable';
|
|
612
646
|
throw error;
|
|
613
647
|
}
|
|
@@ -7,6 +7,8 @@ import makeWASocket, {
|
|
|
7
7
|
useMultiFileAuthState,
|
|
8
8
|
} from '@whiskeysockets/baileys';
|
|
9
9
|
|
|
10
|
+
import { t } from '../shared/i18n.mjs';
|
|
11
|
+
|
|
10
12
|
const SILENT_LOGGER = Object.freeze({
|
|
11
13
|
level: 'silent',
|
|
12
14
|
trace() {},
|
|
@@ -57,7 +59,7 @@ function normalizeIdentity(socket, authState) {
|
|
|
57
59
|
return {
|
|
58
60
|
accountJid,
|
|
59
61
|
name: typeof source?.name === 'string' && source.name.trim()
|
|
60
|
-
? source.name.trim().slice(0, 100) : 'WhatsApp机器人',
|
|
62
|
+
? source.name.trim().slice(0, 100) : t('WhatsApp机器人'),
|
|
61
63
|
};
|
|
62
64
|
}
|
|
63
65
|
|