@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
@@ -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 {
@@ -32,14 +33,10 @@ import {
32
33
  harnessQuestionText,
33
34
  validHarnessQuestion,
34
35
  } from './harness-question.mjs';
36
+ import { deliverOutboundArtifacts } from './semantic/artifact-delivery.mjs';
35
37
  import {
36
- materializeOutboundArtifact,
37
- releaseOutboundArtifact,
38
- } from './semantic/artifact.mjs';
39
- import {
40
- createArtifactFailureReceipt,
41
38
  createDeliveryReceipt,
42
- mergeDeliveryReceipts,
39
+ createTextDeliveryBlock,
43
40
  providerMessageIdsFor,
44
41
  } from './semantic/delivery.mjs';
45
42
 
@@ -59,38 +56,38 @@ function canClaimInteractionReply(message, pending, senderId) {
59
56
  }
60
57
 
61
58
  function artifactFailureText(fileName, error, descriptor) {
62
- const name = String(fileName ?? '结果文件')
59
+ const name = String(fileName ?? t('结果文件'))
63
60
  .replace(/[\r\n]+/g, ' ')
64
61
  .trim()
65
- .slice(0, 255) || '结果文件';
62
+ .slice(0, 255) || t('结果文件');
66
63
  switch (error?.code) {
67
64
  case 'artifact-delivery-uncertain':
68
- return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
65
+ return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
69
66
  case 'artifact-permission-required':
70
67
  if (descriptor?.key === 'slack') {
71
- return `结果文件「${name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。`;
68
+ return t('结果文件「{name}」已生成,但 Slack 应用缺少 files:write 权限。请更新 Manifest、重新安装应用并重新连接机器人后重试。', { name });
72
69
  }
73
70
  if (descriptor?.key === 'discord') {
74
- return `结果文件「${name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。`;
71
+ return t('结果文件「{name}」已生成,但机器人缺少 Discord 的 Send Messages、Attach Files 或 Read Message History 权限。', { name });
75
72
  }
76
73
  if (descriptor?.key === 'telegram') {
77
- return `结果文件「${name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。`;
74
+ return t('结果文件「{name}」已生成,但 Telegram 不允许机器人在当前聊天发送文档,请检查聊天权限。', { name });
78
75
  }
79
- return `结果文件「${name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。`;
76
+ return t('结果文件「{name}」已生成,但当前机器人没有文件发送权限,请检查渠道权限。', { name });
80
77
  case 'artifact-too-large':
81
- return `结果文件「${name}」超过当前渠道大小上限,未发送。`;
78
+ return t('结果文件「{name}」超过当前渠道大小上限,未发送。', { name });
82
79
  case 'artifact-empty':
83
- return `结果文件「${name}」为空,未发送。`;
80
+ return t('结果文件「{name}」为空,未发送。', { name });
84
81
  case 'artifact-invalid':
85
82
  case 'artifact-changed':
86
83
  case 'artifact-unavailable':
87
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
84
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
88
85
  case 'artifact-rate-limited':
89
- return `结果文件「${name}」暂时被当前渠道限流,未能发送,请稍后重试。`;
86
+ return t('结果文件「{name}」暂时被当前渠道限流,未能发送,请稍后重试。', { name });
90
87
  case 'artifact-provider-rejected':
91
- return `结果文件「${name}」已生成,但当前渠道拒绝了该文件或文件消息。`;
88
+ return t('结果文件「{name}」已生成,但当前渠道拒绝了该文件或文件消息。', { name });
92
89
  default:
93
- return `结果文件「${name}」已生成,但当前渠道暂时未能发送,请稍后重试。`;
90
+ return t('结果文件「{name}」已生成,但当前渠道暂时未能发送,请稍后重试。', { name });
94
91
  }
95
92
  }
96
93
 
@@ -325,7 +322,7 @@ export class TextHarnessBridge {
325
322
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
326
323
  this.#status.lastError = error?.message ?? String(error);
327
324
  this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a command:`, error);
328
- await this.#bot.sendText(target, '消息处理失败,请稍后重试。').catch(() => undefined);
325
+ await this.#bot.sendText(target, t('消息处理失败,请稍后重试。')).catch(() => undefined);
329
326
  }
330
327
  }
331
328
 
@@ -333,79 +330,35 @@ export class TextHarnessBridge {
333
330
  return sendRememberedConnectionTest({
334
331
  state: this.#state,
335
332
  text,
336
- channelLabel: `${this.#descriptor.label}机器人`,
333
+ channelLabel: t('{label}机器人', { label: this.#descriptor.label }),
337
334
  send: (target, message) => this.#bot.sendText(target, message),
338
335
  });
339
336
  }
340
337
 
341
338
  async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
342
- const receipts = baseReceipt ? [baseReceipt] : [];
343
- let userVisible = Boolean(baseReceipt);
344
- for (const artifact of artifacts) {
345
- this.#signal?.throwIfAborted();
346
- try {
347
- if (typeof this.#bot.sendFile !== 'function') {
348
- const unavailable = new Error('Native file delivery is unavailable');
349
- unavailable.code = 'artifact-provider-unavailable';
350
- throw unavailable;
351
- }
352
- const file = await materializeOutboundArtifact(artifact, {
353
- signal: this.#signal,
354
- });
355
- this.#signal?.throwIfAborted();
356
- const result = await this.#bot.sendFile(target, file);
357
- receipts.push(createDeliveryReceipt({
358
- deliveryId: file.deliveryKey,
359
- presentation: `${this.#descriptor.key}-file`,
360
- providerMessageIds: providerMessageIdsFor(result),
361
- artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
362
- }));
363
- userVisible = true;
364
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
365
- } catch (error) {
366
- if (this.#signal?.aborted) throw error;
367
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
368
- this.#logger.warn?.(
369
- `[dsh-im:${this.#descriptor.key}] result file delivery failed (${error?.code ?? 'unknown'})`,
370
- );
371
- let providerMessageIds = [];
372
- let noticeSent = false;
373
- try {
374
- const notice = await this.#bot.sendText(
375
- target,
376
- artifactFailureText(artifact?.fileName, error, this.#descriptor),
377
- );
378
- providerMessageIds = providerMessageIdsFor(notice);
379
- noticeSent = true;
380
- } catch {
381
- this.#logger.warn?.(
382
- `[dsh-im:${this.#descriptor.key}] unable to send the safe result-file failure notice`,
383
- );
384
- }
385
- const failureReceipt = createArtifactFailureReceipt({
386
- artifactId: artifact?.artifactId ?? 'unknown',
387
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
388
- error,
389
- providerMessageIds,
390
- });
391
- receipts.push(failureReceipt);
392
- if (noticeSent || failureReceipt.artifacts[0]?.outcome === 'unknown') userVisible = true;
393
- } finally {
394
- releaseOutboundArtifact(artifact);
395
- }
396
- }
397
- const receipt = receipts.length === 0
398
- ? null
399
- : receipts.length === 1
400
- ? receipts[0]
401
- : mergeDeliveryReceipts({
402
- deliveryId: replyTo,
403
- presentation: baseReceipt
404
- ? `${this.#descriptor.key}-text-and-files`
405
- : `${this.#descriptor.key}-files`,
406
- receipts,
407
- });
408
- return { receipt, userVisible };
339
+ const delivery = await deliverOutboundArtifacts({
340
+ artifacts,
341
+ baseReceipt,
342
+ deliveryId: replyTo,
343
+ channelKey: this.#descriptor.key,
344
+ signal: this.#signal,
345
+ sendImage: typeof this.#bot.sendImage === 'function'
346
+ ? (file) => this.#bot.sendImage(target, file)
347
+ : undefined,
348
+ sendFile: typeof this.#bot.sendFile === 'function'
349
+ ? (file) => this.#bot.sendFile(target, file)
350
+ : undefined,
351
+ sendFailureNotice: (artifact, error) => this.#bot.sendText(
352
+ target,
353
+ artifactFailureText(artifact?.fileName, error, this.#descriptor),
354
+ ),
355
+ logger: this.#logger,
356
+ });
357
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
358
+ + delivery.artifactsSent;
359
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
360
+ + delivery.artifactSendErrors;
361
+ return { receipt: delivery.receipt, userVisible: delivery.userVisible };
409
362
  }
