@xmanrui/dsh-im 1.4.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 (82) hide show
  1. package/README.en.md +18 -6
  2. package/README.md +18 -6
  3. package/lib/index.js +206 -176
  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 +105 -82
  8. package/src/channels/dingtalk/dingtalk-bridge.mjs +79 -110
  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 +169 -180
  17. package/src/channels/feishu/feishu-cards.mjs +62 -54
  18. package/src/channels/feishu/feishu-channel.mjs +57 -21
  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 +149 -130
  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 +170 -0
  56. package/src/channels/shared/semantic/artifact.mjs +2 -2
  57. package/src/channels/shared/semantic/delivery.mjs +40 -0
  58. package/src/channels/shared/text-harness-bridge.mjs +154 -136
  59. package/src/channels/shared/token-bot-controller.mjs +32 -11
  60. package/src/channels/shared/workspace-command.mjs +69 -58
  61. package/src/channels/slack/config-store.mjs +3 -1
  62. package/src/channels/slack/slack-api.mjs +6 -5
  63. package/src/channels/slack/slack-controller.mjs +10 -9
  64. package/src/channels/slack/slack-runtime.mjs +16 -4
  65. package/src/channels/telegram/config-store.mjs +2 -1
  66. package/src/channels/telegram/telegram-api.mjs +111 -19
  67. package/src/channels/telegram/telegram-rich-message.mjs +147 -0
  68. package/src/channels/telegram/telegram-runtime.mjs +383 -5
  69. package/src/channels/wecom/config-store.mjs +3 -1
  70. package/src/channels/wecom/wecom-bridge.mjs +154 -153
  71. package/src/channels/wecom/wecom-controller.mjs +20 -19
  72. package/src/channels/wecom/wecom-runtime.mjs +2 -1
  73. package/src/channels/weixin/config-store.mjs +2 -1
  74. package/src/channels/weixin/weixin-api.mjs +140 -107
  75. package/src/channels/weixin/weixin-bridge.mjs +86 -123
  76. package/src/channels/weixin/weixin-controller.mjs +22 -21
  77. package/src/channels/weixin/weixin-runtime.mjs +3 -2
  78. package/src/channels/whatsapp/config-store.mjs +3 -1
  79. package/src/channels/whatsapp/whatsapp-bridge.mjs +3 -1
  80. package/src/channels/whatsapp/whatsapp-controller.mjs +15 -14
  81. package/src/channels/whatsapp/whatsapp-runtime.mjs +47 -13
  82. package/src/channels/whatsapp/whatsapp-web-session.mjs +3 -1
@@ -38,14 +38,9 @@ import {
38
38
  } from '../shared/preset-command.mjs';
39
39
  import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
40
40
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
41
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
41
42
  import {
42
- materializeOutboundArtifact,
43
- releaseOutboundArtifact,
44
- } from '../shared/semantic/artifact.mjs';
45
- import {
46
- createArtifactFailureReceipt,
47
43
  createDeliveryReceipt,
48
- mergeDeliveryReceipts,
49
44
  } from '../shared/semantic/delivery.mjs';
50
45
  import {
51
46
  MENU_PAGE_SIZE,
@@ -56,13 +51,15 @@ import {
56
51
  watchListCard,
57
52
  workspaceListCard,
58
53
  } from './feishu-cards.mjs';
54
+ import { t } from '../shared/i18n.mjs';
59
55
  import { MAX_WATCHES_PER_KEY } from './state-store.mjs';
60
56
  import {
61
57
  FEISHU_GROUP_RESPONSE_MODES,
62
58
  normalizeFeishuGroupResponseMode,
63
59
  } from './group-response-mode.mjs';
64
60
 
65
- const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
61
+ // Lazily evaluated: t() must run after setImHostLanguage, not at import time.
62
+ const INTERACTION_RESOLVED_TEXT = () => t('这个问题已在其他客户端处理,无需再次回答。');
66
63
  const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
67
64
 
68
65
  const MENU_COMMAND = /^\/m(?:enu)?$/i;
@@ -92,34 +89,37 @@ const REPAIR_URL_HOSTS = new Set([
92
89
  'open.larksuite.com',
93
90
  ]);
94
91
 
95
- const HELP_TEXT = [
96
- '北汇星河 AIOS 已连接 DeepSeek Harness。',
97
- '',
98
- '直接发送文字、图片或文件即可继续当前会话。',
99
- '/new 开启一个全新会话',
100
- '/compact 压缩当前会话的较早上下文',
101
- '/workspace 工作区绝对路径 切换工作区',
102
- '/workspacelist 列出工作区绝对路径',
103
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
104
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
105
- '/models 按序号列出所有可用模型',
106
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
107
- '示例:先发 /models,再发 /model 2',
108
- '/presetlist 按序号列出可用 Agent Preset',
109
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
110
- '纯数字 ID:/preset id:<ID>',
111
- '/preset --default 跟随 Host 默认',
112
- '/stop 停止当前任务',
113
- '/steer 补充指令 纠偏当前任务',
114
- '/status 检查连接状态',
115
- '/repair 修复卡片按钮回调',
116
- '/m(或 /menu) 打开交互卡片菜单',
117
- '/watch [Session ID 或序号] 关注会话,任务完成自动推送',
118
- '/unwatch [Session ID 或序号] 取消关注',
119
- '/watchlist 查看关注列表',
120
- '/archived on|off 会话列表是否包含归档会话',
121
- '/help 显示本帮助',
122
- ].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
+ }
123
123
 
