@xmanrui/dsh-im 2.4.0 → 2.5.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 (47) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +1 -1
  3. package/lib/client.js +321 -109
  4. package/lib/index.js +214 -207
  5. package/package.json +1 -1
  6. package/plugin-src/client/channel-card-meta.js +36 -1
  7. package/plugin-src/client/channels/dingtalk/api.js +2 -0
  8. package/plugin-src/client/channels/dingtalk/index.js +9 -1
  9. package/plugin-src/client/channels/feishu/api.js +3 -0
  10. package/plugin-src/client/channels/feishu/index.js +9 -1
  11. package/plugin-src/client/channels/qq/api.js +2 -0
  12. package/plugin-src/client/channels/qq/index.js +9 -1
  13. package/plugin-src/client/channels/shared/token-api.js +2 -0
  14. package/plugin-src/client/channels/shared/token-channel.js +9 -1
  15. package/plugin-src/client/channels/wecom/api.js +2 -0
  16. package/plugin-src/client/channels/wecom/index.js +9 -1
  17. package/plugin-src/client/channels/weixin/api.js +2 -10
  18. package/plugin-src/client/channels/weixin/index.js +9 -5
  19. package/plugin-src/client/channels/whatsapp/api.js +2 -0
  20. package/plugin-src/client/channels/whatsapp/index.js +9 -1
  21. package/plugin-src/client/i18n.js +4 -0
  22. package/plugin-src/client/index.js +16 -2
  23. package/plugin-src/client/last-message-error.js +17 -0
  24. package/plugin-src/client/styles.js +5 -1
  25. package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
  26. package/src/channels/dingtalk/dingtalk-api.mjs +1 -1
  27. package/src/channels/dingtalk/dingtalk-bridge.mjs +49 -16
  28. package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
  29. package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
  30. package/src/channels/feishu/bridge.mjs +99 -43
  31. package/src/channels/feishu/multi-bot-controller.mjs +2 -0
  32. package/src/channels/qq/qq-bridge.mjs +83 -28
  33. package/src/channels/qq/qq-controller.mjs +2 -0
  34. package/src/channels/shared/harness-client.mjs +40 -4
  35. package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
  36. package/src/channels/shared/message-failure.mjs +244 -0
  37. package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
  38. package/src/channels/shared/text-harness-bridge.mjs +65 -60
  39. package/src/channels/shared/token-bot-controller.mjs +2 -0
  40. package/src/channels/slack/slack-controller.mjs +2 -0
  41. package/src/channels/wecom/wecom-bridge.mjs +49 -15
  42. package/src/channels/wecom/wecom-controller.mjs +2 -0
  43. package/src/channels/weixin/weixin-api.mjs +47 -0
  44. package/src/channels/weixin/weixin-bridge.mjs +261 -43
  45. package/src/channels/weixin/weixin-controller.mjs +2 -15
  46. package/src/channels/weixin/weixin-runtime.mjs +1 -0
  47. package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
@@ -11,6 +11,7 @@ import {
11
11
  connectionTestTargetUnavailable,
12
12
  } from '../shared/connection-test.mjs';
13
13
  import { t } from '../shared/i18n.mjs';
14
+ import { publicMessageFailure } from '../shared/message-failure.mjs';
14
15
 
15
16
  const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
16
17
  const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
@@ -516,6 +517,7 @@ export class DingtalkController {
516
517
  messagesReceived: Number(currentStatus.messagesReceived) || 0,
517
518
  messagesReplied: Number(currentStatus.messagesReplied) || 0,
518
519
  },