410
363
 
411
364
  async #process(message, messageId, senderId, conversationKey, {
@@ -421,6 +374,7 @@ export class TextHarnessBridge {
421
374
  const target = message.replyTarget;
422
375
  const text = cleanText(message.content);
423
376
  let stream = null;
377
+ let semanticStream = false;
424
378
  try {
425
379
  this.#signal?.throwIfAborted();
426
380
  if (message.kind === 'group' && message.addressed !== true) {
@@ -431,38 +385,38 @@ export class TextHarnessBridge {
431
385
  const hasImages = hasInboundImages(message);
432
386
  const hasFiles = hasInboundFiles(message);
433
387
  if (!text && !hasImages && !hasFiles) {
434
- await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
388
+ await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
435
389
  return;
436
390
  }
437
391
  const command = text.toLowerCase();
438
392
  if (!hasImages && !hasFiles && command === '/help') {
439
393
  await this.#bot.sendText(target, [
440
- `${this.#descriptor.label}机器人已连接 DeepSeek Harness。`,
394
+ t('{label}机器人已连接 DeepSeek Harness。', { label: this.#descriptor.label }),
441
395
  '',
442
- '直接发送文字、图片或文件即可继续当前会话。',
443
- '/new 开启一个全新会话',
444
- '/compact 压缩当前会话的较早上下文',
445
- '/workspace 工作区绝对路径 切换工作区',
446
- '/workspacelist 列出工作区绝对路径',
447
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
448
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
449
- '/models 按序号列出所有可用模型',
450
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
451
- '示例:先发 /models,再发 /model 2',
452
- '/presetlist 按序号列出可用 Agent Preset',
453
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
454
- '纯数字 ID:/preset id:<ID>',
455
- '/preset --default 跟随 Host 默认',
456
- '/stop 停止当前任务',
457
- '/steer 补充指令 纠偏当前任务',
458
- '/status 检查连接状态',
459
- '/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 显示本帮助'),
460
414
  ].join('\n'));
461
415
  return;
462
416
  }
463
417
  if (!hasImages && !hasFiles && command === '/status') {
464
418
  await this.#harness.ensureRunning({ signal: this.#signal });
465
- await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
419
+ await this.#bot.sendText(target, t('{label}机器人与 DeepSeek Harness 连接正常。', { label: this.#descriptor.label }));
466
420
  return;
467
421
  }
468
422
  const workspaceCommand = !hasImages && !hasFiles
@@ -476,7 +430,7 @@ export class TextHarnessBridge {
476
430
  }
477
431
  if (!hasImages && !hasFiles && command === '/new') {
478
432
  await this.#state.clearSession(conversationKey);
479
- await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
433
+ await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
480
434
  return;
481
435
  }
482
436
  const compactCommand = !hasImages && !hasFiles
@@ -497,7 +451,17 @@ export class TextHarnessBridge {
497
451
  this.#logger.warn?.(`[dsh-im:${this.#descriptor.key}] typing indicator failed:`, error);
498
452
  });
499
453
  let streamFinished = false;
500
- 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') {
501
465
  try {
502
466
  stream = await this.#bot.openStream(target);
503
467
  } catch (error) {
@@ -524,35 +488,48 @@ export class TextHarnessBridge {
524
488
  control: { owner: this, key: conversationKey },
525
489
  onUpdate: stream ? async (update) => {
526
490
  const progress = update.type === 'text' ? update.text
527
- : update.type === 'tool' ? `正在使用${update.name}…` : update.text;
528
- 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
+ }
529
498
  } : undefined,
530
499
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
531
500
  key: conversationKey,
532
501
  actor: senderId,
533
502
  target,
534
- requiresMention: message.kind === 'group',
503
+ requiresMention: message.kind === 'group' && message.requiresMention !== false,
535
504
  }),
536
505
  onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
537
506
  files: message.files,
538
507
  },
