@xmanrui/dsh-im 1.5.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 +16 -4
- package/README.md +16 -4
- package/lib/index.js +208 -178
- 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 +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
- 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 +131 -123
- package/src/channels/feishu/feishu-cards.mjs +62 -54
- package/src/channels/feishu/feishu-channel.mjs +5 -4
- 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 +57 -52
- 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 +1 -1
- package/src/channels/shared/semantic/delivery.mjs +38 -0
- package/src/channels/shared/text-harness-bridge.mjs +130 -63
- 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 +72 -6
- package/src/channels/telegram/telegram-rich-message.mjs +147 -0
- package/src/channels/telegram/telegram-runtime.mjs +373 -5
- package/src/channels/wecom/config-store.mjs +3 -1
- package/src/channels/wecom/wecom-bridge.mjs +59 -55
- 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-bridge.mjs +50 -49
- 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 +5 -4
- package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
|
@@ -43,31 +43,32 @@ import {
|
|
|
43
43
|
createDeliveryReceipt,
|
|
44
44
|
providerMessageIdsFor,
|
|
45
45
|
} from '../shared/semantic/delivery.mjs';
|
|
46
|
+
import { t } from '../shared/i18n.mjs';
|
|
46
47
|
|
|
47
|
-
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
48
|
-
const GENERIC_PROCESSING_ERROR = '消息处理失败,请稍后重试。';
|
|
48
|
+
const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
|
|
49
|
+
const GENERIC_PROCESSING_ERROR = () => t('消息处理失败,请稍后重试。');
|
|
49
50
|
|
|
50
|
-
const HELP_TEXT = [
|
|
51
|
-
'微信已连接 DeepSeek Harness。',
|
|
51
|
+
const HELP_TEXT = () => [
|
|
52
|
+
t('微信已连接 DeepSeek Harness。'),
|
|
52
53
|
'',
|
|
53
|
-
'直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。',
|
|
54
|
-
'/new 开启一个全新会话',
|
|
55
|
-
'/compact 压缩当前会话的较早上下文',
|
|
56
|
-
'/workspace 工作区绝对路径 切换工作区',
|
|
57
|
-
'/workspacelist 列出工作区绝对路径',
|
|
58
|
-
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
59
|
-
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
60
|
-
'/models 按序号列出所有可用模型',
|
|
61
|
-
'/model [序号或完整模型ID] 查看或切换当前会话模型',
|
|
62
|
-
'示例:先发 /models,再发 /model 2',
|
|
63
|
-
'/presetlist 按序号列出可用 Agent Preset',
|
|
64
|
-
'/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
|
|
65
|
-
'纯数字 ID:/preset id:<ID>',
|
|
66
|
-
'/preset --default 跟随 Host 默认',
|
|
67
|
-
'/stop 停止当前任务',
|
|
68
|
-
'/steer 补充指令 纠偏当前任务',
|
|
69
|
-
'/status 检查连接状态',
|
|
70
|
-
'/help 显示本帮助',
|
|
54
|
+
t('直接发送文字、图片、文件或带文字识别结果的语音即可继续当前会话。'),
|
|
55
|
+
t('/new 开启一个全新会话'),
|
|
56
|
+
t('/compact 压缩当前会话的较早上下文'),
|
|
57
|
+
t('/workspace 工作区绝对路径 切换工作区'),
|
|
58
|
+
t('/workspacelist 列出工作区绝对路径'),
|
|
59
|
+
t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
|
|
60
|
+
t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
|
|
61
|
+
t('/models 按序号列出所有可用模型'),
|
|
62
|
+
t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
|
|
63
|
+
t('示例:先发 /models,再发 /model 2'),
|
|
64
|
+
t('/presetlist 按序号列出可用 Agent Preset'),
|
|
65
|
+
t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
|
|
66
|
+
t('纯数字 ID:/preset id:<ID>'),
|
|
67
|
+
t('/preset --default 跟随 Host 默认'),
|
|
68
|
+
t('/stop 停止当前任务'),
|
|
69
|
+
t('/steer 补充指令 纠偏当前任务'),
|
|
70
|
+
t('/status 检查连接状态'),
|
|
71
|
+
t('/help 显示本帮助'),
|
|
71
72
|
].join('\n');
|
|
72
73
|
|
|
73
74
|
function conversationKey(userId) {
|
|
@@ -108,7 +109,7 @@ function canClaimInteractionReply(message, pending) {
|
|
|
108
109
|
&& nonEmptyString(extractWeixinText(message));
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
|
112
|
+
function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR()) {
|
|
112
113
|
const diagnostic = imagePromptDiagnostic(error);
|
|
113
114
|
return {
|
|
114
115
|
code: diagnostic?.code ?? 'message-processing-failed',
|
|
@@ -119,24 +120,24 @@ function safeMessageError(error, userMessage = GENERIC_PROCESSING_ERROR) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
function artifactFailureText(fileName, error) {
|
|
122
|
-
const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
|
|
123
|
+
const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
|
|
123
124
|
switch (error?.code) {
|
|
124
125
|
case 'artifact-delivery-uncertain':
|
|
125
|
-
return
|
|
126
|
+
return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
|
|
126
127
|
case 'artifact-permission-required':
|
|
127
|
-
return
|
|
128
|
+
return t('结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。', { name });
|
|
128
129
|
case 'artifact-too-large':
|
|
129
|
-
return
|
|
130
|
+
return t('结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。', { name });
|
|
130
131
|
case 'artifact-rate-limited':
|
|
131
|
-
return
|
|
132
|
+
return t('结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。', { name });
|
|
132
133
|
case 'artifact-provider-rejected':
|
|
133
|
-
return
|
|
134
|
+
return t('结果文件「{name}」已生成,但微信拒绝了该文件消息。', { name });
|
|
134
135
|
case 'artifact-invalid':
|
|
135
136
|
case 'artifact-changed':
|
|
136
137
|
case 'artifact-unavailable':
|
|
137
|
-
return
|
|
138
|
+
return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
|
|
138
139
|
default:
|
|
139
|
-
return
|
|
140
|
+
return t('结果文件「{name}」已生成,但暂时未能通过微信发送,请稍后重试。', { name });
|
|
140
141
|
}
|
|
141
142
|
}
|
|
142
143
|
|
|
@@ -244,7 +245,7 @@ export class WeixinHarnessBridge {
|
|
|
244
245
|
this.#status.lastError = error?.message ?? String(error);
|
|
245
246
|
this.#status.lastMessageError = safeMessageError(error);
|
|
246
247
|
this.#logger.error?.('[dsh-weixin] failed to process a command:', error);
|
|
247
|
-
return this.#send(sender, GENERIC_PROCESSING_ERROR, contextToken, runId)
|
|
248
|
+
return this.#send(sender, GENERIC_PROCESSING_ERROR(), contextToken, runId)
|
|
248
249
|
.catch(() => undefined);
|
|
249
250
|
}).finally(() => {
|
|
250
251
|
this.#acceptedMessageIds.delete(messageId);
|
|
@@ -400,26 +401,26 @@ export class WeixinHarnessBridge {
|
|
|
400
401
|
const hasImages = hasInboundImages(promptMessage);
|
|
401
402
|
const hasFiles = hasInboundFiles(promptMessage);
|
|
402
403
|
if (!text && !hasImages && !hasFiles) {
|
|
403
|
-
await this.#send(sender, '目前支持文字、图片、文件,以及微信已转成文字的语音消息。', contextToken, runId);
|
|
404
|
+
await this.#send(sender, t('目前支持文字、图片、文件,以及微信已转成文字的语音消息。'), contextToken, runId);
|
|
404
405
|
await this.#state.markSeen(messageId);
|
|
405
406
|
return;
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
const command = text.trim().toLowerCase();
|
|
409
410
|
if (!hasImages && !hasFiles && command === '/help') {
|
|
410
|
-
await this.#send(sender, HELP_TEXT, contextToken, runId);
|
|
411
|
+
await this.#send(sender, HELP_TEXT(), contextToken, runId);
|
|
411
412
|
await this.#state.markSeen(messageId);
|
|
412
413
|
return;
|
|
413
414
|
}
|
|
414
415
|
if (!hasImages && !hasFiles && command === '/status') {
|
|
415
416
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
416
|
-
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
|
417
|
+
await this.#send(sender, t('微信与 DeepSeek Harness 连接正常。'), contextToken, runId);
|
|
417
418
|
await this.#state.markSeen(messageId);
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
420
421
|
if (!hasImages && !hasFiles && command === '/new') {
|
|
421
422
|
await this.#state.clearSession(key);
|
|
422
|
-
await this.#send(sender, '已开启新会话。请发送你的问题。', contextToken, runId);
|
|
423
|
+
await this.#send(sender, t('已开启新会话。请发送你的问题。'), contextToken, runId);
|
|
423
424
|
await this.#state.markSeen(messageId);
|
|
424
425
|
return;
|
|
425
426
|
}
|
|
@@ -483,7 +484,7 @@ export class WeixinHarnessBridge {
|
|
|
483
484
|
}
|
|
484
485
|
const answerText = typeof answer === 'string' && answer.trim()
|
|
485
486
|
? answer
|
|
486
|
-
: artifacts.length > 0 ? '结果文件已生成。' : answer;
|
|
487
|
+
: artifacts.length > 0 ? t('结果文件已生成。') : answer;
|
|
487
488
|
let textDeliveryError = null;
|
|
488
489
|
let textReceipt = null;
|
|
489
490
|
try {
|
|
@@ -519,7 +520,7 @@ export class WeixinHarnessBridge {
|
|
|
519
520
|
this.#status.lastError = error?.message ?? String(error);
|
|
520
521
|
const userMessage = inboundFileUserMessage(error)
|
|
521
522
|
?? imagePromptUserMessage(error)
|
|
522
|
-
?? GENERIC_PROCESSING_ERROR;
|
|
523
|
+
?? GENERIC_PROCESSING_ERROR();
|
|
523
524
|
this.#status.lastMessageError = safeMessageError(error, userMessage);
|
|
524
525
|
this.#logger.error?.('[dsh-weixin] failed to process an inbound message:', error);
|
|
525
526
|
try {
|
|
@@ -557,7 +558,7 @@ export class WeixinHarnessBridge {
|
|
|
557
558
|
if (!text || hasWeixinImageItems(message)) {
|
|
558
559
|
await this.#send(
|
|
559
560
|
expected.actor,
|
|
560
|
-
'请用文字回答当前问题。',
|
|
561
|
+
t('请用文字回答当前问题。'),
|
|
561
562
|
contextToken,
|
|
562
563
|
runId,
|
|
563
564
|
);
|
|
@@ -569,7 +570,7 @@ export class WeixinHarnessBridge {
|
|
|
569
570
|
if (claimed && (!pending || pending !== expected)) {
|
|
570
571
|
await this.#send(
|
|
571
572
|
expected.actor,
|
|
572
|
-
INTERACTION_RESOLVED_TEXT,
|
|
573
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
573
574
|
contextToken,
|
|
574
575
|
runId,
|
|
575
576
|
);
|
|
@@ -586,7 +587,7 @@ export class WeixinHarnessBridge {
|
|
|
586
587
|
try {
|
|
587
588
|
await this.#presentInteraction(pending);
|
|
588
589
|
} catch {
|
|
589
|
-
this.#status.lastError = '微信交互问题发送失败。';
|
|
590
|
+
this.#status.lastError = t('微信交互问题发送失败。');
|
|
590
591
|
this.#logger.error?.('[dsh-weixin] failed to retry an interaction question');
|
|
591
592
|
pending.interaction.reconnect?.();
|
|
592
593
|
return;
|
|
@@ -596,7 +597,7 @@ export class WeixinHarnessBridge {
|
|
|
596
597
|
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
597
598
|
await this.#send(
|
|
598
599
|
expected.actor,
|
|
599
|
-
INTERACTION_RESOLVED_TEXT,
|
|
600
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
600
601
|
contextToken,
|
|
601
602
|
runId,
|
|
602
603
|
).catch(() => undefined);
|
|
@@ -621,7 +622,7 @@ export class WeixinHarnessBridge {
|
|
|
621
622
|
try {
|
|
622
623
|
await this.#presentInteraction(pending);
|
|
623
624
|
} catch {
|
|
624
|
-
this.#status.lastError = '微信交互问题发送失败。';
|
|
625
|
+
this.#status.lastError = t('微信交互问题发送失败。');
|
|
625
626
|
this.#logger.error?.('[dsh-weixin] failed to send the next interaction question');
|
|
626
627
|
pending.interaction.reconnect?.();
|
|
627
628
|
}
|
|
@@ -646,7 +647,7 @@ export class WeixinHarnessBridge {
|
|
|
646
647
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
647
648
|
await this.#send(
|
|
648
649
|
pending.actor,
|
|
649
|
-
INTERACTION_RESOLVED_TEXT,
|
|
650
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
650
651
|
pending.contextToken,
|
|
651
652
|
pending.runId,
|
|
652
653
|
).catch(() => undefined);
|
|
@@ -656,11 +657,11 @@ export class WeixinHarnessBridge {
|
|
|
656
657
|
pending.submitting = false;
|
|
657
658
|
pending.answers.pop();
|
|
658
659
|
pending.index -= 1;
|
|
659
|
-
this.#status.lastError = '回答提交失败。';
|
|
660
|
+
this.#status.lastError = t('回答提交失败。');
|
|
660
661
|
this.#logger.error?.('[dsh-weixin] failed to answer a Harness interaction');
|
|
661
662
|
await this.#send(
|
|
662
663
|
pending.actor,
|
|
663
|
-
'回答提交失败,请重新发送当前问题的答案。',
|
|
664
|
+
t('回答提交失败,请重新发送当前问题的答案。'),
|
|
664
665
|
pending.contextToken,
|
|
665
666
|
pending.runId,
|
|
666
667
|
).catch(() => undefined);
|
|
@@ -706,7 +707,7 @@ export class WeixinHarnessBridge {
|
|
|
706
707
|
});
|
|
707
708
|
await this.#send(
|
|
708
709
|
actor,
|
|
709
|
-
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
710
|
+
t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
|
|
710
711
|
contextToken,
|
|
711
712
|
runId,
|
|
712
713
|
).catch(() => undefined);
|
|
@@ -792,7 +793,7 @@ export class WeixinHarnessBridge {
|
|
|
792
793
|
this.#status.lastMessageAt = new Date().toISOString();
|
|
793
794
|
await this.#send(
|
|
794
795
|
nonEmptyString(message?.from_user_id),
|
|
795
|
-
INTERACTION_RESOLVED_TEXT,
|
|
796
|
+
INTERACTION_RESOLVED_TEXT(),
|
|
796
797
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
797
798
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
798
799
|
).catch(() => undefined);
|
|
@@ -840,7 +841,7 @@ export class WeixinHarnessBridge {
|
|
|
840
841
|
}
|
|
841
842
|
await this.#send(
|
|
842
843
|
nonEmptyString(message?.from_user_id),
|
|
843
|
-
GENERIC_PROCESSING_ERROR,
|
|
844
|
+
GENERIC_PROCESSING_ERROR(),
|
|
844
845
|
nonEmptyString(message?.context_token) ?? undefined,
|
|
845
846
|
nonEmptyString(message?.run_id) ?? undefined,
|
|
846
847
|
).catch(() => undefined);
|
|
@@ -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;
|
|
@@ -640,7 +641,7 @@ export class WhatsappRuntime {
|
|
|
640
641
|
|
|
641
642
|
async sendConnectionTest(text) {
|
|
642
643
|
if (!this.#status.ready || !this.#client) {
|
|
643
|
-
const error = new Error('WhatsApp机器人尚未连接');
|
|
644
|
+
const error = new Error(t('WhatsApp机器人尚未连接'));
|
|
644
645
|
error.code = 'test-target-unavailable';
|
|
645
646
|
throw error;
|
|
646
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
|
|