@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
@@ -36,35 +36,38 @@ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.m
36
36
  import {
37
37
  createDeliveryReceipt,
38
38
  } from '../shared/semantic/delivery.mjs';
39
+ import { t } from '../shared/i18n.mjs';
39
40
 
40
41
  const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
41
42
 
42
- const HELP_TEXT = [
43
- '企业微信机器人已连接 DeepSeek Harness。',
44
- '',
45
- '直接发送文字、图片或文件即可继续当前会话。',
46
- '/new 开启一个全新会话',
47
- '/compact 压缩当前会话的较早上下文',
48
- '/workspace 工作区绝对路径 切换工作区',
49
- '/workspacelist 列出工作区绝对路径',
50
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
51
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
52
- '/models 按序号列出所有可用模型',
53
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
54
- '示例:先发 /models,再发 /model 2',
55
- '/presetlist 按序号列出可用 Agent Preset',
56
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
57
- '纯数字 ID:/preset id:<ID>',
58
- '/preset --default 跟随 Host 默认',
59
- '/stop 停止当前任务',
60
- '/steer 补充指令 纠偏当前任务',
61
- '/status 检查连接状态',
62
- '/help 显示本帮助',
63
- ].join('\n');
43
+ // Built lazily: t() must run after setImHostLanguage, not at import time.
44
+ function helpText() {
45
+ return [
46
+ t('企业微信机器人已连接 DeepSeek Harness。'),
47
+ '',
48
+ t('直接发送文字、图片或文件即可继续当前会话。'),
49
+ t('/new 开启一个全新会话'),
50
+ t('/compact 压缩当前会话的较早上下文'),
51
+ t('/workspace 工作区绝对路径 切换工作区'),
52
+ t('/workspacelist 列出工作区绝对路径'),
53
+ t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
54
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
55
+ t('/models 按序号列出所有可用模型'),
56
+ t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
57
+ t('示例:先发 /models,再发 /model 2'),
58
+ t('/presetlist 按序号列出可用 Agent Preset'),
59
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
60
+ t('纯数字 ID:/preset id:<ID>'),
61
+ t('/preset --default 跟随 Host 默认'),
62
+ t('/stop 停止当前任务'),
63
+ t('/steer 补充指令 纠偏当前任务'),
64
+ t('/status 检查连接状态'),
65
+ t('/help 显示本帮助'),
66
+ ].join('\n');
67
+ }
64
68
  const MAX_REPLY_BYTES = 18_000;
65
69
  const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
66
70
  const MAX_PREFETCHED_IMAGES = 4;
67
- const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
68
71
 
69
72
  function nonEmptyString(value) {
70
73
  return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -137,7 +140,7 @@ function imageSource(client, image) {
137
140
  throw new ImagePromptError(
138
141
  'image-too-large',
139
142
  `Enterprise WeChat image exceeds ${maxBytes} bytes`,
140
- '图片超过 5 MB,请压缩后重试。',
143
+ t('图片超过 5 MB,请压缩后重试。'),
141
144
  );
142
145
  }
143
146
  return { data, name: result?.filename };
@@ -219,7 +222,7 @@ function prefetchInboundImages(message, signal) {
219
222
  throw new ImagePromptError(
220
223
  'image-too-large',
221
224
  `Enterprise WeChat image exceeds ${maxBytes} bytes`,
222
- '图片超过 5 MB,请压缩后重试。',
225
+ t('图片超过 5 MB,请压缩后重试。'),
223
226
  );
224
227
  }
225
228
  return result;
@@ -238,7 +241,7 @@ function imageQueueFullMessage(message) {
238
241
  throw new ImagePromptError(
239
242
  'image-queue-full',
240
243
  `Enterprise WeChat already has ${MAX_PREFETCHED_IMAGES} prefetched images`,
241
- '当前待处理图片较多,请稍后重新发送。',
244
+ t('当前待处理图片较多,请稍后重新发送。'),
242
245
  );
243
246
  },
244
247
  })),
@@ -272,31 +275,32 @@ function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
272
275
 
