@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
@@ -1,6 +1,8 @@
1
1
  export const DELIVERY_RECEIPT_SCHEMA_VERSION = 1;
2
2
 
3
3
  const ARTIFACT_OUTCOMES = new Set(['sent', 'rejected', 'failed', 'unknown']);
4
+ const DELIVERY_OUTCOMES = new Set(['sent', 'failed', 'unknown']);
5
+ const TEXT_FORMATS = new Set(['plain', 'markdown']);
4
6
  const REJECTED_ARTIFACT_ERRORS = new Set([
5
7
  'artifact-changed',
6
8
  'artifact-context-required',
@@ -52,6 +54,24 @@ function requiredString(value, name) {
52
54
  return value;
53
55
  }
54
56
 
57
+ export function createTextDeliveryBlock(value, format = 'plain') {
58
+ const block = typeof value === 'string'
59
+ ? { kind: 'text', text: value, format }
60
+ : value;
61
+ if (!block || typeof block !== 'object' || Array.isArray(block)
62
+ || block.kind !== 'text' || typeof block.text !== 'string' || !block.text.trim()) {
63
+ throw new TypeError('text delivery block must contain non-empty text');
64
+ }
65
+ if (!TEXT_FORMATS.has(block.format)) {
66
+ throw new TypeError('text delivery format must be plain or markdown');
67
+ }
68
+ return Object.freeze({
69
+ kind: 'text',
70
+ text: block.text,
71
+ format: block.format,
72
+ });
73
+ }
74
+
55
75
  function providerIds(values) {
56
76
  if (!Array.isArray(values)) throw new TypeError('providerMessageIds must be an array');
57
77
  const ids = [];
@@ -98,12 +118,22 @@ export function createDeliveryReceipt({
98
118
  presentation,
99
119
  providerMessageIds = [],
100
120
  artifacts = [],
121
+ deliveryOutcome,
122
+ reason,
101
123
  }) {
124
+ if (deliveryOutcome !== undefined && !DELIVERY_OUTCOMES.has(deliveryOutcome)) {
125
+ throw new TypeError('deliveryOutcome must be sent, failed, or unknown');
126
+ }
127
+ const normalizedReason = reason === undefined
128
+ ? undefined
129
+ : requiredString(reason, 'delivery reason');
102
130
  return Object.freeze({
103
131
  schemaVersion: DELIVERY_RECEIPT_SCHEMA_VERSION,
104
132
  deliveryId: requiredString(deliveryId, 'deliveryId'),
105
133
  presentation: requiredString(presentation, 'presentation'),
106
134
  providerMessageIds: providerIds(providerMessageIds),
135
+ ...(deliveryOutcome === undefined ? {} : { deliveryOutcome }),
136
+ ...(normalizedReason === undefined ? {} : { reason: normalizedReason }),
107
137
  artifacts: artifactResults(artifacts),
108
138
  });
109
139
  }
@@ -137,11 +167,17 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
137
167
  }
138
168
  const messageIds = [];
139
169
  const artifacts = new Map();
170
+ let deliveryOutcome;
171
+ let reason;
140
172
  for (const receipt of receipts) {
141
173
  if (!receipt || receipt.schemaVersion !== DELIVERY_RECEIPT_SCHEMA_VERSION) {
142
174
  throw new TypeError('receipt must use DeliveryReceipt schema version 1');
143
175
  }
144
176
  messageIds.push(...(receipt.providerMessageIds ?? []));
177
+ if (deliveryOutcome === undefined && receipt.deliveryOutcome !== undefined) {
178
+ deliveryOutcome = receipt.deliveryOutcome;
179
+ reason = receipt.reason;
180
+ }
145
181
  for (const artifact of receipt.artifacts ?? []) {
146
182
  artifacts.set(artifact.artifactId, artifact);
147
183
  }
@@ -150,6 +186,8 @@ export function mergeDeliveryReceipts({ deliveryId, presentation, receipts }) {
150
186
  deliveryId,
151
187
  presentation,
152
188
  providerMessageIds: messageIds,
189
+ deliveryOutcome,
190
+ reason,
153
191
  artifacts: [...artifacts.values()],
154
192
  });
155
193
  }
@@ -1,3 +1,4 @@
1
+ import { t } from './i18n.mjs';
1
2
  import { runWorkspaceCommand } from './workspace-command.mjs';
2
3
  import { runCompactCommand } from './compact-command.mjs';
3
4
  import {
@@ -35,6 +36,7 @@ import {
35
36
  import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
36
37
  import {
37
38
  createDeliveryReceipt,
39
+ createTextDeliveryBlock,
38
40
  providerMessageIdsFor,
39
41
  } from './semantic/delivery.mjs';
40
42
 
@@ -54,38 +56,38 @@ function canClaimInteractionReply(message, pending, senderId) {
54
56
  }
55
57
 
56
58
  function artifactFailureText(fileName, error, descriptor) {
57
- const name = String(fileName ?? '结果文件')
59
+ const name = String(fileName ?? t('结果文件'))
58
60
  .replace(/[\r\n]+/g, ' ')
59
61
  .trim()
60
- .slice(0, 255) || '结果文件';
62
+ .slice(0, 255) || t('结果文件');
61
63
  switch (error?.code) {
62
64
  case 'artifact-delivery-uncertain':
63
- return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
65
+ return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
64
66
  case 'artifact-permission-required':
65
67
  if (descriptor?.key === 'slack') {
66
- return `结果文件「${name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。`;
68
+ return t('结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。', { name });
67
69
  }
68
70
  if (descriptor?.key === 'discord') {
69
- return `结果文件「${name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。`;
71
+ return t('结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。', { name });
70
72
  }
71
73
  if (descriptor?.key === 'telegram') {
72
- return `结果文件「${name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。`;
74
+ return t('结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。', { name });
73
75
  }
74
- return `结果文件「${name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。`;
76
+ return t('结果文件「{name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。', { name });
75
77
  case 'artifact-too-large':
76
- return `结果文件「${name}」超过当前渠道大小上限,未发送。`;
78
+ return t('结果文件「{name}」超过当前渠道大小上限,未发送。', { name });
77
79
  case 'artifact-empty':
78
- return `结果文件「${name}」为空,未发送。`;
80
+ return t('结果文件「{name}」为空,未发送。', { name });
79
81
  case 'artifact-invalid':
80
82
  case 'artifact-changed':
81
83
  case 'artifact-unavailable':
82
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
84
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
83
85
  case 'artifact-rate-limited':
84
- return `结果文件「${name}」暂时被当前渠道限流,未能发送,请稍后重试。`;
86
+ return t('结果文件「{name}」暂时被当前渠道限流,未能发送,请稍后重试。', { name });
85
87
  case 'artifact-provider-rejected':
86
- return `结果文件「${name}」已生成,但当前渠道拒绝了该文件或文件消息。`;
88
+ return t('结果文件「{name}」已生成,但当前渠道拒绝了该文件或文件消息。', { name });
87
89
  default:
88
- return `结果文件「${name}」已生成,但当前渠道暂时未能发送,请稍后重试。`;
90
+ return t('结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。', { name });
89
91
  }
90
92
  }
91
93
 
@@ -320,7 +322,7 @@ export class TextHarnessBridge {
320
322
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
321
323
  this.#status.lastError = error?.message ?? String(error);
322
324
  this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a command:`, error);
323
- await this.#bot.sendText(target, '消息处理失败,请稍后重试。').catch(() => undefined);
325
+ await this.#bot.sendText(target, t('消息处理失败,请稍后重试。')).catch(() => undefined);
324
326
  }
325
327
  }
326
328
 
@@ -328,7 +330,7 @@ export class TextHarnessBridge {
328
330
  return sendRememberedConnectionTest({
329
331
  state: this.#state,
330
332
  text,
331
- channelLabel: `${this.#descriptor.label}机器人`,
333
+ channelLabel: t('{label}机器人', { label: this.#descriptor.label }),
332
334
  send: (target, message) => this.#bot.sendText(target, message),
333
335
  });
334
336
  }
@@ -372,6 +374,7 @@ export class TextHarnessBridge {
372
374
  const target = message.replyTarget;
373
375
  const text = cleanText(message.content);
374
376
  let stream = null;
377
+ let semanticStream = false;
375
378
  try {
376
379
  this.#signal?.throwIfAborted();
377
380
  if (message.kind === 'group' && message.addressed !== true) {
@@ -382,38 +385,38 @@ export class TextHarnessBridge {
382
385
  const hasImages = hasInboundImages(message);
383
386
  const hasFiles = hasInboundFiles(message);
384
387
  if (!text && !hasImages && !hasFiles) {
385
- await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
388
+ await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
386
389
  return;
387
390
  }
388
391
  const command = text.toLowerCase();
389
392
  if (!hasImages && !hasFiles && command === '/help') {
390
393
  await this.#bot.sendText(target, [
391
- `${this.#descriptor.label}机器人已连接 DeepSeek Harness。`,
394
+ t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
392
395
  '',
393
- '直接发送文字、图片或文件即可继续当前会话。',
394
- '/new 开启一个全新会话',
395
- '/compact 压缩当前会话的较早上下文',
396
- '/workspace 工作区绝对路径 切换工作区',
397
- '/workspacelist 列出工作区绝对路径',
398
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
399
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
400
- '/models 按序号列出所有可用模型',
401
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
402
- '示例:先发 /models,再发 /model 2',
403
- '/presetlist 按序号列出可用 Agent Preset',
404
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
405
- '纯数字 ID:/preset id:<ID>',
406
- '/preset --default 跟随 Host 默认',
407
- '/stop 停止当前任务',
408
- '/steer 补充指令 纠偏当前任务',
409
- '/status 检查连接状态',
410
- '/help 显示本帮助',
396
+ t('直接发送文字、图片或文件即可继续当前会话。'),
397
+ t('/new 开启一个全新会话'),
398
+ t('/compact 压缩当前会话的较早上下文'),
399
+ t('/workspace 工作区绝对路径 切换工作区'),
400
+ t('/workspacelist 列出工作区绝对路径'),
401
+ t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
402
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
403
+ t('/models 按序号列出所有可用模型'),
404
+ t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
405
+ t('示例:先发 /models,再发 /model 2'),
406
+ t('/presetlist 按序号列出可用 Agent Preset'),
407
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
408
+ t('纯数字 ID:/preset id:<ID>'),
409
+ t('/preset --default 跟随 Host 默认'),
410
+ t('/stop 停止当前任务'),
411
+ t('/steer 补充指令 纠偏当前任务'),
412
+ t('/status 检查连接状态'),
413
+ t('/help 显示本帮助'),
411
414
  ].join('\n'));
412
415
  return;
413
416
  }
414
417
  if (!hasImages && !hasFiles && command === '/status') {
415
418
  await this.#harness.ensureRunning({ signal: this.#signal });
416
- await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
419
+ await this.#bot.sendText(target, t('{label}机器人与 DeepSeek Harness 连接正常。', { label: this.#descriptor.label }));
417
420
  return;
418
421
  }
419
422
  const workspaceCommand = !hasImages && !hasFiles
@@ -427,7 +430,7 @@ export class TextHarnessBridge {
427
430
  }
428
431
  if (!hasImages && !hasFiles && command === '/new') {
429
432
  await this.#state.clearSession(conversationKey);
430
- await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
433
+ await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
431
434
  return;
432
435
  }
433
436
  const compactCommand = !hasImages && !hasFiles
@@ -448,7 +451,17 @@ export class TextHarnessBridge {
448
451
  this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
449
452
  });
450
453
  let streamFinished = false;
451
- if (typeof this.#bot.openStream === 'function') {
454
+ if (typeof this.#bot.openDeliveryStream === 'function') {
455
+ try {
456
+ stream = await this.#bot.openDeliveryStream(target);
457
+ semanticStream = true;
458
+ } catch (error) {
459
+ this.#logger.warn?.(
460
+ `[dsh-im:${this.#descriptor.key}] unable to start a semantic reply stream; using final delivery:`,
461
+ error,
462
+ );
463
+ }
464
+ } else if (typeof this.#bot.openStream === 'function') {
452
465
  try {
453
466
  stream = await this.#bot.openStream(target);
454
467
  } catch (error) {
@@ -475,35 +488,48 @@ export class TextHarnessBridge {
475
488
  control: { owner: this, key: conversationKey },
476
489
  onUpdate: stream ? async (update) => {
477
490
  const progress = update.type === 'text' ? update.text
478
- : update.type === 'tool' ? `正在使用${update.name}…` : update.text;
479
- if (progress) await stream.update(progress);
491
+ : update.type === 'tool' ? t('正在使用{name}…', { name: update.name }) : update.text;
492
+ if (progress) {
493
+ const format = update.type === 'text' ? 'markdown' : 'plain';
494
+ await stream.update(semanticStream
495
+ ? createTextDeliveryBlock(progress, format)
496
+ : progress);
497
+ }
480
498
  } : undefined,
481
499
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
482
500
  key: conversationKey,
483
501
  actor: senderId,
484
502
  target,
485
- requiresMention: message.kind === 'group',
503
+ requiresMention: message.kind === 'group' && message.requiresMention !== false,
486
504
  }),
487
505
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
488
506
  files: message.files,
489
507
  },
490
508
  });
491
- const visibleAnswer = !cleanText(answer) && artifacts.length > 0
492
- ? FILE_ONLY_COMPLETION_TEXT
509
+ const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
510
+ const visibleAnswer = fileOnlyCompletion
511
+ ? t(FILE_ONLY_COMPLETION_TEXT)
493
512
  : answer;
513
+ const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
494
514
  let textDeliveryError = null;
495
515
  let textReceipt = null;
496
516
  if (stream) {
497
517
  try {
498
- const result = await stream.finish(visibleAnswer);
518
+ const result = await stream.finish(semanticStream
519
+ ? createTextDeliveryBlock(visibleAnswer, answerFormat)
520
+ : visibleAnswer);
499
521
  streamFinished = true;
500
522
  textReceipt = createDeliveryReceipt({
501
523
  deliveryId: messageId,
502
- presentation: `${this.#descriptor.key}-stream`,
524
+ presentation: result?.presentation
525
+ ?? stream.presentation
526
+ ?? `${this.#descriptor.key}-stream`,
503
527
  providerMessageIds: [
504
528
  ...providerMessageIdsFor(stream),
505
529
  ...providerMessageIdsFor(result),
506
530
  ],
531
+ deliveryOutcome: result?.deliveryOutcome,
532
+ reason: result?.reason,
507
533
  });
508
534
  } catch (error) {
509
535
  stream.cancel?.();
@@ -515,40 +541,74 @@ export class TextHarnessBridge {
515
541
  }
516
542
  if (!streamFinished) {
517
543
  try {
518
- const result = await this.#bot.sendText(target, visibleAnswer);
544
+ const result = typeof this.#bot.sendDelivery === 'function'
545
+ ? await this.#bot.sendDelivery(
546
+ target,
547
+ createTextDeliveryBlock(visibleAnswer, answerFormat),
548
+ )
549
+ : await this.#bot.sendText(target, visibleAnswer);
519
550
  textReceipt = createDeliveryReceipt({
520
551
  deliveryId: messageId,
521
- presentation: `${this.#descriptor.key}-text`,
552
+ presentation: result?.presentation ?? `${this.#descriptor.key}-text`,
522
553
  providerMessageIds: providerMessageIdsFor(result),
554
+ deliveryOutcome: result?.deliveryOutcome,
555
+ reason: result?.reason,
523
556
  });
524
557
  } catch (error) {
525
558
  textDeliveryError = error;
526
559
  }
527
560
  }
561
+ if (textReceipt?.deliveryOutcome === 'failed') {
562
+ const reason = textReceipt.reason ?? 'text-delivery-failed';
563
+ textDeliveryError = new Error(`Final text delivery failed (${reason})`);
564
+ textDeliveryError.code = reason;
565
+ }
528
566
  // A failed final text must not discard an already registered result file.
529
567
  // Settle the independent attachment path before surfacing the text error.
530
568
  const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
531
- if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
532
- this.#status.messagesReplied += 1;
533
- this.#status.lastReplyAt = new Date().toISOString();
534
- this.#status.lastError = null;
569
+ if (textDeliveryError && !delivery.userVisible) {
570
+ textDeliveryError.deliveryReceipt = delivery.receipt;
571
+ throw textDeliveryError;
572
+ }
573
+ if (delivery.userVisible) {
574
+ this.#status.messagesReplied += 1;
575
+ this.#status.lastReplyAt = new Date().toISOString();
576
+ this.#status.lastError = null;
577
+ }
535
578
  return delivery.receipt;
536
579
  } catch (error) {
537
580
  if (error?.code === 'turn-stopped') {
538
581
  if (stream) {
539
582
  try {
540
- await stream.finish('已停止。');
583
+ await stream.finish(t('已停止。'));
541
584
  } catch {
542
585
  stream.cancel?.();
543
586
  }
544
587
  }
545
588
  return;
546
589
  }
547
- stream?.cancel?.();
548
- if (this.#signal?.aborted) return;
590
+ if (this.#signal?.aborted) {
591
+ stream?.cancel?.();
592
+ return;
593
+ }
549
594
  this.#status.lastError = error?.message ?? String(error);
595
+ const presentStreamFailure = async (text) => {
596
+ if (typeof stream?.fail !== 'function') return false;
597
+ try {
598
+ const result = await stream.fail(text);
599
+ return Boolean(result) && result.deliveryOutcome !== 'failed';
600
+ } catch (streamError) {
601
+ this.#logger.warn?.(
602
+ `[dsh-im:${this.#descriptor.key}] unable to finalize the failed stream:`,
603
+ streamError,
604
+ );
605
+ return false;
606
+ }
607
+ };
550
608
  const imageErrorMessage = imagePromptUserMessage(error);
551
609
  if (imageErrorMessage) {
610
+ if (await presentStreamFailure(imageErrorMessage)) return;
611
+ stream?.cancel?.();
552
612
  try {
553
613
  await this.#bot.sendText(target, imageErrorMessage);
554
614
  } catch (sendError) {
@@ -561,6 +621,8 @@ export class TextHarnessBridge {
561
621
  }
562
622
  const fileErrorMessage = inboundFileUserMessage(error);
563
623
  if (fileErrorMessage) {
624
+ if (await presentStreamFailure(fileErrorMessage)) return;
625
+ stream?.cancel?.();
564
626
  try {
565
627
  await this.#bot.sendText(target, fileErrorMessage);
566
628
  } catch (sendError) {
@@ -572,14 +634,19 @@ export class TextHarnessBridge {
572
634
  return;
573
635
  }
574
636
  this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
637
+ if (await presentStreamFailure('消息处理失败,请稍后重试。')) {
638
+ return error.deliveryReceipt;
639
+ }
640
+ stream?.cancel?.();
575
641
  try {
576
- await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
642
+ await this.#bot.sendText(target, t('消息处理失败,请稍后重试。'));
577
643
  } catch (sendError) {
578
644
  this.#logger.error?.(
579
645
  `[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
580
646
  sendError,
581
647
  );
582
648
  }
649
+ return error.deliveryReceipt;
583
650
  } finally {
584
651
  await Promise.allSettled([
585
652
  this.#cancelPendingInteraction(conversationKey),
@@ -615,7 +682,7 @@ export class TextHarnessBridge {
615
682
  const text = cleanText(message.content);
616
683
  if (!text || hasInboundImages(message) || hasInboundFiles(message)) {
617
684
  try {
618
- await this.#bot.sendText(target, '请用文字回答当前问题。');
685
+ await this.#bot.sendText(target, t('请用文字回答当前问题。'));
619
686
  } catch (error) {
620
687
  this.#logger.error?.(
621
688
  `[dsh-im:${this.#descriptor.key}] failed to reject a non-text interaction reply:`,
@@ -643,7 +710,7 @@ export class TextHarnessBridge {
643
710
  try {
644
711
  await this.#presentInteraction(pending);
645
712
  } catch (error) {
646
- this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
713
+ this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
647
714
  this.#logger.error?.(
648
715
  `[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
649
716
  error,
@@ -681,7 +748,7 @@ export class TextHarnessBridge {
681
748
  try {
682
749
  await this.#presentInteraction(pending);
683
750
  } catch (error) {
684
- this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
751
+ this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
685
752
  this.#logger.error?.(
686
753
  `[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
687
754
  error,
@@ -707,7 +774,7 @@ export class TextHarnessBridge {
707
774
  this.#clearPendingInteraction(key, pending.interactionId);
708
775
  if (this.#signal?.aborted) return;
709
776
  try {
710
- await this.#bot.sendText(target, INTERACTION_RESOLVED_TEXT);
777
+ await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
711
778
  } catch (sendError) {
712
779
  this.#logger.error?.(
713
780
  `[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
@@ -720,13 +787,13 @@ export class TextHarnessBridge {
720
787
  pending.submitting = false;
721
788
  pending.answers.pop();
722
789
  pending.index -= 1;
723
- this.#status.lastError = '回答提交失败。';
790
+ this.#status.lastError = t('回答提交失败。');
724
791
  this.#logger.error?.(
725
792
  `[dsh-im:${this.#descriptor.key}] failed to answer a Harness interaction:`,
726
793
  error,
727
794
  );
728
795
  try {
729
- await this.#bot.sendText(target, '回答提交失败,请重新发送当前问题的答案。');
796
+ await this.#bot.sendText(target, t('回答提交失败,请重新发送当前问题的答案。'));
730
797
  } catch (sendError) {
731
798
  this.#logger.error?.(
732
799
  `[dsh-im:${this.#descriptor.key}] failed to send an interaction retry notice:`,
@@ -773,7 +840,7 @@ export class TextHarnessBridge {
773
840
  try {
774
841
  await this.#bot.sendText(
775
842
  target,
776
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
843
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
777
844
  );
778
845
  } catch (error) {
779
846
  this.#logger.error?.(
@@ -874,7 +941,7 @@ export class TextHarnessBridge {
874
941
  this.#status.lastMessageAt = new Date().toISOString();
875
942
  }
876
943
  try {
877
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
944
+ await this.#bot.sendText(message.replyTarget, t(INTERACTION_RESOLVED_TEXT));
878
945
  } catch (error) {
879
946
  this.#logger.error?.(
880
947
  `[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
@@ -1,4 +1,5 @@
1
1
  import { connectionTestMessage } from './connection-test.mjs';
2
+ import { t } from './i18n.mjs';
2
3
 
3
4
  function cleanString(value) {
4
5
  return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -70,7 +71,9 @@ export class TokenBotController {
70
71
  if (!token) {
71
72
  this.#errors.set(config.botId, safeError(
72
73
  'missing-token',
73
- `${this.#descriptor.label}机器人凭据缺失,请移除后重新接入。`,
74
+ t('{label}机器人凭据缺失,请移除后重新接入。', {
75
+ label: this.#descriptor.label,
76
+ }),
74
77
  ));
75
78
  return;
76
79
  }
@@ -80,7 +83,9 @@ export class TokenBotController {
80
83
  } catch (error) {
81
84
  this.#errors.set(config.botId, safeError(
82
85
  'connection-failed',
83
- `${this.#descriptor.label}连接未就绪,插件会自动重试。`,
86
+ t('{label}连接未就绪,插件会自动重试。', {
87
+ label: this.#descriptor.label,
88
+ }),
84
89
  ));
85
90
  this.#logger.warn?.(
86
91
  `[dsh-im:${this.#descriptor.key}] bot ${config.botId} failed to initialize:`,
@@ -129,7 +134,9 @@ export class TokenBotController {
129
134
  } catch (error) {
130
135
  this.#errors.set(identity.botId, safeError(
131
136
  'connection-failed',
132
- `${this.#descriptor.label}机器人已接入,消息连接暂未就绪。`,
137
+ t('{label}机器人已接入,消息连接暂未就绪。', {
138
+ label: this.#descriptor.label,
139
+ }),
133
140
  ));
134
141
  this.#logger.warn?.(
135
142
  `[dsh-im:${this.#descriptor.key}] bot ${identity.botId} credential connection failed:`,
@@ -153,7 +160,9 @@ export class TokenBotController {
153
160
  } catch (error) {
154
161
  this.#errors.set(botId, safeError(
155
162
  'connection-failed',
156
- `${this.#descriptor.label}连接仍未就绪,请稍后重试。`,
163
+ t('{label}连接仍未就绪,请稍后重试。', {
164
+ label: this.#descriptor.label,
165
+ }),
157
166
  ));
158
167
  throw error;
159
168
  } finally {
@@ -181,7 +190,9 @@ export class TokenBotController {
181
190
  } catch (error) {
182
191
  this.#errors.set(botId, safeError(
183
192
  'connection-failed',
184
- `${this.#descriptor.label}连接仍未就绪,请稍后重试。`,
193
+ t('{label}连接仍未就绪,请稍后重试。', {
194
+ label: this.#descriptor.label,
195
+ }),
185
196
  ));
186
197
  throw error;
187
198
  } finally {
@@ -197,14 +208,19 @@ export class TokenBotController {
197
208
  return this.#withBotTransition(botId, async () => {
198
209
  const runtime = this.#runtimes.get(botId);
199
210
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
200
- const error = new Error(`${this.#descriptor.label}机器人尚未连接`);
211
+ const error = new Error(t('{label}机器人尚未连接', {
212
+ label: this.#descriptor.label,
213
+ }));
201
214
  error.code = 'test-target-unavailable';
202
215
  throw error;
203
216
  }
204
- const cardLabel = `${config.name}(${this.#maskPlatformId(config.platformId)})`;
217
+ const cardLabel = t('{name}{id})', {
218
+ name: config.name,
219
+ id: this.#maskPlatformId(config.platformId),
220
+ });
205
221
  await runtime.sendConnectionTest(connectionTestMessage(
206
222
  cardLabel,
207
- `${this.#descriptor.label}机器人`,
223
+ t('{label}机器人', { label: this.#descriptor.label }),
208
224
  ));
209
225
  return { sent: true };
210
226
  });
@@ -260,9 +276,14 @@ export class TokenBotController {
260
276
  },
261
277
  health: {
262
278
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
263
- summary: connected ? `${this.#descriptor.label}${this.#descriptor.connectionLabel}运行正常`
264
- : state === 'error' ? `${this.#descriptor.label}连接未就绪,插件会自动重试`
265
- : `${this.#descriptor.label}连接当前离线`,
279
+ summary: connected ? t('{label}{connectionLabel}运行正常', {
280
+ label: this.#descriptor.label,
281
+ connectionLabel: t(this.#descriptor.connectionLabel),
282
+ })
283
+ : state === 'error' ? t('{label}连接未就绪,插件会自动重试', {
284
+ label: this.#descriptor.label,
285
+ })
286
+ : t('{label}连接当前离线', { label: this.#descriptor.label }),
266
287
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
267
288
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
268
289
  },