@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
@@ -40,8 +40,11 @@ import {
40
40
  providerMessageIdsFor,
41
41
  } from '../shared/semantic/delivery.mjs';
42
42
  import { sendMarkdownReply } from './markdown-reply.mjs';
43
+ import { t } from '../shared/i18n.mjs';
43
44
 
44
- const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
45
+ function interactionResolvedText() {
46
+ return t('这个问题已在其他客户端处理,无需再次回答。');
47
+ }
45
48
  const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
46
49
 
47
50
  export const QQ_IMAGE_HOSTS = Object.freeze([
@@ -55,28 +58,30 @@ export const QQ_IMAGE_HOSTS = Object.freeze([
55
58
 
56
59
  const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
57
60
 
58
- const HELP_TEXT = [
59
- 'QQ 机器人已连接 DeepSeek Harness。',
60
- '',
61
- '直接发送文字、图片或文件即可继续当前会话。',
62
- '/new 开启一个全新会话',
63
- '/compact 压缩当前会话的较早上下文',
64
- '/workspace 工作区绝对路径 切换工作区',
65
- '/workspacelist 列出工作区绝对路径',
66
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
67
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
68
- '/models 按序号列出所有可用模型',
69
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
70
- '示例:先发 /models,再发 /model 2',
71
- '/presetlist 按序号列出可用 Agent Preset',
72
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
73
- '纯数字 ID:/preset id:<ID>',
74
- '/preset --default 跟随 Host 默认',
75
- '/stop 停止当前任务',
76
- '/steer 补充指令 纠偏当前任务',
77
- '/status 检查连接状态',
78
- '/help 显示本帮助',
79
- ].join('\n');
61
+ function helpText() {
62
+ return [
63
+ t('QQ 机器人已连接 DeepSeek Harness。'),
64
+ '',
65
+ t('直接发送文字、图片或文件即可继续当前会话。'),
66
+ t('/new 开启一个全新会话'),
67
+ t('/compact 压缩当前会话的较早上下文'),
68
+ t('/workspace 工作区绝对路径 切换工作区'),
69
+ t('/workspacelist 列出工作区绝对路径'),
70
+ t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
71
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
72
+ t('/models 按序号列出所有可用模型'),
73
+ t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
74
+ t('示例:先发 /models,再发 /model 2'),
75
+ t('/presetlist 按序号列出可用 Agent Preset'),
76
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
77
+ t('纯数字 ID:/preset id:<ID>'),
78
+ t('/preset --default 跟随 Host 默认'),
79
+ t('/stop 停止当前任务'),
80
+ t('/steer 补充指令 纠偏当前任务'),
81
+ t('/status 检查连接状态'),
82
+ t('/help 显示本帮助'),
83
+ ].join('\n');
84
+ }
80
85
 
81
86
  function conversationKey(message) {
82
87
  return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
@@ -167,35 +172,35 @@ function nonEmptyString(value) {
167
172
  }
168
173
 
169
174
  function artifactFailureText(fileName, error) {
170
- const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
175
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
171
176
  if (error?.name === 'UploadDailyLimitExceededError') {
172
- return `结果文件「${name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。`;
177
+ return t('结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。', { name });
173
178
  }
174
179
  switch (error?.code) {
175
180
  case 'artifact-delivery-uncertain':
176
- return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
181
+ return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
177
182
  case 'artifact-permission-required':
178
- return `结果文件「${name}」已生成,但当前 QQ 机器人没有文件消息权限。`;
183
+ return t('结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。', { name });
179
184
  case 'artifact-too-large':
180
- return `结果文件「${name}」超过当前 QQ 机器人可发送的文件大小,未发送。`;
185
+ return t('结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。', { name });
181
186
  case 'artifact-empty':
182
- return `结果文件「${name}」为空,QQ 不允许发送空文件。`;
187
+ return t('结果文件「{name}」为空,QQ 不允许发送空文件。', { name });
183
188
  case 'artifact-changed':
184
189
  case 'artifact-invalid':
185
190
  case 'artifact-unavailable':
186
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
191
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
187
192
  case 'artifact-rate-limited':
188
- return `结果文件「${name}」暂时被 QQ 限流,未能发送,请稍后重试。`;
193
+ return t('结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。', { name });
189
194
  case 'artifact-provider-rejected':
190
- return `结果文件「${name}」已生成,但 QQ 拒绝了该文件或文件消息。`;
195
+ return t('结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。', { name });
191
196
  default:
192
- return `结果文件「${name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。`;
197
+ return t('结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。', { name });
193
198
  }
194
199
  }
195
200
 
196
201
  function answerTextForDelivery(answer, artifacts) {
197
202
  if (typeof answer === 'string' && answer.trim()) return answer;
198
- return artifacts.length > 0 ? '结果文件已生成。' : answer;
203
+ return artifacts.length > 0 ? t('结果文件已生成。') : answer;
199
204
  }
200
205
 
201
206
  function qqArtifactError(error, { dispatched = false } = {}) {
@@ -420,7 +425,7 @@ export class QqHarnessBridge {
420
425
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
421
426
  this.#status.lastError = error?.message ?? String(error);
422
427
  this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
423
- return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
428
+ return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
424
429
  .catch(() => undefined);
425
430
  }).finally(() => {
426
431
  this.#acceptedMessageIds.delete(messageId);
@@ -609,25 +614,25 @@ export class QqHarnessBridge {
609
614
  let stream = null;
610
615
  try {
611
616
  if (!text && !hasImages && !hasFiles) {
612
- await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
617
+ await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
613
618
  await this.#state.markSeen(messageId);
614
619
  return;
615
620
  }
616
621
  const command = text.toLowerCase();
617
622
  if (!hasImages && !hasFiles && command === '/help') {
618
- await this.#bot.sendText(target, HELP_TEXT);
623
+ await this.#bot.sendText(target, helpText());
619
624
  await this.#state.markSeen(messageId);
620
625
  return;
621
626
  }
622
627
  if (!hasImages && !hasFiles && command === '/status') {
623
628
  await this.#harness.ensureRunning({ signal: this.#signal });
624
- await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
629
+ await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
625
630
  await this.#state.markSeen(messageId);
626
631
  return;
627
632
  }
628
633
  if (!hasImages && !hasFiles && command === '/new') {
629
634
  await this.#state.clearSession(key);
630
- await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
635
+ await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
631
636
  await this.#state.markSeen(messageId);
632
637
  return;
633
638
  }
@@ -771,7 +776,7 @@ export class QqHarnessBridge {
771
776
  this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
772
777
  }
773
778
  try {
774
- await this.#bot.sendText(target, '已停止。');
779
+ await this.#bot.sendText(target, t('已停止。'));
775
780
  } catch (sendError) {
776
781
  this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
777
782
  }
@@ -791,7 +796,7 @@ export class QqHarnessBridge {
791
796
  target,
792
797
  inboundFileUserMessage(error)
793
798
  ?? imagePromptUserMessage(error)
794
- ?? '消息处理失败,请稍后重试。',
799
+ ?? t('消息处理失败,请稍后重试。'),
795
800
  );
796
801
  await this.#state.markSeen(messageId);
797
802
  } catch (sendError) {
@@ -818,14 +823,14 @@ export class QqHarnessBridge {
818
823
  if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
819
824
  const text = nonEmptyString(safeText(message));
820
825
  if (!text || hasQqImageAttachments(message) || hasQqFileAttachments(message)) {
821
- await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
826
+ await this.#bot.sendText(message.replyTarget, t('请用文字回答当前问题。'));
822
827
  return;
823
828
  }
824
829
 
825
830
  const pending = this.#pendingInteractions.get(key);
826
831
  if (!pending || pending !== expected || pending.submitting) {
827
832
  if (claimed && (!pending || pending !== expected)) {
828
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
833
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText());
829
834
  return;
830
835
  }
831
836
  return this.#enqueueMessage(message, messageId, key, {
@@ -838,7 +843,7 @@ export class QqHarnessBridge {
838
843
  try {
839
844
  await this.#presentInteraction(pending);
840
845
  } catch {
841
- this.#status.lastError = 'QQ 交互问题发送失败。';
846
+ this.#status.lastError = t('QQ 交互问题发送失败。');
842
847
  this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
843
848
  pending.interaction.reconnect?.();
844
849
  return;
@@ -846,7 +851,7 @@ export class QqHarnessBridge {
846
851
  const presentedPending = this.#pendingInteractions.get(key);
847
852
  if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
848
853
  if (claimed && (!presentedPending || presentedPending !== expected)) {
849
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT)
854
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText())
850
855
  .catch(() => undefined);
851
856
  return;
852
857
  }
@@ -869,7 +874,7 @@ export class QqHarnessBridge {
869
874
  try {
870
875
  await this.#presentInteraction(pending);
871
876
  } catch {
872
- this.#status.lastError = 'QQ 交互问题发送失败。';
877
+ this.#status.lastError = t('QQ 交互问题发送失败。');
873
878
  this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
874
879
  pending.interaction.reconnect?.();
875
880
  }
@@ -891,16 +896,16 @@ export class QqHarnessBridge {
891
896
  if (this.#signal?.aborted) return;
892
897
  if (error?.code === 'interaction-not-pending') {
893
898
  this.#clearPendingInteraction(key, pending.interactionId);
894
- await this.#bot.sendText(pending.target, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
899
+ await this.#bot.sendText(pending.target, interactionResolvedText()).catch(() => undefined);
895
900
  return;
896
901
  }
897
902
  if (this.#pendingInteractions.get(key) !== pending) return;
898
903
  pending.submitting = false;
899
904
  pending.answers.pop();
900
905
  pending.index -= 1;
901
- this.#status.lastError = '回答提交失败。';
906
+ this.#status.lastError = t('回答提交失败。');
902
907
  this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
903
- await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
908
+ await this.#bot.sendText(pending.target, t('回答提交失败,请重新发送当前问题的答案。'))
904
909
  .catch(() => undefined);
905
910
  }
906
911
  }
@@ -945,7 +950,7 @@ export class QqHarnessBridge {
945
950
  });
946
951
  await this.#bot.sendText(
947
952
  target,
948
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
953
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
949
954
  ).catch(() => undefined);
950
955
  return;
951
956
  }
@@ -1030,7 +1035,7 @@ export class QqHarnessBridge {
1030
1035
  await this.#state.markSeen(messageId);
1031
1036
  this.#status.messagesReceived += 1;
1032
1037
  this.#status.lastMessageAt = new Date().toISOString();
1033
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
1038
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText()).catch(() => undefined);
1034
1039
  }
1035
1040
 
1036
1041
  #takePendingInteraction(key, interactionId) {
@@ -1072,7 +1077,7 @@ export class QqHarnessBridge {
1072
1077
  if (!this.#state.hasSeen(messageId)) {
1073
1078
  await this.#state.markSeen(messageId).catch(() => undefined);
1074
1079
  }
1075
- await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
1080
+ await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
1076
1081
  .catch(() => undefined);
1077
1082
  }
1078
1083
  }
@@ -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 { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
5
6
 
6
7
  const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
@@ -77,14 +78,14 @@ export class QqController {
77
78
  if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
78
79
  const appSecret = await this.#resolveSecret(config.secretRef);
79
80
  if (!appSecret) {
80
- this.#errors.set(config.botId, safeError('missing-secret', 'QQ 机器人凭据缺失,请移除后重新扫码。'));
81
+ this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
81
82
  return;
82
83
  }
83
84
  try {
84
85
  await this.#startRuntime(config, appSecret);
85
86
  this.#errors.delete(config.botId);
86
87
  } catch (error) {
87
- this.#errors.set(config.botId, safeError('connection-failed', 'QQ 连接未就绪,插件会自动重试。'));
88
+ this.#errors.set(config.botId, safeError('connection-failed', t('QQ 连接未就绪,插件会自动重试。')));
88
89
  this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error);
89
90
  } finally {
90
91
  this.#touch();
@@ -147,7 +148,7 @@ export class QqController {
147
148
  onFailure: (error) => {
148
149
  if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES.has(record.state)) return;
149
150
  record.state = 'failed';
150
- record.error = safeError('qr-connect-failed', 'QQ 扫码服务暂时不可用,请重新生成二维码。');
151
+ record.error = safeError('qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
151
152
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
152
153
  this.#touch();
153
154
  firstQrReject(error);
@@ -158,10 +159,10 @@ export class QqController {
158
159
  } catch (error) {
159
160
  if (record.controller.signal.aborted) {
160
161
  record.state = 'cancelled';
161
- record.error = safeError('cancelled', '扫码绑定已取消。');
162
+ record.error = safeError('cancelled', t('扫码绑定已取消。'));
162
163
  } else if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
163
164
  record.state = 'failed';
164
- record.error = safeError('qr-start-failed', '无法生成 QQ 二维码,请稍后重试。');
165
+ record.error = safeError('qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
165
166
  }
166
167
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
167
168
  this.#touch();
@@ -207,7 +208,7 @@ export class QqController {
207
208
  await this.#startRuntime(config, normalizedSecret);
208
209
  this.#errors.delete(identity.botId);
209
210
  } catch (error) {
210
- this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
211
+ this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
211
212
  this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error);
212
213
  }
213
214
  this.#touch();
@@ -223,7 +224,7 @@ export class QqController {
223
224
  record.dispose?.();
224
225
  await record.task?.catch(() => undefined);
225
226
  if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
226
- record.error ??= safeError('cancelled', '扫码绑定已取消。');
227
+ record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
227
228
  }
228
229
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
229
230
  this.#touch();
@@ -240,7 +241,7 @@ export class QqController {
240
241
  await this.#startRuntime(config, secret);
241
242
  this.#errors.delete(botId);
242
243
  } catch (error) {
243
- this.#errors.set(botId, safeError('connection-failed', 'QQ 连接仍未就绪,请稍后重试。'));
244
+ this.#errors.set(botId, safeError('connection-failed', t('QQ 连接仍未就绪,请稍后重试。')));
244
245
  throw error;
245
246
  } finally {
246
247
  this.#touch();
@@ -255,12 +256,12 @@ export class QqController {
255
256
  return this.#withBotTransition(botId, async () => {
256
257
  const runtime = this.#runtimes.get(botId);
257
258
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
258
- const error = new Error('QQ机器人尚未连接');
259
+ const error = new Error(t('QQ机器人尚未连接'));
259
260
  error.code = 'test-target-unavailable';
260
261
  throw error;
261
262
  }
262
263
  await runtime.sendConnectionTest(connectionTestMessage(
263
- `QQ 机器人(${maskQqAppId(config.appId)})`,
264
+ t('QQ 机器人({appId})', { appId: maskQqAppId(config.appId) }),
264
265
  ));
265
266
  return { sent: true };
266
267
  });
@@ -306,11 +307,11 @@ export class QqController {
306
307
  state,
307
308
  connected,
308
309
  configured: true,
309
- bot: { name: 'QQ机器人', appIdMasked: maskQqAppId(config.appId) },
310
+ bot: { name: t('QQ机器人'), appIdMasked: maskQqAppId(config.appId) },
310
311
  health: {
311
312
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
312
- summary: connected ? 'QQ WebSocket 长连接运行正常'
313
- : state === 'error' ? 'QQ 连接未就绪,插件会自动重试' : 'QQ 连接当前离线',
313
+ summary: connected ? t('QQ WebSocket 长连接运行正常')
314
+ : state === 'error' ? t('QQ 连接未就绪,插件会自动重试') : t('QQ 连接当前离线'),
314
315
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
315
316
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
316
317
  },
@@ -364,10 +365,10 @@ export class QqController {
364
365
  } catch (error) {
365
366
  if (record.controller.signal.aborted) {
366
367
  record.state = 'cancelled';
367
- record.error = safeError('cancelled', '扫码绑定已取消。');
368
+ record.error = safeError('cancelled', t('扫码绑定已取消。'));
368
369
  } else {
369
370
  record.state = 'failed';
370
- record.error = safeError('activation-failed', 'QQ 已授权,但无法安全保存接入配置。');
371
+ record.error = safeError('activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
371
372
  this.#logger.error?.('[dsh-im:qq] provisioning failed:', error);
372
373
  }
373
374
  } finally {
@@ -417,7 +418,7 @@ export class QqController {
417
418
  await this.#restoreCredential(identity.secretRef, previousSecret);
418
419
  throw error;
419
420
  }
420
- this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
421
+ this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
421
422
  this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error);
422
423
  }
423
424
  this.#touch();
@@ -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
  import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
8
9
 
9
10
  function timeoutError() {
@@ -73,7 +74,7 @@ export class QqRuntime {
73
74
 
74
75
  async sendConnectionTest(text) {
75
76
  if (!this.#status.ready || !this.#bot) {
76
- throw connectionTestTargetUnavailable('QQ机器人');
77
+ throw connectionTestTargetUnavailable(t('QQ机器人'));
77
78
  }
78
79
  const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
79
80
  ? this.#config.ownerUserOpenid.trim()
@@ -88,7 +89,7 @@ export class QqRuntime {
88
89
  : (ownerUserOpenid && ownerUserOpenid !== '*'
89
90
  ? { scope: 'c2c', targetId: ownerUserOpenid }
90
91
  : null);
91
- if (!target) throw connectionTestTargetUnavailable('QQ机器人');
92
+ if (!target) throw connectionTestTargetUnavailable(t('QQ机器人'));
92
93
  await this.#bot.sendText(target, text);
93
94
  return { sent: true };
94
95
  }
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  /** Matches DeepSeek Harness agent-preset directory ids. */
2
4
  export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
3
5
 
@@ -17,7 +19,7 @@ export function validateAgentPresetId(value) {
17
19
  if (value == null || value === '') return null;
18
20
  const id = normalizeAgentPresetId(value);
19
21
  if (!id) {
20
- const error = new Error('Agent Preset 无效。');
22
+ const error = new Error(t('Agent Preset 无效。'));
21
23
  error.code = 'agent-preset-invalid';
22
24
  throw error;
23
25
  }
@@ -1,3 +1,4 @@
1
+ import { t } from './i18n.mjs';
1
2
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
2
3
 
3
4
  const COMPACT_COMMAND = /^\/compact(?=$|\s)([\s\S]*)$/i;
@@ -41,27 +42,27 @@ function compactResultText(result) {
41
42
  const text = result.text?.trim() ?? '';
42
43
  const compacted = /^Compacted (\d+) history items \(~(\d+) tokens\)\.$/u.exec(text);
43
44
  if (compacted) {
44
- return `已压缩 ${compacted[1]} 条历史记录(约 ${compacted[2]} 个 token)。`;
45
+ return t('已压缩 {count} 条历史记录(约 {tokens} 个 token)。', { count: compacted[1], tokens: compacted[2] });
45
46
  }
46
- if (COMPACT_RESULT_TEXT.has(text)) return COMPACT_RESULT_TEXT.get(text);
47
+ if (COMPACT_RESULT_TEXT.has(text)) return t(COMPACT_RESULT_TEXT.get(text));
47
48
  if (text) return text;
48
- return result.kind === 'success' ? '上下文压缩完成。' : '上下文压缩失败。';
49
+ return result.kind === 'success' ? t('上下文压缩完成。') : t('上下文压缩失败。');
49
50
  }
50
51
 
51
52
  function compactErrorMessage(error) {
52
53
  const code = error?.code ?? error?.failure?.code;
53
54
  if (code === 'session-not-found') {
54
- return '当前聊天绑定的会话已不存在,请发送新消息开启会话。';
55
+ return t('当前聊天绑定的会话已不存在,请发送新消息开启会话。');
55
56
  }
56
- if (code === 'agent-busy') return '当前会话正在生成回复,请稍后重试。';
57
- if (code === 'cancelled' || error?.name === 'AbortError') return '上下文压缩已取消。';
57
+ if (code === 'agent-busy') return t('当前会话正在生成回复,请稍后重试。');
58
+ if (code === 'cancelled' || error?.name === 'AbortError') return t('上下文压缩已取消。');
58
59
  if (code === WORKSPACE_SESSION_STALE || code === 'workspace-bot-not-found') {
59
- return '工作区或机器人状态已发生变化,请重试。';
60
+ return t('工作区或机器人状态已发生变化,请重试。');
60
61
  }
61
62
  if (code === 'commands-unavailable') {
62
- return '当前 Harness 暂不支持从机器人执行上下文压缩。';
63
+ return t('当前 Harness 暂不支持从机器人执行上下文压缩。');
63
64
  }
64
- return '上下文压缩失败,请稍后重试。';
65
+ return t('上下文压缩失败,请稍后重试。');
65
66
  }
66
67
 
67
68
  /**
@@ -72,21 +73,21 @@ export async function runCompactCommand(text, harness, state, conversationKey, o
72
73
  if (typeof text !== 'string') return null;
73
74
  const match = COMPACT_COMMAND.exec(text.trim());
74
75
  if (!match) return null;
75
- if (match[1].trim()) return commandResult(COMPACT_USAGE);
76
+ if (match[1].trim()) return commandResult(t(COMPACT_USAGE));
76
77
  if (typeof state?.sessionFor !== 'function') {
77
- return commandResult('当前机器人没有可用的会话状态。');
78
+ return commandResult(t('当前机器人没有可用的会话状态。'));
78
79
  }
79
80
  const sessionId = state.sessionFor(conversationKey);
80
81
  if (typeof sessionId !== 'string' || !sessionId) {
81
- return commandResult('当前聊天还没有可压缩的会话,请先发送一条消息。');
82
+ return commandResult(t('当前聊天还没有可压缩的会话,请先发送一条消息。'));
82
83
  }
83
84
  if (typeof harness?.executeCommand !== 'function') {
84
- return commandResult('当前机器人暂不支持上下文压缩。');
85
+ return commandResult(t('当前机器人暂不支持上下文压缩。'));
85
86
  }
86
87
  try {
87
88
  const execution = await harness.executeCommand(sessionId, '/compact', options);
88
89
  if (execution === undefined) {
89
- return commandResult('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。');
90
+ return commandResult(t('当前 Harness 未注册 /compact 命令,请确认上下文压缩组件已启用。'));
90
91
  }
91
92
  return commandResult(compactResultText(execution?.result));
92
93
  } catch (error) {
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  const targets = new WeakMap();
2
4
 
3
5
  export const CONNECTION_TEST_STATE_IDENTITY = Symbol('dsh-im.connection-test-state-identity');
@@ -25,13 +27,13 @@ export function connectionTestTarget(state) {
25
27
  return target ? structuredClone(target) : null;
26
28
  }
27
29
 
28
- export function connectionTestMessage(botName, channelLabel = '机器人') {
30
+ export function connectionTestMessage(botName, channelLabel = t('机器人')) {
29
31
  const name = cleanText(botName) ?? channelLabel;
30
- return `✅ DeepSeek Harness 连接测试成功\n这条消息由插件页面中的“${name}”机器人卡片发出。`;
32
+ return `${t('✅ DeepSeek Harness 连接测试成功')}\n${t('这条消息由插件页面中的“{name}”机器人卡片发出。', { name })}`;
31
33
  }
32
34
 
33
- export function connectionTestTargetUnavailable(channelLabel = '机器人') {
34
- const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
35
+ export function connectionTestTargetUnavailable(channelLabel = t('机器人')) {
36
+ const error = new Error(t('{channelLabel}尚未收到可用于测试的私聊消息。', { channelLabel }));
35
37
  error.code = 'test-target-unavailable';
36
38
  return error;
37
39
  }
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  const CONTROL_COMMAND = /^\/(?:stop|steer)(?=$|\s)/iu;
2
4
  const STOP_COMMAND = /^\/stop(?=$|\s)/iu;
3
5
  const STOP_USAGE = '用法:/stop(不带参数)';
@@ -40,35 +42,35 @@ export async function runControlCommand(text, harness, state, key, {
40
42
  const command = text.trim();
41
43
  const stop = STOP_COMMAND.test(command);
42
44
 
43
- if (hasImages) return commandResult(TEXT_ONLY);
45
+ if (hasImages) return commandResult(t(TEXT_ONLY));
44
46
 
45
47
  if (stop) {
46
- if (!/^\/stop$/iu.test(command)) return commandResult(STOP_USAGE);
48
+ if (!/^\/stop$/iu.test(command)) return commandResult(t(STOP_USAGE));
47
49
  const session = boundSession(harness, state, key);
48
- if (!session) return commandResult('当前聊天没有正在运行的任务。');
50
+ if (!session) return commandResult(t('当前聊天没有正在运行的任务。'));
49
51
  if (typeof session.stopActiveTurn !== 'function') {
50
52
  throw new TypeError('Harness session does not support stopping active turns');
51
53
  }
52
54
  const stopped = await session.stopActiveTurn(control, requestOptions(signal));
53
55
  return stopped
54
- ? commandResult('已请求停止当前任务。', { stopped: true })
55
- : commandResult('当前聊天没有正在运行的任务。');
56
+ ? commandResult(t('已请求停止当前任务。'), { stopped: true })
57
+ : commandResult(t('当前聊天没有正在运行的任务。'));
56
58
  }
57
59
 
58
60
  const match = /^\/steer(?:\s+([\s\S]*))?$/iu.exec(command);
59
61
  const instruction = match?.[1]?.trim() ?? '';
60
- if (!instruction) return commandResult(STEER_USAGE);
62
+ if (!instruction) return commandResult(t(STEER_USAGE));
61
63
  if (pendingInteraction) {
62
64
  return commandResult([
63
- '当前任务正在等待你的回答或审批。',
65
+ t('当前任务正在等待你的回答或审批。'),
64
66
  '',
65
- '请先处理当前请求,或者发送 /stop 停止任务。',
67
+ t('请先处理当前请求,或者发送 /stop 停止任务。'),
66
68
  ].join('\n'));
67
69
  }
68
70
 
69
71
  const session = boundSession(harness, state, key);
70
72
  if (!session) {
71
- return commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
73
+ return commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
72
74
  }
73
75
  if (typeof session.steerActiveTurn !== 'function') {
74
76
  throw new TypeError('Harness session does not support steering active turns');
@@ -79,6 +81,6 @@ export async function runControlCommand(text, harness, state, key, {
79
81
  requestOptions(signal),
80
82
  );
81
83
  return steered
82
- ? commandResult('已提交补充指令,Agent 会在下一步读取。')
83
- : commandResult('当前聊天没有正在运行的任务,请直接发送普通消息。');
84
+ ? commandResult(t('已提交补充指令,Agent 会在下一步读取。'))
85
+ : commandResult(t('当前聊天没有正在运行的任务,请直接发送普通消息。'));
84
86
  }
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  export function splitMessageText(value, limit) {
2
4
  const text = typeof value === 'string' ? value.trim() : '';
3
5
  if (!text) return [];
@@ -15,7 +17,7 @@ export function splitMessageText(value, limit) {
15
17
  }
16
18
 
17
19
  export function createEditableMessageStream({
18
- initialText = '正在处理…',
20
+ initialText = t('正在处理…'),
19
21
  limit,
20
22
  updateIntervalMs = 800,
21
23
  create,
@@ -77,7 +79,7 @@ export function createEditableMessageStream({
77
79
  pending = null;
78
80
  await inFlight?.catch(() => undefined);
79
81
  const chunks = splitMessageText(text, limit);
80
- const first = chunks[0] ?? '处理完成。';
82
+ const first = chunks[0] ?? t('处理完成。');
81
83
  if (first !== lastSent) await edit(messageId, first);
82
84
  lastSent = first;
83
85
  for (const chunk of chunks.slice(1)) {