@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.
Files changed (80) hide show
  1. package/README.en.md +16 -4
  2. package/README.md +16 -4
  3. package/lib/index.js +208 -178
  4. package/package.json +1 -1
  5. package/plugin-src/host/index.mjs +2 -0
  6. package/src/channels/dingtalk/config-store.mjs +5 -3
  7. package/src/channels/dingtalk/dingtalk-api.mjs +2 -1
  8. package/src/channels/dingtalk/dingtalk-bridge.mjs +45 -40
  9. package/src/channels/dingtalk/dingtalk-card-stream.mjs +3 -1
  10. package/src/channels/dingtalk/dingtalk-controller.mjs +24 -23
  11. package/src/channels/dingtalk/dingtalk-runtime.mjs +4 -3
  12. package/src/channels/dingtalk/state-store.mjs +3 -1
  13. package/src/channels/discord/config-store.mjs +2 -1
  14. package/src/channels/discord/discord-api.mjs +25 -1
  15. package/src/channels/discord/discord-runtime.mjs +237 -11
  16. package/src/channels/feishu/bridge.mjs +131 -123
  17. package/src/channels/feishu/feishu-cards.mjs +62 -54
  18. package/src/channels/feishu/feishu-channel.mjs +5 -4
  19. package/src/channels/feishu/feishu-runtime.mjs +10 -9
  20. package/src/channels/feishu/message-utils.mjs +4 -3
  21. package/src/channels/office/office-job-executor.mjs +15 -14
  22. package/src/channels/office/office-runtime.mjs +6 -5
  23. package/src/channels/qq/config-store.mjs +3 -1
  24. package/src/channels/qq/markdown-reply.mjs +4 -2
  25. package/src/channels/qq/qq-bridge.mjs +57 -52
  26. package/src/channels/qq/qq-controller.mjs +17 -16
  27. package/src/channels/qq/qq-runtime.mjs +3 -2
  28. package/src/channels/shared/agent-preset.mjs +3 -1
  29. package/src/channels/shared/compact-command.mjs +15 -14
  30. package/src/channels/shared/connection-test.mjs +6 -4
  31. package/src/channels/shared/control-command.mjs +13 -11
  32. package/src/channels/shared/editable-message-stream.mjs +4 -2
  33. package/src/channels/shared/harness-approval.mjs +23 -21
  34. package/src/channels/shared/harness-client.mjs +3 -2
  35. package/src/channels/shared/harness-question.mjs +8 -6
  36. package/src/channels/shared/i18n-en/dingtalk.mjs +52 -0
  37. package/src/channels/shared/i18n-en/discord.mjs +8 -0
  38. package/src/channels/shared/i18n-en/feishu.mjs +217 -0
  39. package/src/channels/shared/i18n-en/office.mjs +23 -0
  40. package/src/channels/shared/i18n-en/qq.mjs +54 -0
  41. package/src/channels/shared/i18n-en/shared-a.mjs +91 -0
  42. package/src/channels/shared/i18n-en/shared-b.mjs +215 -0
  43. package/src/channels/shared/i18n-en/shared-c.mjs +99 -0
  44. package/src/channels/shared/i18n-en/slack.mjs +27 -0
  45. package/src/channels/shared/i18n-en/telegram.mjs +21 -0
  46. package/src/channels/shared/i18n-en/wecom.mjs +38 -0
  47. package/src/channels/shared/i18n-en/weixin.mjs +80 -0
  48. package/src/channels/shared/i18n-en/whatsapp.mjs +17 -0
  49. package/src/channels/shared/i18n-en.mjs +35 -0
  50. package/src/channels/shared/i18n.mjs +35 -0
  51. package/src/channels/shared/image-prompt.mjs +17 -15
  52. package/src/channels/shared/inbound-file.mjs +5 -3
  53. package/src/channels/shared/model-command.mjs +35 -31
  54. package/src/channels/shared/preset-command.mjs +41 -38
  55. package/src/channels/shared/semantic/artifact-delivery.mjs +1 -1
  56. package/src/channels/shared/semantic/delivery.mjs +38 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +130 -63
  58. package/src/channels/shared/token-bot-controller.mjs +32 -11
  59. package/src/channels/shared/workspace-command.mjs +69 -58
  60. package/src/channels/slack/config-store.mjs +3 -1
  61. package/src/channels/slack/slack-api.mjs +6 -5
  62. package/src/channels/slack/slack-controller.mjs +10 -9
  63. package/src/channels/slack/slack-runtime.mjs +16 -4
  64. package/src/channels/telegram/config-store.mjs +2 -1
  65. package/src/channels/telegram/telegram-api.mjs +72 -6
  66. package/src/channels/telegram/telegram-rich-message.mjs +147 -0
  67. package/src/channels/telegram/telegram-runtime.mjs +373 -5
  68. package/src/channels/wecom/config-store.mjs +3 -1
  69. package/src/channels/wecom/wecom-bridge.mjs +59 -55
  70. package/src/channels/wecom/wecom-controller.mjs +20 -19
  71. package/src/channels/wecom/wecom-runtime.mjs +2 -1
  72. package/src/channels/weixin/config-store.mjs +2 -1
  73. package/src/channels/weixin/weixin-bridge.mjs +50 -49
  74. package/src/channels/weixin/weixin-controller.mjs +22 -21
  75. package/src/channels/weixin/weixin-runtime.mjs +3 -2
  76. package/src/channels/whatsapp/config-store.mjs +3 -1
  77. package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
  78. package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
  79. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -4
  80. 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 `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
126
+ return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
126
127
  case 'artifact-permission-required':
127
- return `结果文件「${name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。`;
128
+ return t('结果文件「{name}」已生成,但微信机器人当前没有文件消息发送权限,请检查机器人文件消息能力。', { name });
128
129
  case 'artifact-too-large':
129
- return `结果文件「${name}」超过当前微信会话可发送的文件大小,未发送。`;
130
+ return t('结果文件「{name}」超过当前微信会话可发送的文件大小,未发送。', { name });
130
131
  case 'artifact-rate-limited':
131
- return `结果文件「${name}」暂时被微信限流,未能发送,请稍后重试。`;
132
+ return t('结果文件「{name}」暂时被微信限流,未能发送,请稍后重试。', { name });
132
133
  case 'artifact-provider-rejected':
133
- return `结果文件「${name}」已生成,但微信拒绝了该文件消息。`;
134
+ return t('结果文件「{name}」已生成,但微信拒绝了该文件消息。', { name });
134
135
  case 'artifact-invalid':
135
136
  case 'artifact-changed':
136
137
  case 'artifact-unavailable':
137
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
138
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
138
139
  default:
139
- return `结果文件「${name}」已生成,但暂时未能通过微信发送,请稍后重试。`;
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
- `微信机器人(${maskWeixinAccountId(config.accountId)})`,
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
- connectionLabel: ' Web 关联设备',
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