520
+ lastMessageError: publicMessageFailure(currentStatus.lastMessageError),
519
521
  senders: {
520
522
  pending,
521
523
  approved: config.approvedSenders.map(publicApprovedSender),
@@ -9,6 +9,7 @@ import {
9
9
  } from './message-utils.mjs';
10
10
  import {
11
11
  hasInboundImages,
12
+ imagePromptDiagnostic,
12
13
  imagePromptUserMessage,
13
14
  promptContentForMessage,
14
15
  } from '../shared/image-prompt.mjs';
@@ -48,6 +49,12 @@ import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.m
48
49
  import {
49
50
  createDeliveryReceipt,
50
51
  } from '../shared/semantic/delivery.mjs';
52
+ import {
53
+ channelDeliveryFailure,
54
+ clearLastMessageFailure,
55
+ messageFailureText,
56
+ setLastMessageFailure,
57
+ } from '../shared/message-failure.mjs';
51
58
  import {
52
59
  MENU_PAGE_SIZE,
53
60
  PRESET_FOLLOW_DEFAULT_SENTINEL,
@@ -365,6 +372,7 @@ function ensureStatus(status) {
365
372
  status.lastReplyAt ??= null;
366
373
  status.lastRejectedAt ??= null;
367
374
  status.lastError ??= null;
375
+ status.lastMessageError ??= null;
368
376
  }
369
377
 
370
378
  export class FeishuHarnessBridge {
@@ -801,6 +809,30 @@ export class FeishuHarnessBridge {
801
809
  return current;
802
810
  }
803
811
 
812
+ #recordFailure(error, {
813
+ logLabel = 'operation',
814
+ logLevel = 'warn',
815
+ userMessage,
816
+ reason,
817
+ } = {}) {
818
+ this.#status.lastError = error?.message ?? String(error);
819
+ const failure = setLastMessageFailure(this.#status, error, { userMessage, reason });
820
+ this.#logger?.[logLevel]?.(
821
+ `[dsh-feishu] ${logLabel} failed [${failure.referenceId}]:`,
822
+ error?.message ?? String(error),
823
+ );
824
+ return failure;
825
+ }
826
+
827
+ async #sendFailure(chatId, error, options = {}) {
828
+ const failure = this.#recordFailure(error, options);
829
+ const text = options.appendMessage
830
+ ? `${messageFailureText(failure)}\n\n${options.appendMessage}`
831
+ : messageFailureText(failure);
832
+ await this.#send(chatId, text).catch(() => undefined);
833
+ return failure;
834
+ }
835
+
804
836
  async #handleMessageFailure(event, messageId, processingReaction, error) {
805
837
  if (error?.code === 'turn-stopped') {
806
838
  await this.#removeProcessingReaction(messageId, processingReaction);
@@ -813,15 +845,21 @@ export class FeishuHarnessBridge {
813
845
  await this.#removeProcessingReaction(messageId, processingReaction);
814
846
  return;
815
847
  }
816
- this.#logger.error?.('[dsh-feishu] message handling failed:', error?.message ?? String(error));
817
- this.#status.lastError = error?.message ?? String(error);
848
+ const userMessage = inboundFileUserMessage(error)
849
+ ?? imagePromptUserMessage(error);
850
+ const failure = this.#recordFailure(error, {
851
+ logLabel: 'message handling',
852
+ logLevel: 'error',
853
+ userMessage,
854
+ reason: imagePromptDiagnostic(error)?.reason,
855
+ });
818
856
  await this.#finishReaction(messageId, processingReaction, 'ERROR');
857
+ const failureText = error?.batchInputMessage
858
+ ? `${messageFailureText(failure)}\n\n${error.batchInputMessage}`
859
+ : messageFailureText(failure);
819
860
  await this.#send(
820
861
  event.message.chat_id,
821
- error?.batchInputMessage
822
- ?? inboundFileUserMessage(error)
823
- ?? imagePromptUserMessage(error)
824
- ?? t('处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。'),
862
+ failureText,
825
863
  ).catch(() => undefined);
826
864
  }
827
865
 