539
508
  });
540
- const visibleAnswer = !cleanText(answer) && artifacts.length > 0
541
- ? FILE_ONLY_COMPLETION_TEXT
509
+ const fileOnlyCompletion = !cleanText(answer) && artifacts.length > 0;
510
+ const visibleAnswer = fileOnlyCompletion
511
+ ? t(FILE_ONLY_COMPLETION_TEXT)
542
512
  : answer;
513
+ const answerFormat = fileOnlyCompletion ? 'plain' : 'markdown';
543
514
  let textDeliveryError = null;
544
515
  let textReceipt = null;
545
516
  if (stream) {
546
517
  try {
547
- const result = await stream.finish(visibleAnswer);
518
+ const result = await stream.finish(semanticStream
519
+ ? createTextDeliveryBlock(visibleAnswer, answerFormat)
520
+ : visibleAnswer);
548
521
  streamFinished = true;
549
522
  textReceipt = createDeliveryReceipt({
550
523
  deliveryId: messageId,
551
- presentation: `${this.#descriptor.key}-stream`,
524
+ presentation: result?.presentation
525
+ ?? stream.presentation
526
+ ?? `${this.#descriptor.key}-stream`,
552
527
  providerMessageIds: [
553
528
  ...providerMessageIdsFor(stream),
554
529
  ...providerMessageIdsFor(result),
555
530
  ],
531
+ deliveryOutcome: result?.deliveryOutcome,
532
+ reason: result?.reason,
556
533
  });
557
534
  } catch (error) {
558
535
  stream.cancel?.();
@@ -564,40 +541,74 @@ export class TextHarnessBridge {
564
541
  }
565
542
  if (!streamFinished) {
566
543
  try {
567
- 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);
568
550
  textReceipt = createDeliveryReceipt({
569
551
  deliveryId: messageId,
570
- presentation: `${this.#descriptor.key}-text`,
552
+ presentation: result?.presentation ?? `${this.#descriptor.key}-text`,
571
553
  providerMessageIds: providerMessageIdsFor(result),
554
+ deliveryOutcome: result?.deliveryOutcome,
555
+ reason: result?.reason,
572
556
  });
573
557
  } catch (error) {
574
558
  textDeliveryError = error;
575
559
  }
576
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
+ }
577
566
  // A failed final text must not discard an already registered result file.
578
567
  // Settle the independent attachment path before surfacing the text error.
579
568
  const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
580
- if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
581
- this.#status.messagesReplied += 1;
582
- this.#status.lastReplyAt = new Date().toISOString();
583
- 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
+ }
584
578
  return delivery.receipt;
