@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
@@ -51,13 +51,15 @@ import {
51
51
  watchListCard,
52
52
  workspaceListCard,
53
53
  } from './feishu-cards.mjs';
54
+ import { t } from '../shared/i18n.mjs';
54
55
  import { MAX_WATCHES_PER_KEY } from './state-store.mjs';
55
56
  import {
56
57
  FEISHU_GROUP_RESPONSE_MODES,
57
58
  normalizeFeishuGroupResponseMode,
58
59
  } from './group-response-mode.mjs';
59
60
 
60
- const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
61
+ // Lazily evaluated: t() must run after setImHostLanguage, not at import time.
62
+ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
61
63
  const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
62
64
 
63
65
  const MENU_COMMAND = /^\/m(?:enu)?$/i;
@@ -87,34 +89,37 @@ const REPAIR_URL_HOSTS = new Set([
87
89
  'open.larksuite.com',
88
90
  ]);
89
91
 
90
- const HELP_TEXT = [
91
- '北汇星河 AIOS 已连接 DeepSeek Harness。',
92
- '',
93
- '直接发送文字、图片或文件即可继续当前会话。',
94
- '/new 开启一个全新会话',
95
- '/compact 压缩当前会话的较早上下文',
96
- '/workspace 工作区绝对路径 切换工作区',
97
- '/workspacelist 列出工作区绝对路径',
98
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
99
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
100
- '/models 按序号列出所有可用模型',
101
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
102
- '示例:先发 /models,再发 /model 2',
103
- '/presetlist 按序号列出可用 Agent Preset',
104
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
105
- '纯数字 ID:/preset id:<ID>',
106
- '/preset --default 跟随 Host 默认',
107
- '/stop 停止当前任务',
108
- '/steer 补充指令 纠偏当前任务',
109
- '/status 检查连接状态',
110
- '/repair 修复卡片按钮回调',
111
- '/m(或 /menu) 打开交互卡片菜单',
112
- '/watch [Session ID 或序号] 关注会话,任务完成自动推送',
113
- '/unwatch [Session ID 或序号] 取消关注',
114
- '/watchlist 查看关注列表',
115
- '/archived on|off 会话列表是否包含归档会话',
116
- '/help 显示本帮助',
117
- ].join('\n');
92
+ // Built lazily: t() must run after setImHostLanguage, not at import time.
93
+ function helpText() {
94
+ return [
95
+ t('北汇星河 AIOS 已连接 DeepSeek Harness。'),
96
+ '',
97
+ t('直接发送文字、图片或文件即可继续当前会话。'),
98
+ t('/new 开启一个全新会话'),
99
+ t('/compact 压缩当前会话的较早上下文'),
100
+ t('/workspace 工作区绝对路径 切换工作区'),
101
+ t('/workspacelist 列出工作区绝对路径'),
102
+ t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
103
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
104
+ t('/models 按序号列出所有可用模型'),
105
+ t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
106
+ t('示例:先发 /models,再发 /model 2'),
107
+ t('/presetlist 按序号列出可用 Agent Preset'),
108
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
109
+ t('纯数字 ID:/preset id:<ID>'),
110
+ t('/preset --default 跟随 Host 默认'),
111
+ t('/stop 停止当前任务'),
112
+ t('/steer 补充指令 纠偏当前任务'),
113
+ t('/status 检查连接状态'),
114
+ t('/repair 修复卡片按钮回调'),
115
+ t('/m(或 /menu) 打开交互卡片菜单'),
116
+ t('/watch [Session ID 或序号] 关注会话,任务完成自动推送'),
117
+ t('/unwatch [Session ID 或序号] 取消关注'),
118
+ t('/watchlist 查看关注列表'),
119
+ t('/archived on|off 会话列表是否包含归档会话'),
120
+ t('/help 显示本帮助'),
121
+ ].join('\n');
122
+ }
118
123
 
119
124
  const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
120
125
 
@@ -122,43 +127,43 @@ const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
122
127
  function safeErrorText(error) {
123
128
  switch (error?.code) {
124
129
  case 'workspace-not-absolute':
125
- return '工作区必须是绝对路径。';
130
+ return t('工作区必须是绝对路径。');
126
131
  case 'workspace-not-found':
127
- return '工作区路径不存在。';
132
+ return t('工作区路径不存在。');
128
133
  case 'workspace-not-directory':
129
- return '工作区路径必须指向一个目录。';
134
+ return t('工作区路径必须指向一个目录。');
130
135
  case 'workspace-bot-not-found':
131
- return '机器人正在移除或已重新接入,无法操作原会话的工作区。';
136
+ return t('机器人正在移除或已重新接入,无法操作原会话的工作区。');
132
137
  default:
133
- return '操作失败,请稍后重试。';
138
+ return t('操作失败,请稍后重试。');
134
139
  }
135
140
  }