@@ -1025,7 +1063,11 @@ export class FeishuHarnessBridge {
1025
1063
  const batchSubmission = event.batchSubmission ?? null;
1026
1064
  let batchAskCompleted = false;
1027
1065
  try {
1028
- const receipt = await this.#answerWithStream(event, key, message, {
1066
+ const {
1067
+ receipt,
1068
+ artifactSendErrors,
1069
+ textDeliveryErrors = 0,
1070
+ } = await this.#answerWithStream(event, key, message, {
1029
1071
  onAskComplete: batchSubmission
1030
1072
  ? () => {
1031
1073
  batchAskCompleted = this.#batchInputs.complete(
@@ -1038,6 +1080,9 @@ export class FeishuHarnessBridge {
1038
1080
  this.#status.messagesReplied += 1;
1039
1081
  this.#status.lastReplyAt = new Date().toISOString();
1040
1082
  this.#status.lastError = null;
1083
+ if (textDeliveryErrors === 0 && artifactSendErrors === 0) {
1084
+ clearLastMessageFailure(this.#status);
1085
+ }
1041
1086
  return receipt;
1042
1087
  } catch (error) {
1043
1088
  if (batchSubmission && !batchAskCompleted) {
@@ -1049,6 +1094,9 @@ export class FeishuHarnessBridge {
1049
1094
  if (failed.retained) {
1050
1095
  const batchError = new Error(error?.message ?? String(error), { cause: error });
1051
1096
  batchError.code = error?.code;
1097
+ batchError.providerCode = error?.providerCode;
1098
+ batchError.method = error?.method;
1099
+ if (Number.isInteger(error?.status)) batchError.status = error.status;
1052
1100
  batchError.batchInputMessage = failed.message;
1053
1101
  throw batchError;
1054
1102
  }
@@ -1643,9 +1691,7 @@ export class FeishuHarnessBridge {
1643
1691
  })
1644
1692
  .catch(async (error) => {
1645
1693
  if (this.#signal?.aborted) return;
1646
- this.#logger.warn?.('[dsh-feishu] card action failed:', error?.message ?? String(error));
1647
- this.#status.lastError = error?.message ?? String(error);
1648
- await this.#send(entry.chatId, t('卡片操作失败,请稍后重试。')).catch(() => undefined);
1694
+ await this.#sendFailure(entry.chatId, error, { logLabel: 'card action' });
1649
1695
  })
1650
1696
  .finally(() => {
1651
1697
  if (this.#cardActionInFlight.get(dedupeKey) === tracked) {
@@ -2008,8 +2054,7 @@ export class FeishuHarnessBridge {
2008
2054
  },
2009
2055
  );
2010
2056
  } catch (error) {
2011
- this.#logger.warn?.('[dsh-feishu] session list failed:', error.message);
2012
- await this.#send(chatId, t('暂时无法获取会话列表,请稍后重试。'));
2057
+ await this.#sendFailure(chatId, error, { logLabel: 'session list' });
2013
2058
  }
2014
2059
  }
2015
2060
 
@@ -2026,8 +2071,7 @@ export class FeishuHarnessBridge {
2026
2071
  { key, updateMessageId },
2027
2072
  );
2028
2073
  } catch (error) {
2029
- this.#logger.warn?.('[dsh-feishu] workspace list failed:', error.message);
2030
- await this.#send(chatId, t('暂时无法获取工作区列表,请稍后重试。'));
2074
+ await this.#sendFailure(chatId, error, { logLabel: 'workspace list' });
2031
2075
  }
2032
2076
  }
2033
2077
 
@@ -2038,7 +2082,10 @@ export class FeishuHarnessBridge {
2038
2082
  await this.#send(chatId, t('已绑定会话「{title}」\nID:{id}', { title, id: bound?.sessionId ?? sessionId }));
2039
2083
  await this.#sendMenuCard(key, chatId, { updateMessageId });
2040
2084
  } catch (error) {
2041
- await this.#send(chatId, t('绑定失败:{message}', { message: safeErrorText(error) }));
2085
+ await this.#sendFailure(chatId, error, {
2086
+ logLabel: 'session binding',
2087
+ userMessage: t('绑定失败:{message}', { message: safeErrorText(error) }),
2088
+ });
2042
2089
  }
2043
2090
  }
2044
2091
 
@@ -2048,7 +2095,10 @@ export class FeishuHarnessBridge {
2048
2095
  await this.#send(chatId, t('工作区已切换为:{workspace}', { workspace: current }));
2049
2096
  await this.#sendMenuCard(key, chatId, { updateMessageId });
2050
2097
  } catch (error) {
2051
- await this.#send(chatId, t('切换失败:{message}', { message: safeErrorText(error) }));
2098
+ await this.#sendFailure(chatId, error, {
2099
+ logLabel: 'workspace switch',
2100
+ userMessage: t('切换失败:{message}', { message: safeErrorText(error) }),
2101
+ });
2052
2102
  }
2053
2103
  }
2054
2104
 
@@ -2247,8 +2297,7 @@ export class FeishuHarnessBridge {
2247
2297
  catalog._currentId = settings.agentPreset;
2248
2298
  await this.#sendCard(chatId, presetCard(catalog), { key, updateMessageId });
2249
2299
  } catch (error) {
2250
- this.#logger.warn?.('[dsh-feishu] preset card failed:', error.message);
2251
- await this.#send(chatId, t('暂时无法获取预设列表,请稍后重试。'));
2300
+ await this.#sendFailure(chatId, error, { logLabel: 'preset card' });
2252
2301
  }
2253
2302
  }
2254
2303
 
@@ -2273,8 +2322,7 @@ export class FeishuHarnessBridge {
2273
2322
  }
2274
2323
  await this.#sendCard(chatId, modelCard(catalog), { key, updateMessageId });
2275
2324
  } catch (error) {
2276
- this.#logger.warn?.('[dsh-feishu] model card failed:', error.message);
2277
- await this.#send(chatId, t('暂时无法获取模型列表,请稍后重试。'));
2325
+ await this.#sendFailure(chatId, error, { logLabel: 'model card' });
2278
2326
  }