585
579
  } catch (error) {
586
580
  if (error?.code === 'turn-stopped') {
587
581
  if (stream) {
588
582
  try {
589
- await stream.finish('已停止。');
583
+ await stream.finish(t('已停止。'));
590
584
  } catch {
591
585
  stream.cancel?.();
592
586
  }
593
587
  }
594
588
  return;
595
589
  }
596
- stream?.cancel?.();
597
- if (this.#signal?.aborted) return;
590
+ if (this.#signal?.aborted) {
591
+ stream?.cancel?.();
592
+ return;
593
+ }
598
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
+ };
599
608
  const imageErrorMessage = imagePromptUserMessage(error);
600
609
  if (imageErrorMessage) {
610
+ if (await presentStreamFailure(imageErrorMessage)) return;
611
+ stream?.cancel?.();
601
612
  try {
602
613
  await this.#bot.sendText(target, imageErrorMessage);
603
614
  } catch (sendError) {
@@ -610,6 +621,8 @@ export class TextHarnessBridge {
610
621
  }
611
622
  const fileErrorMessage = inboundFileUserMessage(error);
612
623
  if (fileErrorMessage) {
624
+ if (await presentStreamFailure(fileErrorMessage)) return;
625
+ stream?.cancel?.();
613
626
  try {
614
627
  await this.#bot.sendText(target, fileErrorMessage);
615
628
  } catch (sendError) {
@@ -621,14 +634,19 @@ export class TextHarnessBridge {
621
634
  return;
622
635
  }
623
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?.();
624
641
  try {
625
- await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
642
+ await this.#bot.sendText(target, t('消息处理失败,请稍后重试。'));
626
643
  } catch (sendError) {
627
644
  this.#logger.error?.(
628
645
  `[dsh-im:${this.#descriptor.key}] failed to send the safe error reply:`,
629
646
  sendError,
630
647
  );
631
648
  }
649
+ return error.deliveryReceipt;
632
650
  } finally {
633
651
  await Promise.allSettled([
634
652
  this.#cancelPendingInteraction(conversationKey),
@@ -664,7 +682,7 @@ export class TextHarnessBridge {
664
682
  const text = cleanText(message.content);
665
683
  if (!text || hasInboundImages(message) || hasInboundFiles(message)) {
666
684
  try {
667
- await this.#bot.sendText(target, '请用文字回答当前问题。');
685
+ await this.#bot.sendText(target, t('请用文字回答当前问题。'));
668
686
  } catch (error) {
669
687
  this.#logger.error?.(
670
688
  `[dsh-im:${this.#descriptor.key}] failed to reject a non-text interaction reply:`,
@@ -692,7 +710,7 @@ export class TextHarnessBridge {
692
710
  try {
693
711
  await this.#presentInteraction(pending);
694
712
  } catch (error) {
695
- this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
713
+ this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
696
714
  this.#logger.error?.(
697
715
  `[dsh-im:${this.#descriptor.key}] failed to retry an interaction question:`,
698
716
  error,
@@ -730,7 +748,7 @@ export class TextHarnessBridge {
730
748
  try {
731
749
  await this.#presentInteraction(pending);
732
750
  } catch (error) {
733
- this.#status.lastError = `${this.#descriptor.label}交互问题发送失败。`;
751
+ this.#status.lastError = t('{label}交互问题发送失败。', { label: this.#descriptor.label });
734
752
  this.#logger.error?.(
735
753
  `[dsh-im:${this.#descriptor.key}] failed to send the next interaction question:`,
736
754
  error,
@@ -756,7 +774,7 @@ export class TextHarnessBridge {
756
774
  this.#clearPendingInteraction(key, pending.interactionId);
757
775
  if (this.#signal?.aborted) return;
758
776
  try {
759
- await this.#bot.sendText(target, INTERACTION_RESOLVED_TEXT);
777
+ await this.#bot.sendText(target, t(INTERACTION_RESOLVED_TEXT));
760
778
  } catch (sendError) {
761
779
  this.#logger.error?.(
762
780
  `[dsh-im:${this.#descriptor.key}] failed to send an expired interaction notice:`,
@@ -769,13 +787,13 @@ export class TextHarnessBridge {
769
787
  pending.submitting = false;
770
788
  pending.answers.pop();
771
789
  pending.index -= 1;
772
- this.#status.lastError = '回答提交失败。';
790
+ this.#status.lastError = t('回答提交失败。');
773
791
  this.#logger.error?.(
774
792
  `[dsh-im:${this.#descriptor.key}] failed to answer a Harness interaction:`,
775
793
  error,
776
794
  );
777
795
  try {
778
- await this.#bot.sendText(target, '回答提交失败,请重新发送当前问题的答案。');
796
+ await this.#bot.sendText(target, t('回答提交失败,请重新发送当前问题的答案。'));
779
797
  } catch (sendError) {
780
798
  this.#logger.error?.(
781
799
  `[dsh-im:${this.#descriptor.key}] failed to send an interaction retry notice:`,
@@ -822,7 +840,7 @@ export class TextHarnessBridge {
822
840
  try {
823
841
  await this.#bot.sendText(
824
842
  target,
825
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
843
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
826
844
  );
827
845
  } catch (error) {
828
846
  this.#logger.error?.(
@@ -923,7 +941,7 @@ export class TextHarnessBridge {
923
941
  this.#status.lastMessageAt = new Date().toISOString();
924
942
  }
925
943
  try {
926
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
944
+ await this.#bot.sendText(message.replyTarget, t(INTERACTION_RESOLVED_TEXT));
927
945
  } catch (error) {
928
946
  this.#logger.error?.(
929
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
  },