136
141
 
137
142
  function artifactFailureText(fileName, error) {
138
- const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
143
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
139
144
  switch (error?.code) {
140
145
  case 'artifact-permission-required':
141
- return `结果文件「${name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。`;
146
+ return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。', { name });
142
147
  case 'artifact-too-large':
143
- return `结果文件「${name}」超过飞书 30 MB 上限,未发送。`;
148
+ return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { name });
144
149
  case 'artifact-empty':
145
- return `结果文件「${name}」为空,飞书不允许发送空文件。`;
150
+ return t('结果文件「{name}」为空,飞书不允许发送空文件。', { name });
146
151
  case 'artifact-changed':
147
152
  case 'artifact-invalid':
148
153
  case 'artifact-unavailable':
149
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
154
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
150
155
  case 'artifact-rate-limited':
151
- return `结果文件「${name}」暂时被飞书限流,未能发送,请稍后重试。`;
156
+ return t('结果文件「{name}」暂时被飞书限流,未能发送,请稍后重试。', { name });
152
157
  case 'artifact-delivery-uncertain':
153
- return `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
158
+ return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
154
159
  default:
155
- return `结果文件「${name}」已生成,但暂时未能发送,请稍后重试。`;
160
+ return t('结果文件「{name}」已生成,但暂时未能发送,请稍后重试。', { name });
156
161
  }
157
162
  }
158
163
 
159
164
  function answerTextForDelivery(answer, artifacts) {
160
165
  if (typeof answer === 'string' && answer.trim()) return answer;
161
- return artifacts.length > 0 ? '结果文件已生成。' : answer;
166
+ return artifacts.length > 0 ? t('结果文件已生成。') : answer;
162
167
  }
163
168
 
164
169
  function nonEmptyString(value) {
@@ -611,7 +616,7 @@ export class FeishuHarnessBridge {
611
616
  event.message.chat_id,
612
617
  inboundFileUserMessage(error)
613
618
  ?? imagePromptUserMessage(error)
614
- ?? '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
619
+ ?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
615
620
  ).catch(() => undefined);
616
621
  }
617
622
 
@@ -675,7 +680,7 @@ export class FeishuHarnessBridge {
675
680
  const hasFiles = hasInboundFiles(message);
676
681
  const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
677
682
  if (!text && !hasImages && !hasFiles) {
678
- await this.#send(event.message.chat_id, '目前支持文字、图片和文件消息。');
683
+ await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
679
684
  return;
680
685
  }
681
686
 
@@ -684,7 +689,7 @@ export class FeishuHarnessBridge {
684
689
  return;
685
690
  }
686
691
  if (commandText === '/help') {
687
- await this.#send(event.message.chat_id, HELP_TEXT);
692
+ await this.#send(event.message.chat_id, helpText());
688
693
  return;
689
694
  }
690
695
  if (MENU_COMMAND.test(commandText)) {
@@ -694,12 +699,12 @@ export class FeishuHarnessBridge {
694
699
  }
695
700
  if (commandText === '/new') {
696
701
  await this.#state.clearSession(key);
697
- await this.#send(event.message.chat_id, '已开启全新 Harness 会话。');
702
+ await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'));
698
703
  return;
699
704
  }
700
705
  if (commandText === '/status') {
701
706
  await this.#harness.ensureRunning({ signal: this.#signal });
702
- await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
707
+ await this.#send(event.message.chat_id, t('飞书机器人与 DeepSeek Harness 连接正常。'));
703
708
  return;
704
709
  }
705
710
  if (SESSION_LIST_PREFIX.test(commandText)) {
@@ -729,7 +734,7 @@ export class FeishuHarnessBridge {
729
734
  const match = ARCHIVED_COMMAND.exec(commandText);
730
735
  const value = match[1]?.toLowerCase();
731
736
  if (value !== 'on' && value !== 'off') {
732
- await this.#send(event.message.chat_id, '用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)');
737
+ await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'));
733
738
  return;
734
739
  }
735
740
  if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
@@ -737,7 +742,7 @@ export class FeishuHarnessBridge {
737
742
  }
738
743
  await this.#send(
739
744
  event.message.chat_id,
740
- value === 'on' ? '已开启:会话列表包含归档会话。' : '已关闭:会话列表隐藏归档会话。',
745
+ value === 'on' ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
741
746
  );
742
747
  return;
743
748
  }
@@ -794,7 +799,7 @@ export class FeishuHarnessBridge {
794
799
  // and number replies because callback buttons are the capability being fixed.
795
800
  async #handleRepairCommand(event, commandText) {
796
801
  if (event?.message?.chat_type !== 'p2p') {
797
- await this.#send(event.message.chat_id, '为避免授权链接暴露,请私聊机器人发送 /repair。');
802
+ await this.#send(event.message.chat_id, t('为避免授权链接暴露,请私聊机器人发送 /repair。'));
798
803
  return;
799
804
  }
800
805
  const actorOpenId = strictSenderOpenId(event);
@@ -802,19 +807,19 @@ export class FeishuHarnessBridge {
802
807
  await this.#send(
803
808
  event.message.chat_id,
804
809
  this.#repairOwnerOpenIds.size === 0
805
- ? '当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。'
806
- : '此操作只能由机器人接入者在私聊中发起,未进行任何修改。',
810
+ ? t('当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。')
811
+ : t('此操作只能由机器人接入者在私聊中发起,未进行任何修改。'),
807
812
  );
808
813
  return;
809
814
  }
810
815
  if (!this.#repair) {
811
- await this.#send(event.message.chat_id, '当前 Host 版本暂不支持聊天内修复,请先更新插件。');
816
+ await this.#send(event.message.chat_id, t('当前 Host 版本暂不支持聊天内修复,请先更新插件。'));
812
817
  return;
813
818
  }
814
819
 
815
820
  const parsed = REPAIR_COMMAND.exec(commandText);
816
821
  if (!parsed) {
817
- await this.#send(event.message.chat_id, '用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify');
822
+ await this.#send(event.message.chat_id, t('用法:/repair、/repair qr、/repair status、/repair cancel 或 /repair verify'));
818
823
  return;
819
824
  }
820
825
  const operation = parsed[1]?.toLowerCase() ?? 'start';
@@ -828,12 +833,12 @@ export class FeishuHarnessBridge {
828
833
  if (!attempt) {
829
834
  await this.#send(
830
835
  chatId,
831
- '当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。',
836
+ t('当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。'),
832
837
  );
833
838
  return;
834
839
  }
835
840
  if (attempt.actorOpenId !== actorOpenId) {
836
- await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
841
+ await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
837
842
  return;
838
843
  }
839
844
  if (operation === 'cancel') {
@@ -843,7 +848,7 @@ export class FeishuHarnessBridge {
843
848
  snapshot = repairSnapshot(result, { botId: this.#botId });
844
849
  attempt.snapshot = snapshot;
845
850
  } catch {
846
- await this.#send(chatId, '暂时无法取消修复任务,请稍后重试。');
851
+ await this.#send(chatId, t('暂时无法取消修复任务,请稍后重试。'));
847
852
  return;
848
853
  }
849
854
  if (snapshot.state === 'cancelled') {
@@ -858,7 +863,7 @@ export class FeishuHarnessBridge {
858
863
  try {
859
864
  snapshot = await this.#refreshRepairAttempt(attempt);
860
865
  } catch {
861
- await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
866
+ await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
862
867
  return;
863
868
  }
864
869
  if (operation === 'qr') {
@@ -887,7 +892,7 @@ export class FeishuHarnessBridge {
887
892
  const previous = this.#repairAttempt;
888
893
  if (previous && REPAIR_ACTIVE_STATES.has(previous.snapshot.state)) {
889
894
  if (previous.actorOpenId !== actorOpenId) {
890
- await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
895
+ await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
891
896
  return;
892
897
  }
893
898
  try {
@@ -897,7 +902,7 @@ export class FeishuHarnessBridge {
897
902
  return;
898
903
  }
899
904
  } catch {
900
- await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
905
+ await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
901
906
  return;
902
907
  }
903
908
  }
@@ -911,7 +916,7 @@ export class FeishuHarnessBridge {
911
916
  }), { botId: this.#botId });
912
917
  snapshot = await this.#waitForRepairLink(snapshot, { actorOpenId, chatId });
913
918
  } catch {
914
- await this.#send(chatId, '修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。');
919
+ await this.#send(chatId, t('修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。'));
915
920
  return;
916
921
  }
917
922
  const attempt = {
@@ -932,7 +937,7 @@ export class FeishuHarnessBridge {
932
937
  } catch {
933
938
  attempt.stopped = true;
934
939
  await this.#repair.cancel(this.#repairArgs(attempt)).catch(() => undefined);
935
- await this.#send(chatId, '飞书返回了无法安全验证的授权链接,已中止本次修复。');
940
+ await this.#send(chatId, t('飞书返回了无法安全验证的授权链接,已中止本次修复。'));
936
941
  return;
937
942
  }
938
943
  }
@@ -945,7 +950,7 @@ export class FeishuHarnessBridge {
945
950
  }
946
951
  if (!attempt.verificationUrl) {
947
952
  attempt.stopped = true;
948
- await this.#send(chatId, '飞书未返回授权链接,已中止本次修复。');
953
+ await this.#send(chatId, t('飞书未返回授权链接,已中止本次修复。'));
949
954
  return;
950
955
  }
951
956
  await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot);
@@ -1000,7 +1005,7 @@ export class FeishuHarnessBridge {
1000
1005
  attempt.announcedSaving = true;
1001
1006
  await this.#send(
1002
1007
  attempt.chatId,
1003
- '授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。',
1008
+ t('授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。'),
1004
1009
  );
1005
1010
  }
1006
1011
  if (REPAIR_TERMINAL_STATES.has(snapshot.state)) {
@@ -1021,7 +1026,7 @@ export class FeishuHarnessBridge {
1021
1026
  this.#logger.warn?.('[dsh-feishu] callback repair status monitoring failed');
1022
1027
  await this.#send(
1023
1028
  attempt.chatId,
1024
- '修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。',
1029
+ t('修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。'),
1025
1030
  ).catch(() => undefined);
1026
1031
  });
1027
1032
  }
@@ -1030,16 +1035,16 @@ export class FeishuHarnessBridge {
1030
1035
  const remaining = snapshot.remainingSeconds
1031
1036
  ?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
1032
1037
  const expiry = remaining === null
1033
- ? '链接为短期有效'
1034
- : `链接约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟后过期`;
1038
+ ? t('链接为短期有效')
1039
+ : t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
1035
1040
  await this.#send(chatId, [
1036
- existing ? '已有一个修复任务在等待授权。' : '🔧 准备修复卡片按钮。',
1037
- '本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。',
1041
+ existing ? t('已有一个修复任务在等待授权。') : t('🔧 准备修复卡片按钮。'),
1042
+ t('本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。'),
1038
1043
  '',
1039
- '当前设备直接打开:',
1044
+ t('当前设备直接打开:'),
1040
1045
  url,
1041
1046
  '',
1042
- `若要用另一台设备扫码,发送 /repair qr。${expiry}。`,
1047
+ t('若要用另一台设备扫码,发送 /repair qr{expiry}。', { expiry }),
1043
1048
  ].join('\n'));
1044
1049
  }
1045
1050
 
@@ -1055,9 +1060,12 @@ export class FeishuHarnessBridge {
1055
1060
  if (!imageKey) throw new Error('Feishu QR upload returned no image key');
1056
1061
  const remaining = snapshot.remainingSeconds
1057
1062
  ?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
1063
+ const remainingText = remaining === null
1064
+ ? ''
1065
+ : t('(剩余约 {minutes} 分钟)', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
1058
1066
  await this.#send(
1059
1067
  chatId,
1060
- `请用另一台设备扫码完成授权${remaining === null ? '' : `(剩余约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟)`}。`,
1068
+ t('请用另一台设备扫码完成授权{remaining}。', { remaining: remainingText }),
1061
1069
  );
1062
1070
  const response = await this.#client.im.v1.message.create({
1063
1071
  params: { receive_id_type: 'chat_id' },
@@ -1069,40 +1077,40 @@ export class FeishuHarnessBridge {
1069
1077
  });
1070
1078
  if (response?.code && response.code !== 0) throw new Error('Feishu QR message send failed');
1071
1079
  } catch {
1072
- await this.#send(chatId, `二维码暂时无法发送,请直接打开授权链接:\n${url}`);
1080
+ await this.#send(chatId, t('二维码暂时无法发送,请直接打开授权链接:\n{url}', { url }));
1073
1081
  }
1074
1082
  }
1075
1083
 
1076
1084
  #repairStatusText(snapshot, { verificationFocused = false } = {}) {
1077
1085
  if (snapshot.state === 'succeeded') {
1078
- return '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。';
1086
+ return t('✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。');
1079
1087
  }
1080
1088
  if (snapshot.state === 'expired' || snapshot.error?.code === 'expired_token') {
1081
- return '授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。';
1089
+ return t('授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。');
1082
1090
  }
1083
1091
  if (snapshot.state === 'cancelled' || snapshot.error?.code === 'abort') {
1084
- return '已取消本次修复授权,未确认完成修复。';
1092
+ return t('已取消本次修复授权,未确认完成修复。');
1085
1093
  }
1086
1094
  if (snapshot.error?.code === 'access_denied') {
1087
- return '你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。';
1095
+ return t('你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。');
1088
1096
  }
1089
1097
  if (snapshot.error?.code === 'card_action_probe_timeout'
1090
1098
  || snapshot.error?.code === 'card-action-probe-timeout') {
1091
- return '授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。';
1099
+ return t('授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。');
1092
1100
  }
1093
1101
  if (snapshot.state === 'error') {
1094
- return '修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。';
1102
+ return t('修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。');
1095
1103
  }
1096
1104
  if (snapshot.state === 'saving') {
1097
- return '授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。';
1105
+ return t('授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。');
1098
1106
  }
1099
1107
  if (verificationFocused) {
1100
- return '授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。';
1108
+ return t('授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。');
1101
1109
  }
1102
1110
  const remaining = snapshot.remainingSeconds === null
1103
1111
  ? ''
1104
- : `,剩余约 ${Math.max(1, Math.ceil(snapshot.remainingSeconds / 60))} 分钟`;
1105
- return `修复任务正在等待授权${remaining}。发送 /repair qr 可获取二维码,/repair cancel 可取消。`;
1112
+ : t(',剩余约 {minutes} 分钟', { minutes: Math.max(1, Math.ceil(snapshot.remainingSeconds / 60)) });
1113
+ return t('修复任务正在等待授权{remaining}。发送 /repair qr 可获取二维码,/repair cancel 可取消。', { remaining });
1106
1114
  }
1107
1115
 
1108
1116
  /**
@@ -1134,7 +1142,7 @@ export class FeishuHarnessBridge {
1134
1142
  // restart) or never came from us: nudge instead of staying silent.
1135
1143
  const chatId = nonEmptyString(event?.context?.open_chat_id);
1136
1144
  if (chatId) {
1137
- this.#send(chatId, '这个菜单已过期,请回复 /m 重新打开。').catch(() => undefined);
1145
+ this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。')).catch(() => undefined);
1138
1146
  }
1139
1147
  return Promise.resolve();
1140
1148
  }
@@ -1161,12 +1169,12 @@ export class FeishuHarnessBridge {
1161
1169
  }
1162
1170
  if (action === 'new') {
1163
1171
  await this.#state.clearSession(key);
1164
- await this.#send(chatId, '已开启全新 Harness 会话。');
1172
+ await this.#send(chatId, t('已开启全新 Harness 会话。'));
1165
1173
  return;
1166
1174
  }
1167
1175
  if (action === 'status') {
1168
1176
  await this.#harness.ensureRunning({ signal: this.#signal });
1169
- await this.#send(chatId, '飞书机器人与 DeepSeek Harness 连接正常。');
1177
+ await this.#send(chatId, t('飞书机器人与 DeepSeek Harness 连接正常。'));
1170
1178
  return;
1171
1179
  }
1172
1180
  if (action === 'help') {
@@ -1213,7 +1221,7 @@ export class FeishuHarnessBridge {
1213
1221
  if (menu.kind === 'menu') {
1214
1222
  const action = ['sessions', 'workspaces', 'new', 'status', 'help', 'repair', 'watchlist'][number - 1];
1215
1223
  if (!action) {
1216
- await this.#send(chatId, '菜单没有这个编号,回复 /m 重新打开。');
1224
+ await this.#send(chatId, t('菜单没有这个编号,回复 /m 重新打开。'));
1217
1225
  return;
1218
1226
  }
1219
1227
  if (action === 'repair') {
@@ -1226,7 +1234,7 @@ export class FeishuHarnessBridge {
1226
1234
  if (menu.kind === 'sessions') {
1227
1235
  const session = menu.sessions[number - 1];
1228
1236
  if (!session?.sessionId) {
1229
- await this.#send(chatId, `本页只有 ${menu.sessions.length} 个会话,回复 /sessionlist 重新查看。`);
1237
+ await this.#send(chatId, t('本页只有 {count} 个会话,回复 /sessionlist 重新查看。', { count: menu.sessions.length }));
1230
1238
  return;
1231
1239
  }
1232
1240
  // The number label sits on the session (bind) button of the row.
@@ -1236,7 +1244,7 @@ export class FeishuHarnessBridge {
1236
1244
  if (menu.kind === 'workspaces') {
1237
1245
  const workspace = menu.paths[number - 1];
1238
1246
  if (!workspace) {
1239
- await this.#send(chatId, `只有 ${menu.paths.length} 个工作区,回复 /workspacelist 重新查看。`);
1247
+ await this.#send(chatId, t('只有 {count} 个工作区,回复 /workspacelist 重新查看。', { count: menu.paths.length }));
1240
1248
  return;
1241
1249
  }
1242
1250
  await this.#handleCardAction(`workspace:${workspace}`, { chatId, key });
@@ -1245,7 +1253,7 @@ export class FeishuHarnessBridge {
1245
1253
  if (menu.kind === 'watches') {
1246
1254
  const entry = menu.entries[number - 1];
1247
1255
  if (!entry?.sessionId) {
1248
- await this.#send(chatId, `关注列表只有 ${menu.entries.length} 个会话。`);
1256
+ await this.#send(chatId, t('关注列表只有 {count} 个会话。', { count: menu.entries.length }));
1249
1257
  return;
1250
1258
  }
1251
1259
  await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key });
@@ -1271,7 +1279,7 @@ export class FeishuHarnessBridge {
1271
1279
  const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
1272
1280
  const workspace = listed?.workspace ?? resolved.workspace;
1273
1281
  if (sessions.length === 0) {
1274
- await this.#send(chatId, `工作区:${workspace}\n该工作区暂无会话。`);
1282
+ await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }));
1275
1283
  return;
1276
1284
  }
1277
1285
  const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
@@ -1296,7 +1304,7 @@ export class FeishuHarnessBridge {
1296
1304
  );
1297
1305
  } catch (error) {
1298
1306
  this.#logger.warn?.('[dsh-feishu] session list failed:', error.message);
1299
- await this.#send(chatId, '暂时无法获取会话列表,请稍后重试。');
1307
+ await this.#send(chatId, t('暂时无法获取会话列表,请稍后重试。'));
1300
1308
  }
1301
1309
  }
1302
1310
 
@@ -1307,26 +1315,26 @@ export class FeishuHarnessBridge {
1307
1315
  await this.#sendCard(chatId, workspaceListCard(paths, current), { key });
1308
1316
  } catch (error) {
1309
1317
  this.#logger.warn?.('[dsh-feishu] workspace list failed:', error.message);
1310
- await this.#send(chatId, '暂时无法获取工作区列表,请稍后重试。');
1318
+ await this.#send(chatId, t('暂时无法获取工作区列表,请稍后重试。'));
1311
1319
  }
1312
1320
  }
1313
1321
 
1314
1322
  async #bindSession(key, chatId, sessionId) {
1315
1323
  try {
1316
1324
  const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
1317
- const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || '暂无标题';
1318
- await this.#send(chatId, `已绑定会话「${title}」\nID:${bound?.sessionId ?? sessionId}`);
1325
+ const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || t('暂无标题');
1326
+ await this.#send(chatId, t('已绑定会话「{title}」\nID{id}', { title, id: bound?.sessionId ?? sessionId }));
1319
1327
  } catch (error) {
1320
- await this.#send(chatId, `绑定失败:${safeErrorText(error)}`);
1328
+ await this.#send(chatId, t('绑定失败:{message}', { message: safeErrorText(error) }));
1321
1329
  }
1322
1330
  }
1323
1331
 
1324
1332
  async #switchWorkspace(key, chatId, workspace) {
1325
1333
  try {
1326
1334
  const current = await this.#harness.switchWorkspace(workspace);
1327
- await this.#send(chatId, `工作区已切换为:${current}`);
1335
+ await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
1328
1336
  } catch (error) {
1329
- await this.#send(chatId, `切换失败:${safeErrorText(error)}`);
1337
+ await this.#send(chatId, t('切换失败:{message}', { message: safeErrorText(error) }));
1330
1338
  }
1331
1339
  }
1332
1340
 
@@ -1398,7 +1406,7 @@ export class FeishuHarnessBridge {
1398
1406
  */
1399
1407
  async #resolveWatchTarget(target) {
1400
1408
  if (typeof target !== 'string' || target === '') {
1401
- return { error: '用法:/watch <Session ID 或当前工作区序号>' };
1409
+ return { error: t('用法:/watch <Session ID 或当前工作区序号>') };
1402
1410
  }
1403
1411
  const numeric = /^\d{1,4}$/.test(target) ? Number(target) : null;
1404
1412
  const currentPath = typeof this.#harness?.currentWorkspace === 'function'
@@ -1409,13 +1417,13 @@ export class FeishuHarnessBridge {
1409
1417
  return Array.isArray(listed?.sessions) ? listed.sessions : [];
1410
1418
  };
1411
1419
  if (numeric !== null) {
1412
- if (!currentPath) return { error: '当前机器人没有可用的工作区,无法按序号解析会话。' };
1420
+ if (!currentPath) return { error: t('当前机器人没有可用的工作区,无法按序号解析会话。') };
1413
1421
  const sessions = this.#visibleSessions(await listSessions(currentPath));
1414
1422
  const session = sessions[numeric - 1];
1415
1423
  if (!session?.sessionId) {
1416
- return { error: `当前工作区只有 ${sessions.length} 个会话。` };
1424
+ return { error: t('当前工作区只有 {count} 个会话。', { count: sessions.length }) };
1417
1425
  }
1418
- return { sessionId: session.sessionId, title: session.title ?? '暂无标题' };
1426
+ return { sessionId: session.sessionId, title: session.title ?? t('暂无标题') };
1419
1427
  }
1420
1428
  const extraPaths = typeof this.#harness?.listWorkspaces === 'function'
1421
1429
  ? (await this.#harness.listWorkspaces()).filter((path) => path !== currentPath)
@@ -1424,9 +1432,9 @@ export class FeishuHarnessBridge {
1424
1432
  for (const workspace of paths) {
1425
1433
  const sessions = await listSessions(workspace);
1426
1434
  const session = sessions.find((candidate) => candidate.sessionId === target);
1427
- if (session) return { sessionId: target, title: session.title ?? '暂无标题' };
1435
+ if (session) return { sessionId: target, title: session.title ?? t('暂无标题') };
1428
1436
  }
1429
- return { error: '没有找到这个会话,请用 /sessionlist 查看可用会话。' };
1437
+ return { error: t('没有找到这个会话,请用 /sessionlist 查看可用会话。') };
1430
1438
  }
1431
1439
 
1432
1440
  async #latestSessionSeq(sessionId) {
@@ -1443,14 +1451,14 @@ export class FeishuHarnessBridge {
1443
1451
  async #runWatch(key, chatId, target) {
1444
1452
  this.#ensureEventWatcher();
1445
1453
  if (typeof this.#state?.setWatch !== 'function') {
1446
- await this.#send(chatId, '当前状态存储不支持关注。');
1454
+ await this.#send(chatId, t('当前状态存储不支持关注。'));
1447
1455
  return;
1448
1456
  }
1449
1457
  let resolved;
1450
1458
  try {
1451
1459
  resolved = await this.#resolveWatchTarget(target);
1452
1460
  } catch (error) {
1453
- await this.#send(chatId, `无法解析会话:${safeErrorText(error)}`);
1461
+ await this.#send(chatId, t('无法解析会话:{message}', { message: safeErrorText(error) }));
1454
1462
  return;
1455
1463
  }
1456
1464
  if (resolved.error) {
@@ -1460,7 +1468,7 @@ export class FeishuHarnessBridge {
1460
1468
  const existing = this.#state.watchEntries?.(key) ?? [];
1461
1469
  const existingEntry = existing.find((entry) => entry.sessionId === resolved.sessionId);
1462
1470
  if (!existingEntry && existing.length >= MAX_WATCHES_PER_KEY) {
1463
- await this.#send(chatId, `每个聊天最多关注 ${MAX_WATCHES_PER_KEY} 个会话。`);
1471
+ await this.#send(chatId, t('每个聊天最多关注 {count} 个会话。', { count: MAX_WATCHES_PER_KEY }));
1464
1472
  return;
1465
1473
  }
1466
1474
  try {
@@ -1473,10 +1481,10 @@ export class FeishuHarnessBridge {
1473
1481
  chatId,
1474
1482
  lastSeq,
1475
1483
  });
1476
- await this.#send(chatId, `已关注会话「${String(resolved.title).replace(/\s+/gu, ' ')}」,任务完成会推送结果。`);
1484
+ await this.#send(chatId, t('已关注会话「{title}」,任务完成会推送结果。', { title: String(resolved.title).replace(/\s+/gu, ' ') }));
1477
1485
  await this.#queueEventTask(() => this.#compensateSession(resolved.sessionId));
1478
1486
  } catch (error) {
1479
- await this.#send(chatId, `关注失败:${safeErrorText(error)}`);
1487
+ await this.#send(chatId, t('关注失败:{message}', { message: safeErrorText(error) }));
1480
1488
  }
1481
1489
  }
1482
1490
 
@@ -1487,15 +1495,15 @@ export class FeishuHarnessBridge {
1487
1495
  ? entries[Number(target) - 1]
1488
1496
  : entries.find((candidate) => candidate.sessionId === target);
1489
1497
  if (!entry) {
1490
- await this.#send(chatId, '关注列表里没有这个会话,回复 /watchlist 查看。');
1498
+ await this.#send(chatId, t('关注列表里没有这个会话,回复 /watchlist 查看。'));
1491
1499
  return;
1492
1500
  }
1493
1501
  try {
1494
1502
  await this.#state.removeWatch(key, entry.sessionId);
1495
1503
  this.#failedWatchSeqs.delete(`${key}\0${entry.sessionId}`);
1496
- await this.#send(chatId, `已取消关注「${String(entry.title ?? '').replace(/\s+/gu, ' ')}」。`);
1504
+ await this.#send(chatId, t('已取消关注「{title}」。', { title: String(entry.title ?? '').replace(/\s+/gu, ' ') }));
1497
1505
  } catch (error) {
1498
- await this.#send(chatId, `取消失败:${safeErrorText(error)}`);
1506
+ await this.#send(chatId, t('取消失败:{message}', { message: safeErrorText(error) }));
1499
1507
  }
1500
1508
  }
1501
1509
 
@@ -1877,18 +1885,18 @@ export class FeishuHarnessBridge {
1877
1885
 
1878
1886
  const text = extractText(event);
1879
1887
  if (!text) {
1880
- await this.#send(event.message.chat_id, '请用文字回答当前问题。');
1888
+ await this.#send(event.message.chat_id, t('请用文字回答当前问题。'));
1881
1889
  return;
1882
1890
  }
1883
1891
 
1884
1892
  const pending = this.#pendingInteractions.get(key);
1885
1893
  if (!pending || pending !== expected || pending.submitting) {
1886
1894
  if (this.#isResolvedQuestionReply(event, key)) {
1887
- await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
1895
+ await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
1888
1896
  return;
1889
1897
  }
1890
1898
  if (claimed && (!pending || pending !== expected)) {
1891
- await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT);
1899
+ await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT());
1892
1900
  return;
1893
1901
  }
1894
1902
  return this.#enqueueMessage(event, messageId, key, processingReaction, {
@@ -1946,7 +1954,7 @@ export class FeishuHarnessBridge {
1946
1954
  if (error?.code === 'interaction-not-pending') {
1947
1955
  this.#rememberResolvedInteraction(key, pending);
1948
1956
  this.#clearPendingInteraction(key, pending.interactionId);
1949
- await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
1957
+ await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
1950
1958
  return;
1951
1959
  }
1952
1960
  pending.submitting = false;
@@ -1954,7 +1962,7 @@ export class FeishuHarnessBridge {
1954
1962
  pending.index -= 1;
1955
1963
  this.#status.lastError = '回答提交失败。';
1956
1964
  this.#logger.error?.('[dsh-feishu] failed to answer a Harness interaction');
1957
- await this.#send(event.message.chat_id, '回答提交失败,请重新发送当前问题的答案。')
1965
+ await this.#send(event.message.chat_id, t('回答提交失败,请重新发送当前问题的答案。'))
1958
1966
  .catch(() => undefined);
1959
1967
  }
1960
1968
  }
@@ -1999,7 +2007,7 @@ export class FeishuHarnessBridge {
1999
2007
  });
2000
2008
  await this.#send(
2001
2009
  chatId,
2002
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
2010
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
2003
2011
  ).catch(() => undefined);
2004
2012
  return;
2005
2013
  }
@@ -2101,7 +2109,7 @@ export class FeishuHarnessBridge {
2101
2109
  await this.#state.markSeen(messageId);
2102
2110
  this.#status.lastMessageAt = new Date().toISOString();
2103
2111
  this.#status.messagesReceived += 1;
2104
- await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
2112
+ await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT()).catch(() => undefined);
2105
2113
  }
2106
2114
 
2107
2115
  #takePendingInteraction(key, interactionId) {
@@ -2141,10 +2149,10 @@ export class FeishuHarnessBridge {
2141
2149
  #progressText(update) {
2142
2150
  if (update.type === 'text' && update.text) return update.text;
2143
2151
  if (update.type === 'tool') {
2144
- if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
2145
- return `_正在使用 ${update.name || '工具'}…_`;
2152
+ if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
2153
+ return t('_正在使用 {name}…_', { name: update.name || t('工具') });
2146
2154
  }
2147
- return `_${update.text || '正在处理…'}_`;
2155
+ return t('_{text}_', { text: update.text || t('正在处理…') });
2148
2156
  }
2149
2157
 
2150
2158
  async #addReaction(messageId, emojiType) {