2279
2327
  }
2280
2328
 
@@ -2302,8 +2350,7 @@ export class FeishuHarnessBridge {
2302
2350
  }
2303
2351
  await this.#send(chatId, lines.join('\n'));
2304
2352
  } catch (error) {
2305
- this.#logger.warn?.('[dsh-feishu] status text failed:', error.message);
2306
- await this.#send(chatId, t('暂时无法获取系统状态,请稍后重试。'));
2353
+ await this.#sendFailure(chatId, error, { logLabel: 'status text' });
2307
2354
  }
2308
2355
  }
2309
2356
 
@@ -2355,8 +2402,7 @@ export class FeishuHarnessBridge {
2355
2402
 
2356
2403
  await this.#sendCard(chatId, statusCard(info), { key, updateMessageId });
2357
2404
  } catch (error) {
2358
- this.#logger.warn?.('[dsh-feishu] status card failed:', error.message);
2359
- await this.#send(chatId, t('暂时无法获取系统状态,请稍后重试。'));
2405
+ await this.#sendFailure(chatId, error, { logLabel: 'status card' });
2360
2406
  }
2361
2407
  }
2362
2408
 
@@ -2381,8 +2427,7 @@ export class FeishuHarnessBridge {
2381
2427
  );
2382
2428
  await this.#send(chatId, result?.message || t('上下文压缩失败。'));
2383
2429
  } catch (error) {
2384
- this.#logger.warn?.('[dsh-feishu] compact failed:', error.message, error.code);
2385
- await this.#send(chatId, t('上下文压缩失败,请稍后重试。'));
2430
+ await this.#sendFailure(chatId, error, { logLabel: 'compact' });
2386
2431
  }
2387
2432
  }
2388
2433
 
@@ -2405,8 +2450,7 @@ export class FeishuHarnessBridge {
2405
2450
  }
2406
2451
  await this.#send(chatId, result?.message || t('/stop 执行完成。'));
2407
2452
  } catch (error) {
2408
- this.#logger.warn?.('[dsh-feishu] stop failed:', error.message);
2409
- await this.#send(chatId, t('停止任务失败,请稍后重试。'));
2453
+ await this.#sendFailure(chatId, error, { logLabel: 'stop' });
2410
2454
  }
2411
2455
  }
2412
2456
 
@@ -2446,8 +2490,7 @@ export class FeishuHarnessBridge {
2446
2490
  if (reply) await this.#send(chatId, reply);
2447
2491
  }
2448
2492
  } catch (error) {
2449
- this.#logger.warn?.('[dsh-feishu] preset default failed:', error.message);
2450
- await this.#send(chatId, t('预设重置失败,请稍后重试。'));
2493
+ await this.#sendFailure(chatId, error, { logLabel: 'preset reset' });
2451
2494
  return;
2452
2495
  }
2453
2496
  try {
@@ -2470,8 +2513,7 @@ export class FeishuHarnessBridge {
2470
2513
  if (reply) await this.#send(chatId, reply);
2471
2514
  }
2472
2515
  } catch (error) {
2473
- this.#logger.warn?.('[dsh-feishu] preset select failed:', error.message);
2474
- await this.#send(chatId, t('预设切换失败,请稍后重试。'));
2516
+ await this.#sendFailure(chatId, error, { logLabel: 'preset selection' });
2475
2517
  return;
2476
2518
  }
2477
2519
  try {
@@ -2503,8 +2545,7 @@ export class FeishuHarnessBridge {
2503
2545
  if (reply) await this.#send(chatId, reply);
2504
2546
  }
2505
2547
  } catch (error) {
2506
- this.#logger.warn?.('[dsh-feishu] model select failed:', error.message);
2507
- await this.#send(chatId, t('模型切换失败,请稍后重试。'));
2548
+ await this.#sendFailure(chatId, error, { logLabel: 'model selection' });
2508
2549
  return;
2509
2550
  }
2510
2551
  try {
@@ -3046,10 +3087,14 @@ export class FeishuHarnessBridge {
3046
3087
  signal: this.#signal,
3047
3088
  })
3048
3089
  : undefined,
3049
- sendFailureNotice: async (artifact, error) => ({
3090
+ onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
3091
+ userMessage: artifactFailureText(artifact?.fileName, error),
3092
+ reason: error?.code,
3093
+ }),
3094
+ sendFailureNotice: async (_artifact, _error, failure) => ({
3050
3095
  messageId: await this.#send(
3051
3096
  chatId,
3052
- artifactFailureText(artifact?.fileName, error),
3097
+ messageFailureText(failure),
3053
3098
  ),
3054
3099
  }),
