@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
@@ -32,19 +32,19 @@ import {
32
32
  prefetchInboundFiles,
33
33
  } from '../shared/inbound-file.mjs';
34
34
  import {
35
- materializeOutboundArtifact,
36
- releaseOutboundArtifact,
37
35
  trackOutboundArtifactProviderPromise,
38
36
  } from '../shared/semantic/artifact.mjs';
37
+ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
39
38
  import {
40
- createArtifactFailureReceipt,
41
39
  createDeliveryReceipt,
42
- mergeDeliveryReceipts,
43
40
  providerMessageIdsFor,
44
41
  } from '../shared/semantic/delivery.mjs';
45
42
  import { sendMarkdownReply } from './markdown-reply.mjs';
43
+ import { t } from '../shared/i18n.mjs';
46
44
 
47
- const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
45
+ function interactionResolvedText() {
46
+ return t('这个问题已在其他客户端处理,无需再次回答。');
47
+ }
48
48
  const DEFAULT_FILE_UPLOAD_TIMEOUT_MS = 120_000;
49
49
 
50
50
  export const QQ_IMAGE_HOSTS = Object.freeze([
@@ -58,28 +58,30 @@ export const QQ_IMAGE_HOSTS = Object.freeze([
58
58
 
59
59
  const QQ_IMAGE_FILENAME = /\.(?:gif|jpe?g|png|webp)$/i;
60
60
 
61
- const HELP_TEXT = [
62
- 'QQ 机器人已连接 DeepSeek Harness。',
63
- '',
64
- '直接发送文字、图片或文件即可继续当前会话。',
65
- '/new 开启一个全新会话',
66
- '/compact 压缩当前会话的较早上下文',
67
- '/workspace 工作区绝对路径 切换工作区',
68
- '/workspacelist 列出工作区绝对路径',
69
- '/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
70
- '/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
71
- '/models 按序号列出所有可用模型',
72
- '/model [序号或完整模型ID] 查看或切换当前会话模型',
73
- '示例:先发 /models,再发 /model 2',
74
- '/presetlist 按序号列出可用 Agent Preset',
75
- '/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset',
76
- '纯数字 ID:/preset id:<ID>',
77
- '/preset --default 跟随 Host 默认',
78
- '/stop 停止当前任务',
79
- '/steer 补充指令 纠偏当前任务',
80
- '/status 检查连接状态',
81
- '/help 显示本帮助',
82
- ].join('\n');
61
+ function helpText() {
62
+ return [
63
+ t('QQ 机器人已连接 DeepSeek Harness。'),
64
+ '',
65
+ t('直接发送文字、图片或文件即可继续当前会话。'),
66
+ t('/new 开启一个全新会话'),
67
+ t('/compact 压缩当前会话的较早上下文'),
68
+ t('/workspace 工作区绝对路径 切换工作区'),
69
+ t('/workspacelist 列出工作区绝对路径'),
70
+ t('/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题'),
71
+ t('/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话'),
72
+ t('/models 按序号列出所有可用模型'),
73
+ t('/model [序号或完整模型ID] 查看或切换当前会话模型'),
74
+ t('示例:先发 /models,再发 /model 2'),
75
+ t('/presetlist 按序号列出可用 Agent Preset'),
76
+ t('/preset [序号或完整ID] 查看或设置当前机器人 Agent Preset'),
77
+ t('纯数字 ID:/preset id:<ID>'),
78
+ t('/preset --default 跟随 Host 默认'),
79
+ t('/stop 停止当前任务'),
80
+ t('/steer 补充指令 纠偏当前任务'),
81
+ t('/status 检查连接状态'),
82
+ t('/help 显示本帮助'),
83
+ ].join('\n');
84
+ }
83
85
 
84
86
  function conversationKey(message) {
85
87
  return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
@@ -170,35 +172,35 @@ function nonEmptyString(value) {
170
172
  }
171
173
 
172
174
  function artifactFailureText(fileName, error) {
173
- const name = String(fileName ?? '结果文件').replace(/[\r\n]+/g, ' ').trim() || '结果文件';
175
+ const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
174
176
  if (error?.name === 'UploadDailyLimitExceededError') {
175
- return `结果文件「${name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。`;
177
+ return t('结果文件「{name}」已生成,但 QQ 今日文件上传额度已用完,请稍后重试。', { name });
176
178
  }
177
179
  switch (error?.code) {
178
180
  case 'artifact-delivery-uncertain':
179
- return `结果文件「${name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。`;
181
+ return t('结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。', { name });
180
182
  case 'artifact-permission-required':
181
- return `结果文件「${name}」已生成,但当前 QQ 机器人没有文件消息权限。`;
183
+ return t('结果文件「{name}」已生成,但当前 QQ 机器人没有文件消息权限。', { name });
182
184
  case 'artifact-too-large':
183
- return `结果文件「${name}」超过当前 QQ 机器人可发送的文件大小,未发送。`;
185
+ return t('结果文件「{name}」超过当前 QQ 机器人可发送的文件大小,未发送。', { name });
184
186
  case 'artifact-empty':
185
- return `结果文件「${name}」为空,QQ 不允许发送空文件。`;
187
+ return t('结果文件「{name}」为空,QQ 不允许发送空文件。', { name });
186
188
  case 'artifact-changed':
187
189
  case 'artifact-invalid':
188
190
  case 'artifact-unavailable':
189
- return `结果文件「${name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。`;
191
+ return t('结果文件「{name}」暂时无法读取或准备发送,请确认文件仍可访问后重试。', { name });
190
192
  case 'artifact-rate-limited':
191
- return `结果文件「${name}」暂时被 QQ 限流,未能发送,请稍后重试。`;
193
+ return t('结果文件「{name}」暂时被 QQ 限流,未能发送,请稍后重试。', { name });
192
194
  case 'artifact-provider-rejected':
193
- return `结果文件「${name}」已生成,但 QQ 拒绝了该文件或文件消息。`;
195
+ return t('结果文件「{name}」已生成,但 QQ 拒绝了该文件或文件消息。', { name });
194
196
  default:
195
- return `结果文件「${name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。`;
197
+ return t('结果文件「{name}」已生成,但暂时未能通过 QQ 发送,请稍后重试。', { name });
196
198
  }
197
199
  }
198
200
 
199
201
  function answerTextForDelivery(answer, artifacts) {
200
202
  if (typeof answer === 'string' && answer.trim()) return answer;
201
- return artifacts.length > 0 ? '结果文件已生成。' : answer;
203
+ return artifacts.length > 0 ? t('结果文件已生成。') : answer;
202
204
  }
203
205
 
204
206
  function qqArtifactError(error, { dispatched = false } = {}) {
@@ -210,7 +212,7 @@ function qqArtifactError(error, { dispatched = false } = {}) {
210
212
  if (status === 401 || status === 403) wrapped.code = 'artifact-permission-required';
211
213
  else if (status === 413) wrapped.code = 'artifact-too-large';
212
214
  else if (status === 429) wrapped.code = 'artifact-rate-limited';
213
- else if (status === 400 || status === 404) {
215
+ else if ([400, 404, 405, 406, 410, 415, 422].includes(status)) {
214
216
  wrapped.code = 'artifact-provider-rejected';
215
217
  } else {
216
218
  wrapped.code = dispatched ? 'artifact-delivery-uncertain' : 'artifact-provider-failed';
@@ -245,6 +247,68 @@ function waitWithSignal(promise, signal) {
245
247
  });
246
248
  }
247
249
 
250
+ /** Send one materialized artifact through QQ's native image message. */
251
+ export async function sendQqImage(
252
+ bot,
253
+ target,
254
+ file,
255
+ { signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
256
+ ) {
257
+ signal?.throwIfAborted();
258
+ if (typeof bot?.sendImage !== 'function') {
259
+ const unavailable = new Error('QQ image delivery is unavailable');
260
+ unavailable.code = 'artifact-provider-unavailable';
261
+ throw unavailable;
262
+ }
263
+
264
+ try {
265
+ const timeout = AbortSignal.timeout(timeoutMs);
266
+ const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
267
+ const pending = bot.sendImage(
268
+ target,
269
+ { buffer: file.bytes },
270
+ { onProgress: () => signal?.throwIfAborted() },
271
+ );
272
+ trackOutboundArtifactProviderPromise(file, pending);
273
+ return await waitWithSignal(pending, waitSignal);
274
+ } catch (error) {
275
+ if (signal?.aborted) throw abortReason(signal);
276
+ throw qqArtifactError(error, { dispatched: true });
277
+ }
278
+ }
279
+
280
+ async function sendQqFile(
281
+ bot,
282
+ target,
283
+ file,
284
+ { signal, timeoutMs = DEFAULT_FILE_UPLOAD_TIMEOUT_MS } = {},
285
+ ) {
286
+ signal?.throwIfAborted();
287
+ if (typeof bot?.sendFile !== 'function') {
288
+ const unavailable = new Error('QQ file delivery is unavailable');
289
+ unavailable.code = 'artifact-provider-unavailable';
290
+ throw unavailable;
291
+ }
292
+
293
+ try {
294
+ const timeout = AbortSignal.timeout(timeoutMs);
295
+ const waitSignal = signal ? AbortSignal.any([signal, timeout]) : timeout;
296
+ const pending = bot.sendFile(
297
+ target,
298
+ { buffer: file.bytes },
299
+ {
300
+ fileName: file.fileName,
301
+ onProgress: () => signal?.throwIfAborted(),
302
+ },
303
+ );
304
+ trackOutboundArtifactProviderPromise(file, pending);
305
+ return await waitWithSignal(pending, waitSignal);
306
+ } catch (error) {
307
+ if (signal?.aborted) throw abortReason(signal);
308
+ throw qqArtifactError(error, { dispatched: true });
309
+ }
310
+ }
311
+
248
312
  function canClaimInteractionReply(message, pending) {
249
313
  return pending.questions[pending.index]
250
314
  && nonEmptyString(message?.senderId) === pending.actor
@@ -361,7 +425,7 @@ export class QqHarnessBridge {
361
425
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
362
426
  this.#status.lastError = error?.message ?? String(error);
363
427
  this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
364
- return this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
428
+ return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
365
429
  .catch(() => undefined);
366
430
  }).finally(() => {
367
431
  this.#acceptedMessageIds.delete(messageId);
@@ -491,80 +555,35 @@ export class QqHarnessBridge {
491
555
  if (artifacts.length === 0) {
492
556
  return { receipt: baseReceipt, failureNoticeVisible: false };
493
557
  }
494
- const receipts = baseReceipt ? [baseReceipt] : [];
495
- let failureNoticeVisible = false;
496
- for (const artifact of artifacts) {
497
- this.#signal?.throwIfAborted();
498
- try {
499
- if (typeof this.#bot.sendFile !== 'function') {
500
- const unavailable = new Error('QQ file delivery is unavailable');
501
- unavailable.code = 'artifact-provider-unavailable';
502
- throw unavailable;
503
- }
504
- const file = await materializeOutboundArtifact(artifact, {
505
- signal: this.#signal,
506
- });
507
- this.#signal?.throwIfAborted();
508
- let result;
509
- try {
510
- const timeout = AbortSignal.timeout(this.#fileUploadTimeoutMs);
511
- const waitSignal = this.#signal ? AbortSignal.any([this.#signal, timeout]) : timeout;
512
- const pending = this.#bot.sendFile(
513
- target,
514
- { buffer: file.bytes },
515
- {
516
- fileName: file.fileName,
517
- onProgress: () => this.#signal?.throwIfAborted(),
518
- },
519
- );
520
- trackOutboundArtifactProviderPromise(file, pending);
521
- result = await waitWithSignal(pending, waitSignal);
522
- } catch (error) {
523
- if (this.#signal?.aborted) throw abortReason(this.#signal);
524
- throw qqArtifactError(error, { dispatched: true });
525
- }
526
- this.#signal?.throwIfAborted();
527
- const messageId = nonEmptyString(result?.message?.id);
528
- receipts.push(createDeliveryReceipt({
529
- deliveryId: file.deliveryKey,
530
- presentation: 'qq-file',
531
- providerMessageIds: messageId ? [messageId] : [],
532
- artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
533
- }));
534
- this.#status.artifactsSent = (this.#status.artifactsSent ?? 0) + 1;
535
- } catch (rawError) {
536
- if (this.#signal?.aborted) throw rawError;
537
- const error = qqArtifactError(rawError);
538
- this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0) + 1;
539
- this.#logger.warn?.(
540
- `[dsh-im:qq] result file delivery failed (${error?.code ?? error?.name ?? 'unknown'})`,
541
- );
542
- let providerMessageIds = [];
543
- try {
544
- const notice = await this.#bot.sendText(target, artifactFailureText(artifact?.fileName, error));
545
- failureNoticeVisible = true;
546
- providerMessageIds = providerMessageIdsFor(notice);
547
- } catch (noticeError) {
548
- if (this.#signal?.aborted) throw noticeError;
549
- this.#logger.warn?.('[dsh-im:qq] unable to send the safe result-file failure notice');
550
- }
551
- receipts.push(createArtifactFailureReceipt({
552
- artifactId: artifact?.artifactId ?? 'unknown',
553
- deliveryId: artifact?.deliveryKey ?? artifact?.artifactId ?? 'unknown',
554
- error,
555
- providerMessageIds,
556
- }));
557
- } finally {
558
- releaseOutboundArtifact(artifact);
559
- }
560
- }
561
- return {
562
- receipt: mergeDeliveryReceipts({
563
- deliveryId: replyTo,
564
- presentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
565
- receipts,
558
+ const delivery = await deliverOutboundArtifacts({
559
+ artifacts,
560
+ baseReceipt,
561
+ deliveryId: replyTo,
562
+ aggregatePresentation: baseReceipt ? 'qq-text-and-files' : 'qq-files',
563
+ alwaysMerge: true,
564
+ channelKey: 'qq',
565
+ signal: this.#signal,
566
+ sendImage: (file) => sendQqImage(this.#bot, target, file, {
567
+ signal: this.#signal,
568
+ timeoutMs: this.#fileUploadTimeoutMs,
566
569
  }),
567
- failureNoticeVisible,
570
+ sendFile: (file) => sendQqFile(this.#bot, target, file, {
571
+ signal: this.#signal,
572
+ timeoutMs: this.#fileUploadTimeoutMs,
573
+ }),
574
+ sendFailureNotice: (artifact, error) => this.#bot.sendText(
575
+ target,
576
+ artifactFailureText(artifact?.fileName, error),
577
+ ),
578
+ logger: this.#logger,
579
+ });
580
+ this.#status.artifactsSent = (this.#status.artifactsSent ?? 0)
581
+ + delivery.artifactsSent;
582
+ this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
583
+ + delivery.artifactSendErrors;
584
+ return {
585
+ receipt: delivery.receipt,
586
+ failureNoticeVisible: delivery.failureNoticeVisible,
568
587
  };
569
588
  }
570
589
 
@@ -595,25 +614,25 @@ export class QqHarnessBridge {
595
614
  let stream = null;
596
615
  try {
597
616
  if (!text && !hasImages && !hasFiles) {
598
- await this.#bot.sendText(target, '目前支持文字、图片和文件消息。');
617
+ await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
599
618
  await this.#state.markSeen(messageId);
600
619
  return;
601
620
  }
602
621
  const command = text.toLowerCase();
603
622
  if (!hasImages && !hasFiles && command === '/help') {
604
- await this.#bot.sendText(target, HELP_TEXT);
623
+ await this.#bot.sendText(target, helpText());
605
624
  await this.#state.markSeen(messageId);
606
625
  return;
607
626
  }
608
627
  if (!hasImages && !hasFiles && command === '/status') {
609
628
  await this.#harness.ensureRunning({ signal: this.#signal });
610
- await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
629
+ await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
611
630
  await this.#state.markSeen(messageId);
612
631
  return;
613
632
  }
614
633
  if (!hasImages && !hasFiles && command === '/new') {
615
634
  await this.#state.clearSession(key);
616
- await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
635
+ await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
617
636
  await this.#state.markSeen(messageId);
618
637
  return;
619
638
  }
@@ -757,7 +776,7 @@ export class QqHarnessBridge {
757
776
  this.#logger.warn?.('[dsh-im:qq] unable to cancel a stopped QQ stream:', streamError);
758
777
  }
759
778
  try {
760
- await this.#bot.sendText(target, '已停止。');
779
+ await this.#bot.sendText(target, t('已停止。'));
761
780
  } catch (sendError) {
762
781
  this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
763
782
  }
@@ -777,7 +796,7 @@ export class QqHarnessBridge {
777
796
  target,
778
797
  inboundFileUserMessage(error)
779
798
  ?? imagePromptUserMessage(error)
780
- ?? '消息处理失败,请稍后重试。',
799
+ ?? t('消息处理失败,请稍后重试。'),
781
800
  );
782
801
  await this.#state.markSeen(messageId);
783
802
  } catch (sendError) {
@@ -804,14 +823,14 @@ export class QqHarnessBridge {
804
823
  if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
805
824
  const text = nonEmptyString(safeText(message));
806
825
  if (!text || hasQqImageAttachments(message) || hasQqFileAttachments(message)) {
807
- await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
826
+ await this.#bot.sendText(message.replyTarget, t('请用文字回答当前问题。'));
808
827
  return;
809
828
  }
810
829
 
811
830
  const pending = this.#pendingInteractions.get(key);
812
831
  if (!pending || pending !== expected || pending.submitting) {
813
832
  if (claimed && (!pending || pending !== expected)) {
814
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
833
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText());
815
834
  return;
816
835
  }
817
836
  return this.#enqueueMessage(message, messageId, key, {
@@ -824,7 +843,7 @@ export class QqHarnessBridge {
824
843
  try {
825
844
  await this.#presentInteraction(pending);
826
845
  } catch {
827
- this.#status.lastError = 'QQ 交互问题发送失败。';
846
+ this.#status.lastError = t('QQ 交互问题发送失败。');
828
847
  this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
829
848
  pending.interaction.reconnect?.();
830
849
  return;
@@ -832,7 +851,7 @@ export class QqHarnessBridge {
832
851
  const presentedPending = this.#pendingInteractions.get(key);
833
852
  if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
834
853
  if (claimed && (!presentedPending || presentedPending !== expected)) {
835
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT)
854
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText())
836
855
  .catch(() => undefined);
837
856
  return;
838
857
  }
@@ -855,7 +874,7 @@ export class QqHarnessBridge {
855
874
  try {
856
875
  await this.#presentInteraction(pending);
857
876
  } catch {
858
- this.#status.lastError = 'QQ 交互问题发送失败。';
877
+ this.#status.lastError = t('QQ 交互问题发送失败。');
859
878
  this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
860
879
  pending.interaction.reconnect?.();
861
880
  }
@@ -877,16 +896,16 @@ export class QqHarnessBridge {
877
896
  if (this.#signal?.aborted) return;
878
897
  if (error?.code === 'interaction-not-pending') {
879
898
  this.#clearPendingInteraction(key, pending.interactionId);
880
- await this.#bot.sendText(pending.target, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
899
+ await this.#bot.sendText(pending.target, interactionResolvedText()).catch(() => undefined);
881
900
  return;
882
901
  }
883
902
  if (this.#pendingInteractions.get(key) !== pending) return;
884
903
  pending.submitting = false;
885
904
  pending.answers.pop();
886
905
  pending.index -= 1;
887
- this.#status.lastError = '回答提交失败。';
906
+ this.#status.lastError = t('回答提交失败。');
888
907
  this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
889
- await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
908
+ await this.#bot.sendText(pending.target, t('回答提交失败,请重新发送当前问题的答案。'))
890
909
  .catch(() => undefined);
891
910
  }
892
911
  }
@@ -931,7 +950,7 @@ export class QqHarnessBridge {
931
950
  });
932
951
  await this.#bot.sendText(
933
952
  target,
934
- '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
953
+ t('检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。'),
935
954
  ).catch(() => undefined);
936
955
  return;
937
956
  }
@@ -1016,7 +1035,7 @@ export class QqHarnessBridge {
1016
1035
  await this.#state.markSeen(messageId);
1017
1036
  this.#status.messagesReceived += 1;
1018
1037
  this.#status.lastMessageAt = new Date().toISOString();
1019
- await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
1038
+ await this.#bot.sendText(message.replyTarget, interactionResolvedText()).catch(() => undefined);
1020
1039
  }
1021
1040
 
1022
1041
  #takePendingInteraction(key, interactionId) {
@@ -1058,7 +1077,7 @@ export class QqHarnessBridge {
1058
1077
  if (!this.#state.hasSeen(messageId)) {
1059
1078
  await this.#state.markSeen(messageId).catch(() => undefined);
1060
1079
  }
1061
- await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
1080
+ await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
1062
1081
  .catch(() => undefined);
1063
1082
  }
1064
1083
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import { connectionTestMessage } from '../shared/connection-test.mjs';
4
+ import { t } from '../shared/i18n.mjs';
4
5
  import { deriveQqBotIdentity, maskQqAppId } from './config-store.mjs';
5
6
 
6
7
  const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'refreshing', 'connecting']);
@@ -77,14 +78,14 @@ export class QqController {
77
78
  if (this.#closed || this.#runtimes.get(config.botId)?.status?.ready) return;
78
79
  const appSecret = await this.#resolveSecret(config.secretRef);
79
80
  if (!appSecret) {
80
- this.#errors.set(config.botId, safeError('missing-secret', 'QQ 机器人凭据缺失,请移除后重新扫码。'));
81
+ this.#errors.set(config.botId, safeError('missing-secret', t('QQ 机器人凭据缺失,请移除后重新扫码。')));
81
82
  return;
82
83
  }
83
84
  try {
84
85
  await this.#startRuntime(config, appSecret);
85
86
  this.#errors.delete(config.botId);
86
87
  } catch (error) {
87
- this.#errors.set(config.botId, safeError('connection-failed', 'QQ 连接未就绪,插件会自动重试。'));
88
+ this.#errors.set(config.botId, safeError('connection-failed', t('QQ 连接未就绪,插件会自动重试。')));
88
89
  this.#logger.warn?.(`[dsh-im:qq] bot ${config.botId} failed to initialize:`, error);
89
90
  } finally {
90
91
  this.#touch();
@@ -147,7 +148,7 @@ export class QqController {
147
148
  onFailure: (error) => {
148
149
  if (record.controller.signal.aborted || TERMINAL_ATTEMPT_STATES.has(record.state)) return;
149
150
  record.state = 'failed';
150
- record.error = safeError('qr-connect-failed', 'QQ 扫码服务暂时不可用,请重新生成二维码。');
151
+ record.error = safeError('qr-connect-failed', t('QQ 扫码服务暂时不可用,请重新生成二维码。'));
151
152
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
152
153
  this.#touch();
153
154
  firstQrReject(error);
@@ -158,10 +159,10 @@ export class QqController {
158
159
  } catch (error) {
159
160
  if (record.controller.signal.aborted) {
160
161
  record.state = 'cancelled';
161
- record.error = safeError('cancelled', '扫码绑定已取消。');
162
+ record.error = safeError('cancelled', t('扫码绑定已取消。'));
162
163
  } else if (!TERMINAL_ATTEMPT_STATES.has(record.state)) {
163
164
  record.state = 'failed';
164
- record.error = safeError('qr-start-failed', '无法生成 QQ 二维码,请稍后重试。');
165
+ record.error = safeError('qr-start-failed', t('无法生成 QQ 二维码,请稍后重试。'));
165
166
  }
166
167
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
167
168
  this.#touch();
@@ -207,7 +208,7 @@ export class QqController {
207
208
  await this.#startRuntime(config, normalizedSecret);
208
209
  this.#errors.delete(identity.botId);
209
210
  } catch (error) {
210
- this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
211
+ this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
211
212
  this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} credential connection failed:`, error);
212
213
  }
213
214
  this.#touch();
@@ -223,7 +224,7 @@ export class QqController {
223
224
  record.dispose?.();
224
225
  await record.task?.catch(() => undefined);
225
226
  if (!TERMINAL_ATTEMPT_STATES.has(record.state)) record.state = 'cancelled';
226
- record.error ??= safeError('cancelled', '扫码绑定已取消。');
227
+ record.error ??= safeError('cancelled', t('扫码绑定已取消。'));
227
228
  }
228
229
  if (this.#activeAttemptId === record.id) this.#activeAttemptId = null;
229
230
  this.#touch();
@@ -240,7 +241,7 @@ export class QqController {
240
241
  await this.#startRuntime(config, secret);
241
242
  this.#errors.delete(botId);
242
243
  } catch (error) {
243
- this.#errors.set(botId, safeError('connection-failed', 'QQ 连接仍未就绪,请稍后重试。'));
244
+ this.#errors.set(botId, safeError('connection-failed', t('QQ 连接仍未就绪,请稍后重试。')));
244
245
  throw error;
245
246
  } finally {
246
247
  this.#touch();
@@ -255,12 +256,12 @@ export class QqController {
255
256
  return this.#withBotTransition(botId, async () => {
256
257
  const runtime = this.#runtimes.get(botId);
257
258
  if (!runtime?.status?.ready || typeof runtime.sendConnectionTest !== 'function') {
258
- const error = new Error('QQ机器人尚未连接');
259
+ const error = new Error(t('QQ机器人尚未连接'));
259
260
  error.code = 'test-target-unavailable';
260
261
  throw error;
261
262
  }
262
263
  await runtime.sendConnectionTest(connectionTestMessage(
263
- `QQ 机器人(${maskQqAppId(config.appId)})`,
264
+ t('QQ 机器人({appId})', { appId: maskQqAppId(config.appId) }),
264
265
  ));
265
266
  return { sent: true };
266
267
  });
@@ -306,11 +307,11 @@ export class QqController {
306
307
  state,
307
308
  connected,
308
309
  configured: true,
309
- bot: { name: 'QQ机器人', appIdMasked: maskQqAppId(config.appId) },
310
+ bot: { name: t('QQ机器人'), appIdMasked: maskQqAppId(config.appId) },
310
311
  health: {
311
312
  status: connected ? 'healthy' : state === 'error' ? 'error' : 'offline',
312
- summary: connected ? 'QQ WebSocket 长连接运行正常'
313
- : state === 'error' ? 'QQ 连接未就绪,插件会自动重试' : 'QQ 连接当前离线',
313
+ summary: connected ? t('QQ WebSocket 长连接运行正常')
314
+ : state === 'error' ? t('QQ 连接未就绪,插件会自动重试') : t('QQ 连接当前离线'),
314
315
  lastCheckedAt: runtimeStatus?.lastCheckedAt ?? null,
315
316
  lastConnectedAt: runtimeStatus?.lastConnectedAt ?? null,
316
317
  },
@@ -364,10 +365,10 @@ export class QqController {
364
365
  } catch (error) {
365
366
  if (record.controller.signal.aborted) {
366
367
  record.state = 'cancelled';
367
- record.error = safeError('cancelled', '扫码绑定已取消。');
368
+ record.error = safeError('cancelled', t('扫码绑定已取消。'));
368
369
  } else {
369
370
  record.state = 'failed';
370
- record.error = safeError('activation-failed', 'QQ 已授权,但无法安全保存接入配置。');
371
+ record.error = safeError('activation-failed', t('QQ 已授权,但无法安全保存接入配置。'));
371
372
  this.#logger.error?.('[dsh-im:qq] provisioning failed:', error);
372
373
  }
373
374
  } finally {
@@ -417,7 +418,7 @@ export class QqController {
417
418
  await this.#restoreCredential(identity.secretRef, previousSecret);
418
419
  throw error;
419
420
  }
420
- this.#errors.set(identity.botId, safeError('connection-failed', 'QQ 机器人已绑定,消息连接暂未就绪。'));
421
+ this.#errors.set(identity.botId, safeError('connection-failed', t('QQ 机器人已绑定,消息连接暂未就绪。')));
421
422
  this.#logger.warn?.(`[dsh-im:qq] bot ${identity.botId} activation connection failed:`, error);
422
423
  }
423
424
  this.#touch();
@@ -4,6 +4,7 @@ import {
4
4
  connectionTestTarget,
5
5
  connectionTestTargetUnavailable,
6
6
  } from '../shared/connection-test.mjs';
7
+ import { t } from '../shared/i18n.mjs';
7
8
  import { createQqBridgeStatus, QqHarnessBridge } from './qq-bridge.mjs';
8
9
 
9
10
  function timeoutError() {
@@ -73,7 +74,7 @@ export class QqRuntime {
73
74
 
74
75
  async sendConnectionTest(text) {
75
76
  if (!this.#status.ready || !this.#bot) {
76
- throw connectionTestTargetUnavailable('QQ机器人');
77
+ throw connectionTestTargetUnavailable(t('QQ机器人'));
77
78
  }
78
79
  const ownerUserOpenid = typeof this.#config.ownerUserOpenid === 'string'
79
80
  ? this.#config.ownerUserOpenid.trim()
@@ -88,7 +89,7 @@ export class QqRuntime {
88
89
  : (ownerUserOpenid && ownerUserOpenid !== '*'
89
90
  ? { scope: 'c2c', targetId: ownerUserOpenid }
90
91
  : null);
91
- if (!target) throw connectionTestTargetUnavailable('QQ机器人');
92
+ if (!target) throw connectionTestTargetUnavailable(t('QQ机器人'));
92
93
  await this.#bot.sendText(target, text);
93
94
  return { sent: true };
94
95
  }
@@ -1,3 +1,5 @@
1
+ import { t } from './i18n.mjs';
2
+
1
3
  /** Matches DeepSeek Harness agent-preset directory ids. */
2
4
  export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
3
5
 
@@ -17,7 +19,7 @@ export function validateAgentPresetId(value) {
17
19
  if (value == null || value === '') return null;
18
20
  const id = normalizeAgentPresetId(value);
19
21
  if (!id) {
20
- const error = new Error('Agent Preset 无效。');
22
+ const error = new Error(t('Agent Preset 无效。'));
21
23
  error.code = 'agent-preset-invalid';
22
24
  throw error;
23
25
  }