273
276
  function progressText(update) {
274
277
  if (update?.type === 'text') return update.text;
275
- if (update?.type === 'tool') return `正在使用${update.name}…`;
278
+ if (update?.type === 'tool') return t('正在使用{name}…', { name: update.name });
276
279
  return update?.text;
277
280
  }
278
281
 
279
282
  function artifactFailureText(fileName, error) {
280
- const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
283
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim()
284
+ || t('结果文件');
281
285
  switch (error?.code) {
282
286
  case 'artifact-delivery-uncertain':
283
- return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
287
+ return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
284
288
  case 'artifact-permission-required':
285
- return `结果文件「${name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。`;
289
+ return t('结果文件「{name}」已生成,但企业微信智能机器人缺少素材上传或文件消息能力,请检查机器人权限。', { name });
286
290
  case 'artifact-too-large':
287
- return `结果文件「${name}」超过当前企业微信机器人可发送的文件大小,未发送。`;
291
+ return t('结果文件「{name}」超过当前企业微信机器人可发送的文件大小,未发送。', { name });
288
292
  case 'artifact-empty':
289
- return `结果文件「${name}」为空,企业微信不允许发送空文件。`;
293
+ return t('结果文件「{name}」为空,企业微信不允许发送空文件。', { name });
290
294
  case 'artifact-changed':
291
295
  case 'artifact-invalid':
292
296
  case 'artifact-unavailable':
293
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
297
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
294
298
  case 'artifact-rate-limited':
295
- return `结果文件「${name}」暂时被企业微信限流,未能发送,请稍后重试。`;
299
+ return t('结果文件「{name}」暂时被企业微信限流,未能发送,请稍后重试。', { name });
296
300
  case 'artifact-provider-rejected':
297
- return `结果文件「${name}」已生成,但企业微信拒绝了该文件或文件消息。`;
301
+ return t('结果文件「{name}」已生成,但企业微信拒绝了该文件或文件消息。', { name });
298
302
  default:
299
- return `结果文件「${name}」已生成,但暂时未能通过企业微信发送,请稍后重试。`;
303
+ return t('结果文件「{name}」已生成,但暂时未能通过企业微信发送,请稍后重试。', { name });
300
304
  }
301
305
  }
302
306
 