3055
3100
  logger: this.#logger,
@@ -3065,11 +3110,13 @@ export class FeishuHarnessBridge {
3065
3110
  presentation: 'feishu-files',
3066
3111
  }),
3067
3112
  failureNoticeVisible: delivery.failureNoticeVisible,
3113
+ artifactSendErrors: delivery.artifactSendErrors,
3068
3114
  };
3069
3115
  }
3070
3116
  return {
3071
3117
  receipt: delivery.receipt,
3072
3118
  failureNoticeVisible: delivery.failureNoticeVisible,
3119
+ artifactSendErrors: delivery.artifactSendErrors,
3073
3120
  };
3074
3121
  }
3075
3122
 
@@ -3110,7 +3157,7 @@ export class FeishuHarnessBridge {
3110
3157
  },
3111
3158
  );
3112
3159
  } catch (error) {
3113
- textSendError = error;
3160
+ textSendError = channelDeliveryFailure(error);
3114
3161
  this.#logger.warn?.(
3115
3162
  '[dsh-feishu] final text delivery failed; continuing with result files:',
3116
3163
  error,
@@ -3123,8 +3170,11 @@ export class FeishuHarnessBridge {
3123
3170
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
3124
3171
  throw textSendError;
3125
3172
  }
3173
+ if (textSendError && delivery.artifactSendErrors === 0) {
3174
+ setLastMessageFailure(this.#status, textSendError);
3175
+ }
3126
3176
  this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
3127
- return delivery.receipt;
3177
+ return { ...delivery, textDeliveryErrors: textSendError ? 1 : 0 };
3128
3178
  }
3129
3179
 
3130
3180
  let promptStarted = false;
@@ -3177,7 +3227,7 @@ export class FeishuHarnessBridge {
3177
3227
  },
3178
3228
  );
3179
3229
  } catch (fallbackError) {
3180
- textSendError = fallbackError;
3230
+ textSendError = channelDeliveryFailure(fallbackError);
3181
3231
  this.#logger.warn?.(
3182
3232
  '[dsh-feishu] fallback text delivery failed; continuing with result files:',
3183
3233
  fallbackError,
@@ -3195,8 +3245,11 @@ export class FeishuHarnessBridge {
3195
3245
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
3196
3246
  throw textSendError;
3197
3247
  }
3248
+ if (textSendError && delivery.artifactSendErrors === 0) {
3249
+ setLastMessageFailure(this.#status, textSendError);
3250
+ }
3198
3251
  this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
3199
- return delivery.receipt;
3252
+ return { ...delivery, textDeliveryErrors: textSendError ? 1 : 0 };
3200
3253
  }
3201
3254
  if (promptStarted) throw error;
3202
3255
 
@@ -3224,7 +3277,7 @@ export class FeishuHarnessBridge {
3224
3277
  },
3225
3278
  );
3226
3279
  } catch (fallbackError) {
3227
- textSendError = fallbackError;
3280
+ textSendError = channelDeliveryFailure(fallbackError);
3228
3281
  this.#logger.warn?.(
3229
3282
  '[dsh-feishu] fallback text delivery failed; continuing with result files:',
3230
3283
  fallbackError,
@@ -3237,8 +3290,11 @@ export class FeishuHarnessBridge {
3237
3290
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
3238
3291
  throw textSendError;
3239
3292
  }
3293
+ if (textSendError && delivery.artifactSendErrors === 0) {
3294
+ setLastMessageFailure(this.#status, textSendError);
3295
+ }
3240
3296
  this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
3241
- return delivery.receipt;
3297
+ return { ...delivery, textDeliveryErrors: textSendError ? 1 : 0 };
3242
3298
  }
3243
3299
  const delivery = await this.#deliverArtifacts(
3244
3300
  chatId,
@@ -3251,7 +3307,7 @@ export class FeishuHarnessBridge {
3251
3307
  }),
3252
3308
  );
3253
3309
  this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
3254
- return delivery.receipt;
3310
+ return delivery;
3255
3311
  }
3256
3312
 
3257
3313
  async #processInteractionReply(event, messageId, key, expected, processingReaction) {
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { connectionTestMessage } from '../shared/connection-test.mjs';
3
+ import { publicMessageFailure } from '../shared/message-failure.mjs';
3
4
  import { RegistrationManager } from './registration-manager.mjs';