124
124
  const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
125
125
 
@@ -127,43 +127,43 @@ const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
127
127
  function safeErrorText(error) {
128
128
  switch (error?.code) {
129
129
  case 'workspace-not-absolute':
130
- return '工作区必须是绝对路径。';
130
+ return t('工作区必须是绝对路径。');
131
131
  case 'workspace-not-found':
132
- return '工作区路径不存在。';
132
+ return t('工作区路径不存在。');
133
133
  case 'workspace-not-directory':
134
- return '工作区路径必须指向一个目录。';
134
+ return t('工作区路径必须指向一个目录。');
135
135
  case 'workspace-bot-not-found':
136
- return '机器人正在移除或已重新接入,无法操作原会话的工作区。';
136
+ return t('机器人正在移除或已重新接入,无法操作原会话的工作区。');
137
137
  default:
138
- return '操作失败,请稍后重试。';
138
+ return t('操作失败,请稍后重试。');
139
139
  }
140
140
  }
141
141
 
142
142
  function artifactFailureText(fileName, error) {
143
- const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
143
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
144
144
  switch (error?.code) {
145
145
  case 'artifact-permission-required':
146
- return `结果文件「${name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。`;
146
+ return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。', { name });
147
147
  case 'artifact-too-large':
148
- return `结果文件「${name}」超过飞书 30 MB 上限,未发送。`;
148
+ return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { name });
149
149
  case 'artifact-empty':
150
- return `结果文件「${name}」为空,飞书不允许发送空文件。`;
150
+ return t('结果文件「{name}」为空,飞书不允许发送空文件。', { name });
151
151
  case 'artifact-changed':
152
152
  case 'artifact-invalid':
153
153
  case 'artifact-unavailable':
154
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
154
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
155
155
  case 'artifact-rate-limited':
156
- return `结果文件「${name}」暂时被飞书限流,未能发送,请稍后重试。`;
156
+ return t('结果文件「{name}」暂时被飞书限流,未能发送,请稍后重试。', { name });
157
157
  case 'artifact-delivery-uncertain':
158
- return `结果文件「${name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
158
+ return t('结果文件「{name}」发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
159
159
  default:
160
- return `结果文件「${name}」已生成,但暂时未能发送,请稍后重试。`;
160
+ return t('结果文件「{name}」已生成,但暂时未能发送,请稍后重试。', { name });
161
161
  }
162
162
  }
163
163
 
164
164
  function answerTextForDelivery(answer, artifacts) {
165
165
  if (typeof answer === 'string' && answer.trim()) return answer;
166
- return artifacts.length > 0 ? '结果文件已生成。' : answer;
166
+ return artifacts.length > 0 ? t('结果文件已生成。') : answer;
167
167
  }
168
168
 
169
169
  function nonEmptyString(value) {
@@ -616,7 +616,7 @@ export class FeishuHarnessBridge {
616
616
  event.message.chat_id,
617
617
  inboundFileUserMessage(error)
618
618
  ?? imagePromptUserMessage(error)
619
- ?? '处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
619
+ ?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
620
620
  ).catch(() => undefined);
621
621
  }
622
622
 
@@ -680,7 +680,7 @@ export class FeishuHarnessBridge {
680
680
  const hasFiles = hasInboundFiles(message);
681
681
  const commandText = event.message.message_type === 'text' && !hasImages && !hasFiles ? text : null;
682
682
  if (!text && !hasImages && !hasFiles) {
683
- await this.#send(event.message.chat_id, '目前支持文字、图片和文件消息。');
683
+ await this.#send(event.message.chat_id, t('目前支持文字、图片和文件消息。'));
684
684
  return;
685
685
  }
686
686
 
@@ -689,7 +689,7 @@ export class FeishuHarnessBridge {
689
689
  return;
690
690
  }
691
691
  if (commandText === '/help') {
692
- await this.#send(event.message.chat_id, HELP_TEXT);
692
+ await this.#send(event.message.chat_id, helpText());
693
693
  return;
694
694
  }
695
695
  if (MENU_COMMAND.test(commandText)) {
@@ -699,12 +699,12 @@ export class FeishuHarnessBridge {
699
699
  }
700
700
  if (commandText === '/new') {
701
701
  await this.#state.clearSession(key);
702
- await this.#send(event.message.chat_id, '已开启全新 Harness 会话。');
702
+ await this.#send(event.message.chat_id, t('已开启全新 Harness 会话。'));
703
703
  return;
704
704
  }
705
705
  if (commandText === '/status') {
706
706
  await this.#harness.ensureRunning({ signal: this.#signal });
707
- await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
707
+ await this.#send(event.message.chat_id, t('飞书机器人与 DeepSeek Harness 连接正常。'));
708
708
  return;
709
709
  }
710
710
  if (SESSION_LIST_PREFIX.test(commandText)) {
@@ -734,7 +734,7 @@ export class FeishuHarnessBridge {
734
734
  const match = ARCHIVED_COMMAND.exec(commandText);
735
735
  const value = match[1]?.toLowerCase();
736
736
  if (value !== 'on' && value !== 'off') {
737
- await this.#send(event.message.chat_id, '用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)');
737
+ await this.#send(event.message.chat_id, t('用法:/archived on(包含归档会话)或 /archived off(隐藏归档会话)'));
738
738
  return;
739
739
  }
740
740
  if (typeof this.#state?.setIncludeArchivedSessions === 'function') {
@@ -742,7 +742,7 @@ export class FeishuHarnessBridge {
742
742
  }
743
743
  await this.#send(
744
744
  event.message.chat_id,
745
- value === 'on' ? '已开启:会话列表包含归档会话。' : '已关闭:会话列表隐藏归档会话。',
745
+ value === 'on' ? t('已开启:会话列表包含归档会话。') : t('已关闭:会话列表隐藏归档会话。'),
746
746
  );
747
747
  return;
748
748
  }
@@ -799,7 +799,7 @@ export class FeishuHarnessBridge {
799
799
  // and number replies because callback buttons are the capability being fixed.
800
800
  async #handleRepairCommand(event, commandText) {
801
801
  if (event?.message?.chat_type !== 'p2p') {
802
- await this.#send(event.message.chat_id, '为避免授权链接暴露,请私聊机器人发送 /repair。');
802
+ await this.#send(event.message.chat_id, t('为避免授权链接暴露,请私聊机器人发送 /repair。'));
803
803
  return;
804
804
  }
805
805
  const actorOpenId = strictSenderOpenId(event);
@@ -807,19 +807,19 @@ export class FeishuHarnessBridge {
807
807
  await this.#send(
808
808
  event.message.chat_id,
809
809
  this.#repairOwnerOpenIds.size === 0
810
- ? '当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。'
811
- : '此操作只能由机器人接入者在私聊中发起,未进行任何修改。',
810
+ ? t('当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。')
811
+ : t('此操作只能由机器人接入者在私聊中发起,未进行任何修改。'),
812
812
  );
813
813
  return;
814
814
  }
815
815
  if (!this.#repair) {
816
- await this.#send(event.message.chat_id, '当前 Host 版本暂不支持聊天内修复,请先更新插件。');
816
+ await this.#send(event.message.chat_id, t('当前 Host 版本暂不支持聊天内修复,请先更新插件。'));
817
817
  return;
818
818
  }
819
819
 
820
820
  const parsed = REPAIR_COMMAND.exec(commandText);
821
821
  if (!parsed) {
822
- 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'));
823
823
  return;
824
824
  }
825
825
  const operation = parsed[1]?.toLowerCase() ?? 'start';
@@ -833,12 +833,12 @@ export class FeishuHarnessBridge {
833
833
  if (!attempt) {
834
834
  await this.#send(
835
835
  chatId,
836
- '当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。',
836
+ t('当前 Runtime 没有可恢复的修复任务记录(机器人可能刚完成密钥更新并重启)。本命令不会启动新的授权;请查看机器人发送的验证结果,确认上一次任务已结束后再发送 /repair。'),
837
837
  );
838
838
  return;
839
839
  }
840
840
  if (attempt.actorOpenId !== actorOpenId) {
841
- await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
841
+ await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
842
842
  return;
843
843
  }
844
844
  if (operation === 'cancel') {
@@ -848,7 +848,7 @@ export class FeishuHarnessBridge {
848
848
  snapshot = repairSnapshot(result, { botId: this.#botId });
849
849
  attempt.snapshot = snapshot;
850
850
  } catch {
851
- await this.#send(chatId, '暂时无法取消修复任务,请稍后重试。');
851
+ await this.#send(chatId, t('暂时无法取消修复任务,请稍后重试。'));
852
852
  return;
853
853
  }
854
854
  if (snapshot.state === 'cancelled') {
@@ -863,7 +863,7 @@ export class FeishuHarnessBridge {
863
863
  try {
864
864
  snapshot = await this.#refreshRepairAttempt(attempt);
865
865
  } catch {
866
- await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
866
+ await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
867
867
  return;
868
868
  }
869
869
  if (operation === 'qr') {
@@ -892,7 +892,7 @@ export class FeishuHarnessBridge {
892
892
  const previous = this.#repairAttempt;
893
893
  if (previous && REPAIR_ACTIVE_STATES.has(previous.snapshot.state)) {
894
894
  if (previous.actorOpenId !== actorOpenId) {
895
- await this.#send(chatId, '另一位管理员正在修复该机器人,本次不会显示其授权信息。');
895
+ await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
896
896
  return;
897
897
  }
898
898
  try {
@@ -902,7 +902,7 @@ export class FeishuHarnessBridge {
902
902
  return;
903
903
  }
904
904
  } catch {
905
- await this.#send(chatId, '暂时无法查询修复状态,请稍后重试。');
905
+ await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
906
906
  return;
907
907
  }
908
908
  }
@@ -916,7 +916,7 @@ export class FeishuHarnessBridge {
916
916
  }), { botId: this.#botId });
917
917
  snapshot = await this.#waitForRepairLink(snapshot, { actorOpenId, chatId });
918
918
  } catch {
919
- await this.#send(chatId, '修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。');
919
+ await this.#send(chatId, t('修复流程暂时失败,现有机器人连接不受影响;请稍后发送 /repair 重试。'));
920
920
  return;
921
921
  }
922
922
  const attempt = {
@@ -937,7 +937,7 @@ export class FeishuHarnessBridge {
937
937
  } catch {
938
938
  attempt.stopped = true;
939
939
  await this.#repair.cancel(this.#repairArgs(attempt)).catch(() => undefined);
940
- await this.#send(chatId, '飞书返回了无法安全验证的授权链接,已中止本次修复。');
940
+ await this.#send(chatId, t('飞书返回了无法安全验证的授权链接,已中止本次修复。'));
941
941
  return;
942
942
  }
943
943
  }
@@ -950,7 +950,7 @@ export class FeishuHarnessBridge {
950
950
  }
951
951
  if (!attempt.verificationUrl) {
952
952
  attempt.stopped = true;
953
- await this.#send(chatId, '飞书未返回授权链接,已中止本次修复。');
953
+ await this.#send(chatId, t('飞书未返回授权链接,已中止本次修复。'));
954
954
  return;
955
955
  }
956
956
  await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot);
@@ -1005,7 +1005,7 @@ export class FeishuHarnessBridge {
1005
1005
  attempt.announcedSaving = true;
1006
1006
  await this.#send(
1007
1007
  attempt.chatId,
1008
- '授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。',
1008
+ t('授权已确认,正在发送并等待测试按钮回调;收到真实回调后才会完成。'),
1009
1009
  );
1010
1010
  }
1011
1011
  if (REPAIR_TERMINAL_STATES.has(snapshot.state)) {
@@ -1026,7 +1026,7 @@ export class FeishuHarnessBridge {
1026
1026
  this.#logger.warn?.('[dsh-feishu] callback repair status monitoring failed');
1027
1027
  await this.#send(
1028
1028
  attempt.chatId,
1029
- '修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。',
1029
+ t('修复状态查询中断,现有机器人连接不受影响;发送 /repair status 重试查询。'),
1030
1030
  ).catch(() => undefined);
1031
1031
  });
1032
1032
  }
@@ -1035,16 +1035,16 @@ export class FeishuHarnessBridge {
1035
1035
  const remaining = snapshot.remainingSeconds
1036
1036
  ?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
1037
1037
  const expiry = remaining === null
1038
- ? '链接为短期有效'
1039
- : `链接约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟后过期`;
1038
+ ? t('链接为短期有效')
1039
+ : t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
1040
1040
  await this.#send(chatId, [
1041
- existing ? '已有一个修复任务在等待授权。' : '🔧 准备修复卡片按钮。',
1042
- '本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。',
1041
+ existing ? t('已有一个修复任务在等待授权。') : t('🔧 准备修复卡片按钮。'),
1042
+ t('本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。'),
1043
1043
  '',
1044
- '当前设备直接打开:',
1044
+ t('当前设备直接打开:'),
1045
1045
  url,
1046
1046
  '',
1047
- `若要用另一台设备扫码,发送 /repair qr。${expiry}。`,
1047
+ t('若要用另一台设备扫码,发送 /repair qr{expiry}。', { expiry }),
1048
1048
  ].join('\n'));
1049
1049
  }
1050
1050
 
@@ -1060,9 +1060,12 @@ export class FeishuHarnessBridge {
1060
1060
  if (!imageKey) throw new Error('Feishu QR upload returned no image key');
1061
1061
  const remaining = snapshot.remainingSeconds
1062
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)) });
1063
1066
  await this.#send(
1064
1067
  chatId,
1065
- `请用另一台设备扫码完成授权${remaining === null ? '' : `(剩余约 ${Math.max(1, Math.ceil(remaining / 60))} 分钟)`}。`,
1068
+ t('请用另一台设备扫码完成授权{remaining}。', { remaining: remainingText }),
1066
1069
  );
1067
1070
  const response = await this.#client.im.v1.message.create({
1068
1071
  params: { receive_id_type: 'chat_id' },
@@ -1074,40 +1077,40 @@ export class FeishuHarnessBridge {
1074
1077
  });
1075
1078
  if (response?.code && response.code !== 0) throw new Error('Feishu QR message send failed');
1076
1079
  } catch {
1077
- await this.#send(chatId, `二维码暂时无法发送,请直接打开授权链接:\n${url}`);
1080
+ await this.#send(chatId, t('二维码暂时无法发送,请直接打开授权链接:\n{url}', { url }));
1078
1081
  }
1079
1082
  }
1080
1083
 
1081
1084
  #repairStatusText(snapshot, { verificationFocused = false } = {}) {
1082
1085
  if (snapshot.state === 'succeeded') {
1083
- return '✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。';
1086
+ return t('✅ 修复完成:已实测收到 card.action.trigger,菜单按钮现在可用。');
1084
1087
  }
1085
1088
  if (snapshot.state === 'expired' || snapshot.error?.code === 'expired_token') {
1086
- return '授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。';
1089
+ return t('授权链接已过期;平台未返回成功结果,无法确认已修复。发送 /repair 生成新链接。');
1087
1090
  }
1088
1091
  if (snapshot.state === 'cancelled' || snapshot.error?.code === 'abort') {
1089
- return '已取消本次修复授权,未确认完成修复。';
1092
+ return t('已取消本次修复授权,未确认完成修复。');
1090
1093
  }
1091
1094
  if (snapshot.error?.code === 'access_denied') {
1092
- return '你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。';
1095
+ return t('你已取消或拒绝授权,没有确认修复;发送 /repair 可重试。');
1093
1096
  }
1094
1097
  if (snapshot.error?.code === 'card_action_probe_timeout'
1095
1098
  || snapshot.error?.code === 'card-action-probe-timeout') {
1096
- return '授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。';
1099
+ return t('授权已提交,但未收到测试按钮回调。可能尚未点击或配置仍在传播;稍后发送 /repair verify 查询,不要盲目重复授权。');
1097
1100
  }
1098
1101
  if (snapshot.state === 'error') {
1099
- return '修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。';
1102
+ return t('修复流程暂时失败,现有机器人连接不受影响;发送 /repair 可重试。');
1100
1103
  }
1101
1104
  if (snapshot.state === 'saving') {
1102
- return '授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。';
1105
+ return t('授权已确认,正在等待专用测试按钮的真实回调;回调到达前不会宣告成功。');
1103
1106
  }
1104
1107
  if (verificationFocused) {
1105
- return '授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。';
1108
+ return t('授权尚未完成,暂时不能验证卡片按钮。请先打开授权链接并确认。');
1106
1109
  }
1107
1110
  const remaining = snapshot.remainingSeconds === null
1108
1111
  ? ''
1109
- : `,剩余约 ${Math.max(1, Math.ceil(snapshot.remainingSeconds / 60))} 分钟`;
1110
- 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 });
1111
1114
  }
1112
1115
 
1113
1116
  /**
@@ -1139,7 +1142,7 @@ export class FeishuHarnessBridge {
1139
1142
  // restart) or never came from us: nudge instead of staying silent.
1140
1143
  const chatId = nonEmptyString(event?.context?.open_chat_id);
1141
1144
  if (chatId) {
1142
- this.#send(chatId, '这个菜单已过期,请回复 /m 重新打开。').catch(() => undefined);
1145
+ this.#send(chatId, t('这个菜单已过期,请回复 /m 重新打开。')).catch(() => undefined);
1143
1146
  }
1144
1147
  return Promise.resolve();
1145
1148
  }
@@ -1166,12 +1169,12 @@ export class FeishuHarnessBridge {
1166
1169
  }
1167
1170
  if (action === 'new') {
1168
1171
  await this.#state.clearSession(key);
1169
- await this.#send(chatId, '已开启全新 Harness 会话。');
1172
+ await this.#send(chatId, t('已开启全新 Harness 会话。'));
1170
1173
  return;
1171
1174
  }
1172
1175
  if (action === 'status') {
1173
1176
  await this.#harness.ensureRunning({ signal: this.#signal });
1174
- await this.#send(chatId, '飞书机器人与 DeepSeek Harness 连接正常。');
1177
+ await this.#send(chatId, t('飞书机器人与 DeepSeek Harness 连接正常。'));
1175
1178
  return;
1176
1179
  }
1177
1180
  if (action === 'help') {
@@ -1218,7 +1221,7 @@ export class FeishuHarnessBridge {
1218
1221
  if (menu.kind === 'menu') {
1219
1222
  const action = ['sessions', 'workspaces', 'new', 'status', 'help', 'repair', 'watchlist'][number - 1];
1220
1223
  if (!action) {
1221
- await this.#send(chatId, '菜单没有这个编号,回复 /m 重新打开。');
1224
+ await this.#send(chatId, t('菜单没有这个编号,回复 /m 重新打开。'));
1222
1225
  return;
1223
1226
  }
1224
1227
  if (action === 'repair') {
@@ -1231,7 +1234,7 @@ export class FeishuHarnessBridge {
1231
1234
  if (menu.kind === 'sessions') {
1232
1235
  const session = menu.sessions[number - 1];
1233
1236
  if (!session?.sessionId) {
1234
- await this.#send(chatId, `本页只有 ${menu.sessions.length} 个会话,回复 /sessionlist 重新查看。`);
1237
+ await this.#send(chatId, t('本页只有 {count} 个会话,回复 /sessionlist 重新查看。', { count: menu.sessions.length }));
1235
1238
  return;
1236
1239
  }
1237
1240
  // The number label sits on the session (bind) button of the row.
@@ -1241,7 +1244,7 @@ export class FeishuHarnessBridge {
1241
1244
  if (menu.kind === 'workspaces') {
1242
1245
  const workspace = menu.paths[number - 1];
1243
1246
  if (!workspace) {
1244
- await this.#send(chatId, `只有 ${menu.paths.length} 个工作区,回复 /workspacelist 重新查看。`);
1247
+ await this.#send(chatId, t('只有 {count} 个工作区,回复 /workspacelist 重新查看。', { count: menu.paths.length }));
1245
1248
  return;
1246
1249
  }
1247
1250
  await this.#handleCardAction(`workspace:${workspace}`, { chatId, key });
@@ -1250,7 +1253,7 @@ export class FeishuHarnessBridge {
1250
1253
  if (menu.kind === 'watches') {
1251
1254
  const entry = menu.entries[number - 1];
1252
1255
  if (!entry?.sessionId) {
1253
- await this.#send(chatId, `关注列表只有 ${menu.entries.length} 个会话。`);
1256
+ await this.#send(chatId, t('关注列表只有 {count} 个会话。', { count: menu.entries.length }));
1254
1257
  return;
1255
1258
  }
1256
1259
  await this.#handleCardAction(`unwatch:${entry.sessionId}`, { chatId, key });
@@ -1276,7 +1279,7 @@ export class FeishuHarnessBridge {
1276
1279
  const sessions = this.#visibleSessions(Array.isArray(listed?.sessions) ? listed.sessions : []);
1277
1280
  const workspace = listed?.workspace ?? resolved.workspace;
1278
1281
  if (sessions.length === 0) {
1279
- await this.#send(chatId, `工作区:${workspace}\n该工作区暂无会话。`);
1282
+ await this.#send(chatId, t('工作区:{workspace}\n该工作区暂无会话。', { workspace }));
1280
1283
  return;
1281
1284
  }
1282
1285
  const pageCount = Math.ceil(sessions.length / MENU_PAGE_SIZE);
@@ -1301,7 +1304,7 @@ export class FeishuHarnessBridge {
1301
1304
  );
1302
1305
  } catch (error) {
1303
1306
  this.#logger.warn?.('[dsh-feishu] session list failed:', error.message);
1304
- await this.#send(chatId, '暂时无法获取会话列表,请稍后重试。');
1307
+ await this.#send(chatId, t('暂时无法获取会话列表,请稍后重试。'));
1305
1308
  }
1306
1309
  }
1307
1310
 
@@ -1312,26 +1315,26 @@ export class FeishuHarnessBridge {
1312
1315
  await this.#sendCard(chatId, workspaceListCard(paths, current), { key });
1313
1316
  } catch (error) {
1314
1317
  this.#logger.warn?.('[dsh-feishu] workspace list failed:', error.message);
1315
- await this.#send(chatId, '暂时无法获取工作区列表,请稍后重试。');
1318
+ await this.#send(chatId, t('暂时无法获取工作区列表,请稍后重试。'));
1316
1319
  }
1317
1320
  }
1318
1321
 
1319
1322
  async #bindSession(key, chatId, sessionId) {
1320
1323
  try {
1321
1324
  const bound = await this.#harness.bindWorkspaceSession(key, sessionId);
1322
- const title = String(bound?.title ?? '').replace(/\s+/gu, ' ').trim() || '暂无标题';
1323
- 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 }));
1324
1327
  } catch (error) {
1325
- await this.#send(chatId, `绑定失败:${safeErrorText(error)}`);
1328
+ await this.#send(chatId, t('绑定失败:{message}', { message: safeErrorText(error) }));
1326
1329
  }
1327
1330
  }
1328
1331
 
1329
1332
  async #switchWorkspace(key, chatId, workspace) {
1330
1333
  try {
1331
1334
  const current = await this.#harness.switchWorkspace(workspace);
1332
- await this.#send(chatId, `工作区已切换为:${current}`);
1335
+ await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
1333
1336
  } catch (error) {
1334
- await this.#send(chatId, `切换失败:${safeErrorText(error)}`);
1337
+ await this.#send(chatId, t('切换失败:{message}', { message: safeErrorText(error) }));
1335
1338
  }
1336
1339
  }
1337
1340
 
@@ -1403,7 +1406,7 @@ export class FeishuHarnessBridge {
1403
1406
  */
1404
1407
  async #resolveWatchTarget(target) {
1405
1408
  if (typeof target !== 'string' || target === '') {
1406
- return { error: '用法:/watch <Session ID 或当前工作区序号>' };
1409
+ return { error: t('用法:/watch <Session ID 或当前工作区序号>') };
1407
1410
  }
1408
1411
  const numeric = /^\d{1,4}$/.test(target) ? Number(target) : null;
1409
1412
  const currentPath = typeof this.#harness?.currentWorkspace === 'function'
@@ -1414,13 +1417,13 @@ export class FeishuHarnessBridge {
1414
1417
  return Array.isArray(listed?.sessions) ? listed.sessions : [];
1415
1418
  };
1416
1419
  if (numeric !== null) {
1417
- if (!currentPath) return { error: '当前机器人没有可用的工作区,无法按序号解析会话。' };
1420
+ if (!currentPath) return { error: t('当前机器人没有可用的工作区,无法按序号解析会话。') };
1418
1421
  const sessions = this.#visibleSessions(await listSessions(currentPath));
1419
1422
  const session = sessions[numeric - 1];
1420
1423
  if (!session?.sessionId) {
1421
- return { error: `当前工作区只有 ${sessions.length} 个会话。` };
1424
+ return { error: t('当前工作区只有 {count} 个会话。', { count: sessions.length }) };
1422
1425
  }
1423
- return { sessionId: session.sessionId, title: session.title ?? '暂无标题' };
1426
+ return { sessionId: session.sessionId, title: session.title ?? t('暂无标题') };
1424
1427
  }
1425
1428
  const extraPaths = typeof this.#harness?.listWorkspaces === 'function'
1426
1429
  ? (await this.#harness.listWorkspaces()).filter((path) => path !== currentPath)
@@ -1429,9 +1432,9 @@ export class FeishuHarnessBridge {
1429
1432
  for (const workspace of paths) {
1430
1433
  const sessions = await listSessions(workspace);
1431
1434
  const session = sessions.find((candidate) => candidate.sessionId === target);
1432
- if (session) return { sessionId: target, title: session.title ?? '暂无标题' };
1435
+ if (session) return { sessionId: target, title: session.title ?? t('暂无标题') };
1433
1436
  }
1434
- return { error: '没有找到这个会话,请用 /sessionlist 查看可用会话。' };
1437
+ return { error: t('没有找到这个会话,请用 /sessionlist 查看可用会话。') };
1435
1438
  }
1436
1439
 
1437
1440
  async #latestSessionSeq(sessionId) {
@@ -1448,14 +1451,14 @@ export class FeishuHarnessBridge {
1448
1451
  async #runWatch(key, chatId, target) {
1449
1452
  this.#ensureEventWatcher();
1450
1453
  if (typeof this.#state?.setWatch !== 'function') {
1451
- await this.#send(chatId, '当前状态存储不支持关注。');
1454
+ await this.#send(chatId, t('当前状态存储不支持关注。'));
1452
1455
  return;
1453
1456
  }
1454
1457
  let resolved;
1455
1458
  try {
1456
1459
  resolved = await this.#resolveWatchTarget(target);
1457
1460
  } catch (error) {
1458
- await this.#send(chatId, `无法解析会话:${safeErrorText(error)}`);
1461
+ await this.#send(chatId, t('无法解析会话:{message}', { message: safeErrorText(error) }));
1459
1462
  return;
1460
1463
  }
1461
1464
  if (resolved.error) {
@@ -1465,7 +1468,7 @@ export class FeishuHarnessBridge {
1465
1468
  const existing = this.#state.watchEntries?.(key) ?? [];
1466
1469
  const existingEntry = existing.find((entry) => entry.sessionId === resolved.sessionId);
1467
1470
  if (!existingEntry && existing.length >= MAX_WATCHES_PER_KEY) {
1468
- await this.#send(chatId, `每个聊天最多关注 ${MAX_WATCHES_PER_KEY} 个会话。`);
1471
+ await this.#send(chatId, t('每个聊天最多关注 {count} 个会话。', { count: MAX_WATCHES_PER_KEY }));
1469
1472
  return;
1470
1473
  }
1471
1474
  try {
@@ -1478,10 +1481,10 @@ export class FeishuHarnessBridge {
1478
1481
  chatId,
1479
1482
  lastSeq,
1480
1483
  });
1481
- await this.#send(chatId, `已关注会话「${String(resolved.title).replace(/\s+/gu, ' ')}」,任务完成会推送结果。`);
1484
+ await this.#send(chatId, t('已关注会话「{title}」,任务完成会推送结果。', { title: String(resolved.title).replace(/\s+/gu, ' ') }));
1482
1485
  await this.#queueEventTask(() => this.#compensateSession(resolved.sessionId));
1483
1486
  } catch (error) {
1484
- await this.#send(chatId, `关注失败:${safeErrorText(error)}`);
1487
+ await this.#send(chatId, t('关注失败:{message}', { message: safeErrorText(error) }));
1485
1488
  }
1486
1489
  }
1487
1490
 
@@ -1492,15 +1495,15 @@ export class FeishuHarnessBridge {
1492
1495
  ? entries[Number(target) - 1]
1493
1496
  : entries.find((candidate) => candidate.sessionId === target);
1494
1497
  if (!entry) {
1495
- await this.#send(chatId, '关注列表里没有这个会话,回复 /watchlist 查看。');
1498
+ await this.#send(chatId, t('关注列表里没有这个会话,回复 /watchlist 查看。'));
1496
1499
  return;
1497
1500
  }
1498
1501
  try {
1499
1502
  await this.#state.removeWatch(key, entry.sessionId);
1500
1503
  this.#failedWatchSeqs.delete(`${key}\0${entry.sessionId}`);
1501
- await this.#send(chatId, `已取消关注「${String(entry.title ?? '').replace(/\s+/gu, ' ')}」。`);
1504
+ await this.#send(chatId, t('已取消关注「{title}」。', { title: String(entry.title ?? '').replace(/\s+/gu, ' ') }));
1502
1505
  } catch (error) {
1503
- await this.#send(chatId, `取消失败:${safeErrorText(error)}`);
1506
+ await this.#send(chatId, t('取消失败:{message}', { message: safeErrorText(error) }));
1504
1507
  }
1505
1508
  }
1506
1509
 
@@ -1641,64 +1644,50 @@ export class FeishuHarnessBridge {
1641
1644
  }
1642
1645
 
1643
1646
  async #deliverArtifacts(chatId, replyTo, artifacts = [], baseReceipt) {
1644
- const receipts = baseReceipt ? [baseReceipt] : [];
1645
- let failureNoticeVisible = false;
1646
- for (const artifact of artifacts) {
1647
- this.#signal?.throwIfAborted();
1648
- try {
1649
- if (typeof this.#channel?.sendFile !== 'function') {
1650
- const unavailable = new Error('Feishu file delivery is unavailable');
1651
- unavailable.code = 'artifact-provider-unavailable';
1652
- throw unavailable;
1653
- }
1654
- const file = await materializeOutboundArtifact(artifact, {
1655
- signal: this.#signal,
1656
- });
1657
- receipts.push(await this.#channel.sendFile(chatId, file, {
1658
- replyTo,
1659
- signal: this.#signal,
1660
- }));
1661
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
1662
- } catch (error) {
1663
- if (this.#signal?.aborted) throw error;
1664
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
1665
- this.#logger.warn?.(
1666
- `[dsh-feishu] result file delivery failed (${error?.code ?? 'unknown'})`,
1667
- );
1668
- let noticeMessageId = null;
1669
- try {
1670
- noticeMessageId = await this.#send(chatId, artifactFailureText(artifact?.fileName, error));
1671
- failureNoticeVisible = true;
1672
- } catch {
1673
- this.#logger.warn?.('[dsh-feishu] unable to send the safe result-file failure notice');
1674
- }
1675
- receipts.push(createArtifactFailureReceipt({
1676
- artifactId: artifact?.artifactId ?? 'unknown',
1677
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
1678
- error,
1679
- providerMessageIds: noticeMessageId ? [noticeMessageId] : [],
1680
- }));
1681
- } finally {
1682
- releaseOutboundArtifact(artifact);
1683
- }
1684
- }
1685
- if (receipts.length === 0) {
1647
+ const delivery = await deliverOutboundArtifacts({
1648
+ artifacts,
1649
+ baseReceipt,
1650
+ deliveryId: baseReceipt?.deliveryId ?? artifacts[0]?.deliveryKey ?? replyTo,
1651
+ aggregatePresentation: baseReceipt ? 'feishu-text-and-files' : 'feishu-files',
1652
+ channelKey: 'feishu',
1653
+ signal: this.#signal,
1654
+ sendImage: typeof this.#channel?.sendImage === 'function'
1655
+ ? (file) => this.#channel.sendImage(chatId, file, {
1656
+ replyTo,
1657
+ signal: this.#signal,
1658
+ })
1659
+ : undefined,
1660
+ sendFile: typeof this.#channel?.sendFile === 'function'
1661
+ ? (file) => this.#channel.sendFile(chatId, file, {
1662
+ replyTo,
1663
+ signal: this.#signal,
1664
+ })
1665
+ : undefined,
1666
+ sendFailureNotice: async (artifact, error) => ({
1667
+ messageId: await this.#send(
1668
+ chatId,
1669
+ artifactFailureText(artifact?.fileName, error),
1670
+ ),
1671
+ }),
1672
+ logger: this.#logger,
1673
+ });
1674
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
1675
+ + delivery.artifactsSent;
1676
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
1677
+ + delivery.artifactSendErrors;
1678
+ if (!delivery.receipt) {
1686
1679
  return {
1687
1680
  receipt: createDeliveryReceipt({
1688
1681
  deliveryId: replyTo,
1689
1682
  presentation: 'feishu-files',
1690
1683
  }),
1691
- failureNoticeVisible,
1684
+ failureNoticeVisible: delivery.failureNoticeVisible,
1692
1685
  };
1693
1686
  }
1694
- const receipt = receipts.length === 1
1695
- ? receipts[0]
1696
- : mergeDeliveryReceipts({
1697
- deliveryId: baseReceipt?.deliveryId ?? artifacts[0]?.deliveryKey ?? replyTo,
1698
- presentation: baseReceipt ? 'feishu-text-and-files' : 'feishu-files',
1699
- receipts,
1700
- });
1701
- return { receipt, failureNoticeVisible };
1687
+ return {
1688
+ receipt: delivery.receipt,
1689
+ failureNoticeVisible: delivery.failureNoticeVisible,
1690
+ };
1702
1691
  }
1703
1692
 
1704
1693
  async #answerWithStream(event, key, message) {
@@ -1896,18 +1885,18 @@ export class FeishuHarnessBridge {
1896
1885
 
1897
1886
  const text = extractText(event);
1898
1887
  if (!text) {
1899
- await this.#send(event.message.chat_id, '请用文字回答当前问题。');
1888
+ await this.#send(event.message.chat_id, t('请用文字回答当前问题。'));
1900
1889
  return;
1901
1890
  }
1902
1891
 
1903
1892
  const pending = this.#pendingInteractions.get(key);
1904
1893
  if (!pending || pending !== expected || pending.submitting) {
1905
1894
  if (this.#isResolvedQuestionReply(event, key)) {
1906
- 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);
1907
1896
  return;
1908
1897
  }
1909
1898
  if (claimed && (!pending || pending !== expected)) {
1910
- await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT);
1899
+ await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT());
1911
1900
  return;
1912
1901
  }
1913
1902
  return this.#enqueueMessage(event, messageId, key, processingReaction, {
@@ -1965,7 +1954,7 @@ export class FeishuHarnessBridge {
1965
1954
  if (error?.code === 'interaction-not-pending') {
1966
1955
  this.#rememberResolvedInteraction(key, pending);
1967
1956
  this.#clearPendingInteraction(key, pending.interactionId);
1968
- 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);
1969
1958
  return;
1970
1959
  }
1971
1960
  pending.submitting = false;
@@ -1973,7 +1962,7 @@ export class FeishuHarnessBridge {
1973
1962
  pending.index -= 1;
1974
1963
  this.#status.lastError = '回答提交失败。';
1975
1964
  this.#logger.error?.('[dsh-feishu] failed to answer a Harness interaction');
1976
- await this.#send(event.message.chat_id, '回答提交失败,请重新发送当前问题的答案。')
1965
+ await this.#send(event.message.chat_id, t('回答提交失败,请重新发送当前问题的答案。'))
1977
1966
  .catch(() => undefined);
1978
1967
  }
1979
1968
  }
@@ -2018,7 +2007,7 @@ export class FeishuHarnessBridge {
2018
2007
  });
2019
2008
  await this.#send(
2020
2009
  chatId,
2021
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
2010
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
2022
2011
  ).catch(() => undefined);
2023
2012
  return;
2024
2013
  }
@@ -2120,7 +2109,7 @@ export class FeishuHarnessBridge {
2120
2109
  await this.#state.markSeen(messageId);
2121
2110
  this.#status.lastMessageAt = new Date().toISOString();
2122
2111
  this.#status.messagesReceived += 1;
2123
- 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);
2124
2113
  }
2125
2114
 
2126
2115
  #takePendingInteraction(key, interactionId) {
@@ -2160,10 +2149,10 @@ export class FeishuHarnessBridge {
2160
2149
  #progressText(update) {
2161
2150
  if (update.type === 'text' && update.text) return update.text;
2162
2151
  if (update.type === 'tool') {
2163
- if (update.name === 'web_search') return '_正在搜索网络并整理信息…_';
2164
- return `_正在使用 ${update.name || '工具'}…_`;
2152
+ if (update.name === 'web_search') return t('_正在搜索网络并整理信息…_');
2153
+ return t('_正在使用 {name}…_', { name: update.name || t('工具') });
2165
2154
  }
2166
- return `_${update.text || '正在处理…'}_`;
2155
+ return t('_{text}_', { text: update.text || t('正在处理…') });
2167
2156
  }
2168
2157
 
2169
2158
  async #addReaction(messageId, emojiType) {