@@ -415,7 +419,7 @@ function sendWecomFile(client, chatId, file, options) {
415
419
 
416
420
  function answerTextForDelivery(answer, artifacts) {
417
421
  if (typeof answer === 'string' && answer.trim()) return answer;
418
- return artifacts.length > 0 ? '结果文件已生成。' : '任务已完成,但没有生成可显示的文本。';
422
+ return artifacts.length > 0 ? t('结果文件已生成。') : t('任务已完成,但没有生成可显示的文本。');
419
423
  }
420
424
 
421
425
  function providerMessageId(result) {
@@ -535,7 +539,7 @@ export class WecomHarnessBridge {
535
539
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
536
540
  this.#status.lastError = error?.message ?? String(error);
537
541
  this.#logger.error?.('[dsh-im:wecom] failed to process a command');
538
- return this.#sendImmediate(frame, chatId, '消息处理失败,请稍后重试。')
542
+ return this.#sendImmediate(frame, chatId, t('消息处理失败,请稍后重试。'))
539
543
  .catch(() => undefined);
540
544
  }).finally(() => {
541
545
  this.#acceptedMessageIds.delete(messageId);
@@ -763,25 +767,25 @@ export class WecomHarnessBridge {
763
767
  let streamStarted = false;
764
768
  try {
765
769
  if (!text && !hasImages && !hasFiles) {
766
- await this.#sendImmediate(frame, chatId, '目前支持文字、图片、文件和语音转写消息。');
770
+ await this.#sendImmediate(frame, chatId, t('目前支持文字、图片、文件和语音转写消息。'));
767
771
  await this.#state.markSeen(messageId);
768
772
  return;
769
773
  }
770
774
  const command = text.toLowerCase();
771
775
  if (!hasImages && !hasFiles && command === '/help') {
772
- await this.#sendImmediate(frame, chatId, HELP_TEXT);
776
+ await this.#sendImmediate(frame, chatId, helpText());
773
777
  await this.#state.markSeen(messageId);
774
778
  return;
775
779
  }
776
780
  if (!hasImages && !hasFiles && command === '/status') {
777
781
  await this.#harness.ensureRunning({ signal: this.#signal });
778
- await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
782
+ await this.#sendImmediate(frame, chatId, t('企业微信机器人与 DeepSeek Harness 连接正常。'));
779
783
  await this.#state.markSeen(messageId);
780
784
  return;
781
785
  }
782
786
  if (!hasImages && !hasFiles && command === '/new') {
783
787
  await this.#state.clearSession(key);
784
- await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
788
+ await this.#sendImmediate(frame, chatId, t('已开启新会话。请发送你的问题。'));
785
789
  await this.#state.markSeen(messageId);
786
790
  return;
787
791
  }
@@ -812,7 +816,7 @@ export class WecomHarnessBridge {
812
816
 
813
817
  streamId = this.#generateReqId('stream');
814
818
  try {
815
- await this.#client.replyStream(frame, streamId, '正在思考中…', false);
819
+ await this.#client.replyStream(frame, streamId, t('正在思考中…'), false);
816
820
  streamStarted = true;
817
821
  } catch (error) {
818
822
  this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
@@ -911,7 +915,7 @@ export class WecomHarnessBridge {
911
915
  } catch (error) {
912
916
  if (error?.code === 'turn-stopped') {
913
917
  if (streamStarted && streamId) {
914
- await this.#client.replyStream(frame, streamId, '已停止。', true)
918
+ await this.#client.replyStream(frame, streamId, t('已停止。'), true)
915
919
  .catch(() => undefined);
916
920
  }
917
921
  await this.#state.markSeen(messageId);
@@ -922,7 +926,7 @@ export class WecomHarnessBridge {
922
926
  this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
923
927
  const errorText = inboundFileUserMessage(error)
924
928
  ?? imagePromptUserMessage(error)
925
- ?? '消息处理失败,请稍后重试。';
929
+ ?? t('消息处理失败,请稍后重试。');
926
930
  try {
927
931
  if (streamStarted && streamId) {
928
932
  await this.#client.replyStream(frame, streamId, errorText, true);
@@ -958,7 +962,7 @@ export class WecomHarnessBridge {
958
962
 
959
963
  const text = nonEmptyString(interactionReplyText(frame));
960
964
  if (!text) {
961
- await this.#sendImmediate(frame, chatId, '请用文字回答当前问题。')
965
+ await this.#sendImmediate(frame, chatId, t('请用文字回答当前问题。'))
962
966
  .catch(() => undefined);
963
967
  return;
964
968
  }
@@ -966,7 +970,7 @@ export class WecomHarnessBridge {
966
970
  const pending = this.#pendingInteractions.get(key);
967
971
  if (!pending || pending !== expected || pending.submitting) {
968
972
  if (claimed && (!pending || pending !== expected)) {
969
- await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
973
+ await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
970
974
  .catch(() => undefined);
971
975
  return;
972
976
  }
@@ -987,7 +991,7 @@ export class WecomHarnessBridge {
987
991
  try {
988
992
  await this.#presentInteraction(pending);
989
993
  } catch {
990
- this.#status.lastError = '企业微信交互问题发送失败。';
994
+ this.#status.lastError = t('企业微信交互问题发送失败。');
991
995
  this.#logger.error?.('[dsh-im:wecom] failed to retry an interaction question');
992
996
  pending.interaction.reconnect?.();
993
997
  return;
@@ -995,7 +999,7 @@ export class WecomHarnessBridge {
995
999
  const presentedPending = this.#pendingInteractions.get(key);
996
1000
  if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
997
1001
  if (claimed && (!presentedPending || presentedPending !== expected)) {
998
- await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
1002
+ await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
999
1003
  .catch(() => undefined);
1000
1004
  return;
1001
1005
  }
@@ -1018,7 +1022,7 @@ export class WecomHarnessBridge {
1018
1022
  try {
1019
1023
  await this.#presentInteraction(pending);
1020
1024
  } catch {
1021
- this.#status.lastError = '企业微信交互问题发送失败。';
1025
+ this.#status.lastError = t('企业微信交互问题发送失败。');
1022
1026
  this.#logger.error?.('[dsh-im:wecom] failed to send the next interaction question');
1023
1027
  pending.interaction.reconnect?.();
1024
1028
  }
@@ -1042,7 +1046,7 @@ export class WecomHarnessBridge {
1042
1046
  if (this.#pendingInteractions.get(key) === pending) {
1043
1047
  this.#clearPendingInteraction(key, pending.interactionId);
1044
1048
  }
1045
- await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
1049
+ await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。'))
1046
1050
  .catch(() => undefined);
1047
1051
  return;
1048
1052
  }
@@ -1050,9 +1054,9 @@ export class WecomHarnessBridge {
1050
1054
  pending.submitting = false;
1051
1055
  pending.answers.pop();
1052
1056
  pending.index -= 1;
1053
- this.#status.lastError = '回答提交失败。';
1057
+ this.#status.lastError = t('回答提交失败。');
1054
1058
  this.#logger.error?.('[dsh-im:wecom] failed to answer a Harness interaction');
1055
- await this.#sendImmediate(frame, chatId, '回答提交失败,请重新发送当前问题的答案。')
1059
+ await this.#sendImmediate(frame, chatId, t('回答提交失败,请重新发送当前问题的答案。'))
1056
1060
  .catch(() => undefined);
1057
1061
  }
1058
1062
  }
@@ -1097,7 +1101,7 @@ export class WecomHarnessBridge {
1097
1101
  });
1098
1102
  await this.#sendActive(
1099
1103
  chatId,
1100
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
1104
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
1101
1105
  ).catch(() => undefined);
1102
1106
  return;
1103
1107
  }
@@ -1185,7 +1189,7 @@ export class WecomHarnessBridge {
1185
1189
  await this.#state.markSeen(messageId);
1186
1190
  this.#status.messagesReceived += 1;
1187
1191
  this.#status.lastMessageAt = new Date().toISOString();
1188
- await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
1192
+ await this.#sendImmediate(frame, chatId, t('这个问题已在其他客户端处理,无需再次回答。')).catch(() => undefined);
1189
1193
  }
1190
1194
 
1191
1195
  #takePendingInteraction(key, interactionId) {
@@ -5,6 +5,7 @@ import {
5
5
  connectionTestMessage,
6
6
  connectionTestTargetUnavailable,
7
7
  } from '../shared/connection-test.mjs';
8
+ import { t } from '../shared/i18n.mjs';
8
9
 
9
10
  const ACTIVE_ATTEMPT_STATES = new Set(['pending', 'connecting']);
10
11
  const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'failed', 'cancelled', 'expired']);
@@ -82,14 +83,14 @@ export class WecomController {
82
83
  if (this.#closed || existing?.ready || existing?.wecomConnectionState === 'connecting') return;
83
84
  const secret = await this.#resolveSecret(config.secretRef);
84
85
  if (!secret) {
85
- this.#errors.set(config.botId, safeError('missing-secret', '企业微信机器人凭据缺失,请移除后重新扫码。'));
86
+ this.#errors.set(config.botId, safeError('missing-secret', t('企业微信机器人凭据缺失,请移除后重新扫码。')));
86
87
  return;
87
88
  }
88
89
  try {
89
90
  await this.#startRuntime(config, secret);
90
91
  this.#errors.delete(config.botId);
91
92
  } catch (error) {
92
- this.#errors.set(config.botId, safeError('connection-failed', '企业微信连接未就绪,插件会自动重试。'));
93
+ this.#errors.set(config.botId, safeError('connection-failed', t('企业微信连接未就绪,插件会自动重试。')));
93
94
  this.#logger.warn?.(`[dsh-im:wecom] bot ${config.botId} failed to initialize`);
94
95
  } finally {
95
96
  this.#touch();
@@ -134,8 +135,8 @@ export class WecomController {
134
135
  } catch (error) {
135
136
  record.state = record.controller.signal.aborted ? 'cancelled' : 'failed';
136
137
  record.error = record.controller.signal.aborted
137
- ? safeError('cancelled', '扫码绑定已取消。')
138
- : safeError('qr-start-failed', '无法生成企业微信二维码,请稍后重试。');
138
+ ? safeError('cancelled', t('扫码绑定已取消。'))
139
+ : safeError('qr-start-failed', t('无法生成企业微信二维码,请稍后重试。'));
139
140
  this.#finishAttempt(record);
140
141
  throw error;
141
142
  }
@@ -150,7 +151,7 @@ export class WecomController {
150
151
  }
151
152
  if (Date.now() >= record.expiresAt) {
152
153
  record.state = 'expired';
153
- record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
154
+ record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
154
155
  record.controller.abort();
155
156
  this.#finishAttempt(record);
156
157
  return publicAttempt(record);
@@ -200,7 +201,7 @@ export class WecomController {
200
201
  } catch {
201
202
  this.#errors.set(
202
203
  identity.botId,
203
- safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'),
204
+ safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')),
204
205
  );
205
206
  this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} credential connection failed`);
206
207
  }
@@ -216,7 +217,7 @@ export class WecomController {
216
217
  record.controller.abort();
217
218
  await Promise.allSettled([record.polling, record.transition].filter(Boolean));
218
219
  if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
219
- record.error ??= safeError('cancelled', '扫码绑定已取消。');
220
+ record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
220
221
  this.#finishAttempt(record);
221
222
  }
222
223
  return publicAttempt(record);
@@ -232,7 +233,7 @@ export class WecomController {
232
233
  await this.#startRuntime(config, secret);
233
234
  this.#errors.delete(botId);
234
235
  } catch (error) {
235
- this.#errors.set(botId, safeError('connection-failed', '企业微信连接仍未就绪,请稍后重试。'));
236
+ this.#errors.set(botId, safeError('connection-failed', t('企业微信连接仍未就绪,请稍后重试。')));
236
237
  throw error;
237
238
  } finally {
238
239
  this.#touch();
@@ -247,10 +248,10 @@ export class WecomController {
247
248
  return this.#withBotTransition(botId, async () => {
248
249
  const runtime = this.#runtimes.get(botId);
249
250
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
250
- throw connectionTestTargetUnavailable('企业微信机器人');
251
+ throw connectionTestTargetUnavailable(t('企业微信机器人'));
251
252
  }
252
253
  return runtime.sendConnectionTest(connectionTestMessage(
253
- `企业微信机器人(${maskWecomBotId(config.remoteBotId)})`,
254
+ t('企业微信机器人({botId})', { botId: maskWecomBotId(config.remoteBotId) }),
254
255
  ));
255
256
  });
256
257
  }
@@ -295,11 +296,11 @@ export class WecomController {
295
296
  state,
296
297
  connected,
297
298
  configured: true,
298
- bot: { name: '企业微信机器人', appIdMasked: maskWecomBotId(config.remoteBotId) },
299
+ bot: { name: t('企业微信机器人'), appIdMasked: maskWecomBotId(config.remoteBotId) },
299
300
  health: {
300
301
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
301
- summary: connected ? '企业微信 WebSocket 长连接运行正常'
302
- : state === 'error' ? '企业微信连接未就绪,插件会自动重试' : '企业微信连接当前离线',
302
+ summary: connected ? t('企业微信 WebSocket 长连接运行正常')
303
+ : state === 'error' ? t('企业微信连接未就绪,插件会自动重试') : t('企业微信连接当前离线'),
303
304
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
304
305
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
305
306
  },
@@ -341,13 +342,13 @@ export class WecomController {
341
342
  if (result.status === 'waiting') return;
342
343
  if (result.status === 'expired') {
343
344
  record.state = 'expired';
344
- record.error = safeError('expired', '企业微信二维码已过期,请重新生成。');
345
+ record.error = safeError('expired', t('企业微信二维码已过期,请重新生成。'));
345
346
  this.#finishAttempt(record);
346
347
  return;
347
348
  }
348
349
  if (result.status !== 'success') {
349
350
  record.state = 'failed';
350
- record.error = safeError('qr-connect-failed', '企业微信扫码没有完成,请重新生成二维码。');
351
+ record.error = safeError('qr-connect-failed', t('企业微信扫码没有完成,请重新生成二维码。'));
351
352
  this.#finishAttempt(record);
352
353
  return;
353
354
  }
@@ -362,7 +363,7 @@ export class WecomController {
362
363
  } catch (error) {
363
364
  if (record.controller.signal.aborted) return;
364
365
  record.state = 'failed';
365
- record.error = safeError('qr-connect-failed', '企业微信扫码服务暂时不可用,请重新生成二维码。');
366
+ record.error = safeError('qr-connect-failed', t('企业微信扫码服务暂时不可用,请重新生成二维码。'));
366
367
  this.#logger.warn?.('[dsh-im:wecom] QR polling failed');
367
368
  this.#finishAttempt(record);
368
369
  }
@@ -379,10 +380,10 @@ export class WecomController {
379
380
  } catch (error) {
380
381
  if (record.controller.signal.aborted) {
381
382
  record.state = 'cancelled';
382
- record.error = safeError('cancelled', '扫码绑定已取消。');
383
+ record.error = safeError('cancelled', t('扫码绑定已取消。'));
383
384
  } else {
384
385
  record.state = 'failed';
385
- record.error = safeError('activation-failed', '企业微信已授权,但无法安全保存接入配置。');
386
+ record.error = safeError('activation-failed', t('企业微信已授权,但无法安全保存接入配置。'));
386
387
  this.#logger.error?.('[dsh-im:wecom] provisioning failed');
387
388
  }
388
389
  } finally {
@@ -429,7 +430,7 @@ export class WecomController {
429
430
  await this.#restoreCredential(identity.secretRef, previousSecret);
430
431
  throw error;
431
432
  }
432
- this.#errors.set(identity.botId, safeError('connection-failed', '企业微信机器人已绑定,消息连接暂未就绪。'));
433
+ this.#errors.set(identity.botId, safeError('connection-failed', t('企业微信机器人已绑定,消息连接暂未就绪。')));
433
434
  this.#logger.warn?.(`[dsh-im:wecom] bot ${identity.botId} activation connection failed`);
434
435
  }
435
436
  this.#touch();
@@ -2,6 +2,7 @@ import { WSAuthFailureError, WSClient, WSReconnectExhaustedError } from '@wecom/
2
2
 
3
3
  import { createWecomBridgeStatus, WecomHarnessBridge } from './wecom-bridge.mjs';
4
4
  import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
5
+ import { t } from '../shared/i18n.mjs';
5
6
 
6
7
  function timeoutError() {
7
8
  const error = new Error('Enterprise WeChat WebSocket authentication timed out');
@@ -202,7 +203,7 @@ export class WecomRuntime {
202
203
  return sendRememberedConnectionTest({
203
204
  state: this.#state,
204
205
  text,
205
- channelLabel: '企业微信机器人',
206
+ channelLabel: t('企业微信机器人'),
206
207
  send: async ({ chatId }, content) => {
207
208
  if (!this.#status.ready || !this.#client) {
208
209
  throw new Error('Enterprise WeChat runtime is not connected');
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
4
 
5
5
  import { normalizeWeixinApiBaseUrl } from './weixin-api.mjs';
6
+ import { t } from '../shared/i18n.mjs';
6
7
 
7
8
  const EMPTY_DOCUMENT = Object.freeze({ version: 1, accounts: Object.freeze([]) });
8
9
 
@@ -32,7 +33,7 @@ export function deriveWeixinBotIdentity(accountId) {
32
33
 
33
34
  export function maskWeixinAccountId(accountId) {
34
35
  const value = cleanString(accountId) ?? '';
35
- if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : '微信机器人';
36
+ if (value.length <= 10) return value ? `${value.slice(0, 3)}•••` : t('微信机器人');
36
37
  return `${value.slice(0, 6)}••••${value.slice(-4)}`;
37
38
  }
38
39