4
5
  import {
5
6
  CALLBACK_REPAIR_OPERATION,
@@ -633,6 +634,7 @@ export class MultiBotDshFeishuController {
633
634
  groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
634
635
  bot: publicBot(config),
635
636
  connection,
637
+ lastMessageError: publicMessageFailure(connection.lastMessageError),
636
638
  error,
637
639
  };
638
640
  });
@@ -29,6 +29,7 @@ import {
29
29
  import {
30
30
  fetchImageBuffer,
31
31
  hasInboundImages,
32
+ imagePromptDiagnostic,
32
33
  imagePromptUserMessage,
33
34
  promptContentForMessage,
34
35
  } from '../shared/image-prompt.mjs';
@@ -45,6 +46,12 @@ import {
45
46
  createDeliveryReceipt,
46
47
  providerMessageIdsFor,
47
48
  } from '../shared/semantic/delivery.mjs';
49
+ import {
50
+ channelDeliveryFailure,
51
+ clearLastMessageFailure,
52
+ messageFailureText,
53
+ setLastMessageFailure,
54
+ } from '../shared/message-failure.mjs';
48
55
  import { sendMarkdownReply } from './markdown-reply.mjs';
49
56
  import { t } from '../shared/i18n.mjs';
50
57
 
@@ -216,9 +223,15 @@ function answerTextForDelivery(answer, artifacts) {
216
223
  }
217
224
 
218
225
  function qqArtifactError(error, { dispatched = false } = {}) {
219
- if (error?.code?.startsWith?.('artifact-') || error?.name === 'UploadDailyLimitExceededError') {
226
+ if (error?.code?.startsWith?.('artifact-')) {
220
227
  return error;
221
228
  }
229
+ if (error?.name === 'UploadDailyLimitExceededError') {
230
+ const wrapped = new Error('QQ daily file upload limit exceeded', { cause: error });
231
+ wrapped.name = error.name;
232
+ wrapped.code = 'artifact-rate-limited';
233
+ return wrapped;
234
+ }
222
235
  const status = Number(error?.httpStatus);
223
236
  const wrapped = new Error('QQ file delivery failed', { cause: error });
224
237
  if (status === 401 || status === 403) wrapped.code = 'artifact-permission-required';
@@ -341,6 +354,7 @@ export function createQqBridgeStatus() {
341
354
  lastReplyAt: null,
342
355
  lastRejectedAt: null,
343
356
  lastError: null,
357
+ lastMessageError: null,
344
358
  };
345
359
  }
346
360
 
@@ -470,8 +484,12 @@ export class QqHarnessBridge {
470
484
  ).catch((error) => {
471
485
  if (error?.code === 'turn-stopped' || this.#signal?.aborted) return;
472
486
  this.#status.lastError = error?.message ?? String(error);
473
- this.#logger.error?.('[dsh-im:qq] failed to process a command:', error);
474
- return this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
487
+ const failure = setLastMessageFailure(this.#status, error);
488
+ this.#logger.error?.(
489
+ `[dsh-im:qq] failed to process a command [${failure.referenceId}]:`,
490
+ error,
491
+ );
492
+ return this.#bot.sendText(message.replyTarget, messageFailureText(failure))
475
493
  .catch(() => undefined);
476
494
  }).finally(() => {
477
495
  this.#acceptedMessageIds.delete(messageId);
@@ -614,8 +632,12 @@ export class QqHarnessBridge {
614
632
  }).catch(async (error) => {
615
633
  if (this.#signal?.aborted) return;
616
634
  this.#status.lastError = error?.message ?? String(error);
617
- this.#logger.error?.('[dsh-im:qq] failed to process a batch input message:', error);
618
- await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
635
+ const failure = setLastMessageFailure(this.#status, error);
636
+ this.#logger.error?.(
637
+ `[dsh-im:qq] failed to process a batch input message [${failure.referenceId}]:`,
638
+ error,
639
+ );
640
+ await this.#bot.sendText(message.replyTarget, messageFailureText(failure))
619
641
  .catch(() => undefined);
620
642
  }).finally(() => {
621
643
  this.#acceptedMessageIds.delete(messageId);
@@ -627,7 +649,7 @@ export class QqHarnessBridge {
627
649
 
628
650
  async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt = null) {
629
651
  if (artifacts.length === 0) {
630
- return { receipt: baseReceipt, failureNoticeVisible: false };
652
+ return { receipt: baseReceipt, failureNoticeVisible: false, artifactSendErrors: 0 };
631
653
  }
632
654
  const delivery = await deliverOutboundArtifacts({
633
655
  artifacts,
@@ -645,9 +667,13 @@ export class QqHarnessBridge {
645
667
  signal: this.#signal,
646
668
  timeoutMs: this.#fileUploadTimeoutMs,
647
669
  }),
648
- sendFailureNotice: (artifact, error) => this.#bot.sendText(
670
+ onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
671
+ userMessage: artifactFailureText(artifact?.fileName, error),
672
+ reason: error?.code,
673
+ }),
674
+ sendFailureNotice: (_artifact, _error, failure) => this.#bot.sendText(
649
675
  target,
650
- artifactFailureText(artifact?.fileName, error),
676
+ messageFailureText(failure),
651
677
  ),
652
678
  logger: this.#logger,
653
679
  });
@@ -658,6 +684,7 @@ export class QqHarnessBridge {
658
684
  return {
659
685
  receipt: delivery.receipt,
660
686
  failureNoticeVisible: delivery.failureNoticeVisible,
687
+ artifactSendErrors: delivery.artifactSendErrors,
661
688
  };
662
689
  }
663
690
 
@@ -676,6 +703,12 @@ export class QqHarnessBridge {
676
703
  this.#status.messagesReceived += 1;
677
704
  this.#status.lastMessageAt = new Date().toISOString();
678
705
  }
706
+ let messageRecorded = alreadyRecorded;
707
+ const markMessageSeen = async () => {
708
+ if (messageRecorded) return;
709
+ await this.#state.markSeen(messageId);
710
+ messageRecorded = true;
711
+ };
679
712
  if (this.#ownerUserOpenid !== '*' && sender !== this.#ownerUserOpenid) {
680
713
  this.#status.messagesRejected += 1;
681
714
  this.#status.lastRejectedAt = new Date().toISOString();
@@ -694,25 +727,25 @@ export class QqHarnessBridge {
694
727
  try {
695
728
  if (!text && !hasImages && !hasFiles) {
696
729
  await this.#bot.sendText(target, t('目前支持文字、图片和文件消息。'));
697
- await this.#state.markSeen(messageId);
730
+ await markMessageSeen();
698
731
  return;
699
732
  }
700
733
  const command = text.toLowerCase();
701
734
  if (!hasImages && !hasFiles && command === '/help') {
702
735
  await this.#bot.sendText(target, helpText());
703
- await this.#state.markSeen(messageId);
736
+ await markMessageSeen();
704
737
  return;
705
738
  }
706
739
  if (!hasImages && !hasFiles && command === '/status') {
707
740
  await this.#harness.ensureRunning({ signal: this.#signal });
708
741
  await this.#bot.sendText(target, t('QQ 机器人与 DeepSeek Harness 连接正常。'));
709
- await this.#state.markSeen(messageId);
742
+ await markMessageSeen();
710
743
  return;
711
744
  }
712
745
  if (!hasImages && !hasFiles && command === '/new') {
713
746
  await this.#state.clearSession(key);
714
747
  await this.#bot.sendText(target, t('已开启新会话。请发送你的问题。'));
715
- await this.#state.markSeen(messageId);
748
+ await markMessageSeen();
716
749
  return;
717
750
  }
718
751
  const workspaceCommand = hasImages || hasFiles
@@ -722,7 +755,7 @@ export class QqHarnessBridge {
722
755
  for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
723
756
  await this.#bot.sendText(target, reply);
724
757
  }
725
- await this.#state.markSeen(messageId);
758
+ await markMessageSeen();
726
759
  return;
727
760
  }
728
761
  const compactCommand = hasImages || hasFiles
@@ -736,7 +769,7 @@ export class QqHarnessBridge {
736
769
  );
737
770
  if (compactCommand) {
738
771
  await this.#bot.sendText(target, compactCommand.message);
739
- await this.#state.markSeen(messageId);
772
+ await markMessageSeen();
740
773
  return;
741
774
  }
742
775
 
@@ -757,6 +790,9 @@ export class QqHarnessBridge {
757
790
  let answer;
758
791
  let artifacts = [];
759
792
  try {
793
+ // Persist consumption before handing the prompt to Harness. Provider
794
+ // redelivery after a failed error notice must never execute it twice.
795
+ await markMessageSeen();
760
796
  ({ answer, artifacts = [] } = await askInWorkspaceSession({
761
797
  harness: this.#harness,
762
798
  state: this.#state,
@@ -771,10 +807,13 @@ export class QqHarnessBridge {
771
807
  progressMode: 'all',
772
808
  onUpdate: (update) => {
773
809
  if (update.error) {
774
- const label = nonEmptyString(update.toolName)
775
- ? `Tool call ${update.toolName}` : 'Tool call';
776
- const text = `${label}\nError: ${update.error}`;
777
- toolErrors.push(text);
810
+ const name = (nonEmptyString(update.toolName) ?? t('工具'))
811
+ .replace(/[\r\n]+/gu, ' ')
812
+ .slice(0, 80);
813
+ toolErrors.push(t(
814
+ '工具调用「{name}」未成功,请检查工具配置或稍后重试。',
815
+ { name },
816
+ ));
778
817
  }
779
818
  },
780
819
  onInteraction: (interaction) => this.#handleInteraction(interaction, {
@@ -836,7 +875,7 @@ export class QqHarnessBridge {
836
875
  });
837
876
  }
838
877
  } catch (error) {
839
- textSendError = error;
878
+ textSendError = channelDeliveryFailure(error);
840
879
  this.#logger.warn?.('[dsh-im:qq] final text delivery failed; continuing with result files:', error);
841
880
  }
842
881
  const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
@@ -846,10 +885,15 @@ export class QqHarnessBridge {
846
885
  if (textSendError && !artifactDispatched && !delivery.failureNoticeVisible) {
847
886
  throw textSendError;
848
887
  }
849
- await this.#state.markSeen(messageId);
888
+ if (textSendError && delivery.artifactSendErrors === 0) {
889
+ setLastMessageFailure(this.#status, textSendError);
890
+ }
850
891
  this.#status.messagesReplied += 1;
851
892
  this.#status.lastReplyAt = new Date().toISOString();
852
893
  this.#status.lastError = null;
894
+ if (!textSendError && delivery.artifactSendErrors === 0) {
895
+ clearLastMessageFailure(this.#status);
896
+ }
853
897
  return delivery.receipt;
854
898
  } catch (error) {
855
899
  let batchFailureMessage = null;
@@ -872,7 +916,7 @@ export class QqHarnessBridge {
872
916
  } catch (sendError) {
873
917
  this.#logger.warn?.('[dsh-im:qq] unable to announce a stopped QQ turn:', sendError);
874
918
  }
875
- await this.#state.markSeen(messageId);
919
+ await markMessageSeen();
876
920
  return;
877
921
  }
878
922
  try {
@@ -882,16 +926,23 @@ export class QqHarnessBridge {
882
926
  }
883
927
  if (this.#signal?.aborted) return;
884
928
  this.#status.lastError = error?.message ?? String(error);
885
- this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
929
+ const userMessage = inboundFileUserMessage(error)
930
+ ?? imagePromptUserMessage(error);
931
+ const failure = setLastMessageFailure(this.#status, error, {
932
+ userMessage,
933
+ reason: imagePromptDiagnostic(error)?.reason,
934
+ });
935
+ this.#logger.error?.(
936
+ `[dsh-im:qq] failed to process an inbound message [${failure.referenceId}]:`,
937
+ error,
938
+ );
886
939
  try {
887
- const errorMessage = inboundFileUserMessage(error)
888
- ?? imagePromptUserMessage(error)
889
- ?? t('消息处理失败,请稍后重试。');
940
+ const errorMessage = messageFailureText(failure);
890
941
  await this.#bot.sendText(
891
942
  target,
892
943
  batchFailureMessage ? `${errorMessage}\n\n${batchFailureMessage}` : errorMessage,
893
944
  );
894
- await this.#state.markSeen(messageId);
945
+ await markMessageSeen();
895
946
  } catch (sendError) {
896
947
  this.#logger.error?.('[dsh-im:qq] failed to send the safe error reply:', sendError);
897
948
  }
@@ -1166,11 +1217,15 @@ export class QqHarnessBridge {
1166
1217
  async #handleInteractionFailure(message, messageId, error) {
1167
1218
  if (this.#signal?.aborted) return;
1168
1219
  this.#status.lastError = error?.message ?? String(error);
1169
- this.#logger.error?.('[dsh-im:qq] failed to process an interaction reply:', error);
1220
+ const failure = setLastMessageFailure(this.#status, error);
1221
+ this.#logger.error?.(
1222
+ `[dsh-im:qq] failed to process an interaction reply [${failure.referenceId}]:`,
1223
+ error,
1224
+ );
1170
1225
  if (!this.#state.hasSeen(messageId)) {
1171
1226
  await this.#state.markSeen(messageId).catch(() => undefined);
1172
1227
  }
1173
- await this.#bot.sendText(message.replyTarget, t('消息处理失败,请稍后重试。'))
1228
+ await this.#bot.sendText(message.replyTarget, messageFailureText(failure))
1174
1229
  .catch(() => undefined);
1175
1230
  }
1176
1231
  }