@xmanrui/dsh-im 3.1.1 → 3.2.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 (62) hide show
  1. package/README.en.md +15 -0
  2. package/README.md +15 -0
  3. package/lib/client.js +1653 -333
  4. package/lib/index.js +235 -226
  5. package/package.json +5 -1
  6. package/plugin-src/client/channels/dingtalk/api.js +3 -0
  7. package/plugin-src/client/channels/dingtalk/index.js +18 -5
  8. package/plugin-src/client/channels/feishu/api.js +3 -0
  9. package/plugin-src/client/channels/feishu/index.js +18 -5
  10. package/plugin-src/client/channels/qq/api.js +3 -0
  11. package/plugin-src/client/channels/qq/index.js +13 -0
  12. package/plugin-src/client/channels/shared/token-api.js +3 -0
  13. package/plugin-src/client/channels/shared/token-channel.js +13 -1
  14. package/plugin-src/client/channels/wecom/api.js +3 -0
  15. package/plugin-src/client/channels/wecom/index.js +13 -0
  16. package/plugin-src/client/channels/weixin/api.js +3 -0
  17. package/plugin-src/client/channels/weixin/index.js +19 -5
  18. package/plugin-src/client/channels/whatsapp/api.js +3 -0
  19. package/plugin-src/client/channels/whatsapp/index.js +13 -0
  20. package/plugin-src/client/context-enhancement.js +275 -0
  21. package/plugin-src/client/i18n.js +54 -3
  22. package/plugin-src/client/styles.js +78 -0
  23. package/plugin-src/client/update-panel.js +108 -7
  24. package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
  25. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  26. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  27. package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
  28. package/plugin-src/host/channels/qq/production.mjs +1 -0
  29. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  30. package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
  31. package/plugin-src/host/channels/shared/production.mjs +1 -0
  32. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  33. package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
  34. package/plugin-src/host/channels/slack/production.mjs +1 -0
  35. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  36. package/plugin-src/host/channels/wecom/production.mjs +1 -0
  37. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  38. package/plugin-src/host/channels/weixin/production.mjs +1 -0
  39. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  40. package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
  41. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  42. package/plugin-src/host/update-runtime.mjs +3 -2
  43. package/plugin-src/host/update-service.mjs +2 -0
  44. package/scripts/verify-package.mjs +15 -2
  45. package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
  46. package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
  47. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
  48. package/src/channels/discord/discord-runtime.mjs +18 -4
  49. package/src/channels/feishu/bridge.mjs +18 -3
  50. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  51. package/src/channels/qq/qq-bridge.mjs +20 -4
  52. package/src/channels/qq/qq-runtime.mjs +4 -0
  53. package/src/channels/shared/bot-workspace-store.mjs +103 -6
  54. package/src/channels/shared/context-enhancement.mjs +135 -0
  55. package/src/channels/shared/text-harness-bridge.mjs +19 -4
  56. package/src/channels/slack/slack-runtime.mjs +4 -0
  57. package/src/channels/telegram/telegram-runtime.mjs +24 -2
  58. package/src/channels/wecom/wecom-bridge.mjs +18 -3
  59. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  60. package/src/channels/weixin/weixin-bridge.mjs +19 -4
  61. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  62. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
@@ -28,6 +28,7 @@ import {
28
28
  } from '../shared/preset-command.mjs';
29
29
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
30
30
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
31
+ import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
31
32
  import {
32
33
  BatchInputManager,
33
34
  batchInputBusyMessage,
@@ -251,7 +252,11 @@ function conversationKey(message, sender) {
251
252
 
252
253
  function cardTarget(message, sender) {
253
254
  if (String(message?.conversationType) === '2') {
254
- return { type: 'group', openConversationId: nonEmptyString(message?.conversationId) };
255
+ return {
256
+ type: 'group',
257
+ openConversationId: nonEmptyString(message?.conversationId),
258
+ atUserIds: { [sender]: nonEmptyString(message?.senderNick) ?? sender },
259
+ };
255
260
  }
256
261
  return { type: 'user', userId: sender };
257
262
  }
@@ -368,6 +373,7 @@ export class DingtalkHarnessBridge {
368
373
  #clientSecret;
369
374
  #harness;
370
375
  #state;
376
+ #contextEnhancement;
371
377
  #status;
372
378
  #logger;
373
379
  #replyTimeoutMs;
@@ -379,7 +385,8 @@ export class DingtalkHarnessBridge {
379
385
  #interactionKeys = new Map();
380
386
  #interactionTasks = new Set();
381
387
  #commandTasks = new Set();
382
- #acceptedMessageIds = new Set();
388
+ // Keep the accepted configuration through the existing queue/reply lifecycle.
389
+ #acceptedMessageIds = new Map();
383
390
  #approvals;
384
391
  #batchInputs = new BatchInputManager();
385
392
 
@@ -389,6 +396,7 @@ export class DingtalkHarnessBridge {
389
396
  clientSecret,
390
397
  harness,
391
398
  state,
399
+ contextEnhancement,
392
400
  status = createDingtalkBridgeStatus(),
393
401
  logger = console,
394
402
  replyTimeoutMs = 600_000,
@@ -406,6 +414,7 @@ export class DingtalkHarnessBridge {
406
414
  this.#clientSecret = clientSecret.trim();
407
415
  this.#harness = harness;
408
416
  this.#state = state;
417
+ this.#contextEnhancement = contextEnhancement;
409
418
  this.#status = status;
410
419
  this.#logger = logger;
411
420
  this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
@@ -424,13 +433,17 @@ export class DingtalkHarnessBridge {
424
433
  return structuredClone(this.#status);
425
434
  }
426
435
 
427
- accept(message) {
436
+ accept(message, { contextSnapshot } = {}) {
428
437
  if (this.#signal?.aborted) return Promise.resolve();
429
438
  const messageId = nonEmptyString(message?.msgId);
430
439
  const sender = senderStaffId(message);
431
440
  if (!messageId || !sender || this.#state.hasSeen(messageId)
432
441
  || this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
433
- this.#acceptedMessageIds.add(messageId);
442
+ this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined ? captureContextEnhancement(
443
+ this.#contextEnhancement,
444
+ message.conversationType === '1' || message.conversationType === 1 ? 'direct'
445
+ : message.conversationType === '2' || message.conversationType === 2 ? 'group' : null,
446
+ ) : contextSnapshot);
434
447
 
435
448
  let key;
436
449
  try {
@@ -551,7 +564,7 @@ export class DingtalkHarnessBridge {
551
564
  `[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
552
565
  safeErrorDiagnostic(error),
553
566
  );
554
- return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
567
+ return this.#send(sessionWebhook, messageFailureText(failure), this.#atUsersFor(message)).catch(() => undefined);
555
568
  }).finally(() => {
556
569
  this.#acceptedMessageIds.delete(messageId);
557
570
  this.#commandTasks.delete(task);
@@ -573,7 +586,7 @@ export class DingtalkHarnessBridge {
573
586
  : null,
574
587
  isQuestionPending: () => this.#pendingInteractions.has(key),
575
588
  send: sessionWebhook
576
- ? (reply) => this.#send(sessionWebhook, reply)
589
+ ? (reply) => this.#send(sessionWebhook, reply, this.#atUsersFor(message))
577
590
  : async () => undefined,
578
591
  });
579
592
  if (approvalReply) {
@@ -807,7 +820,7 @@ export class DingtalkHarnessBridge {
807
820
  ]);
808
821
  }
809
822
  for (const reply of result?.messages ?? [result?.message]) {
810
- if (reply) await this.#send(sessionWebhook, reply);
823
+ if (reply) await this.#send(sessionWebhook, reply, this.#atUsersFor(message));
811
824
  }
812
825
  this.#status.lastError = null;
813
826
  }
@@ -885,23 +898,23 @@ export class DingtalkHarnessBridge {
885
898
  let batchSettled = batchSubmission === null;
886
899
  try {
887
900
  if (!text && !hasImages && !hasFiles) {
888
- await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'));
901
+ await this.#send(sessionWebhook, t('目前支持文字、图片和文件消息。'), this.#atUsersFor(message));
889
902
  return;
890
903
  }
891
904
 
892
905
  const command = text.toLowerCase();
893
906
  if (isPlainText && !hasImages && !hasFiles && command === '/help') {
894
- await this.#send(sessionWebhook, helpText());
907
+ await this.#send(sessionWebhook, helpText(), this.#atUsersFor(message));
895
908
  return;
896
909
  }
897
910
  if (isPlainText && !hasImages && !hasFiles && command === '/status') {
898
911
  await this.#harness.ensureRunning({ signal: this.#signal });
899
- await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'));
912
+ await this.#send(sessionWebhook, t('钉钉机器人与 DeepSeek Harness 连接正常。'), this.#atUsersFor(message));
900
913
  return;
901
914
  }
902
915
  if (isPlainText && !hasImages && !hasFiles && command === '/new') {
903
916
  await this.#state.clearSession(key);
904
- await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'));
917
+ await this.#send(sessionWebhook, t('已开启新会话。请发送你的问题。'), this.#atUsersFor(message));
905
918
  return;
906
919
  }
907
920
  const workspaceCommand = isPlainText && !hasImages && !hasFiles
@@ -909,7 +922,7 @@ export class DingtalkHarnessBridge {
909
922
  : null;
910
923
  if (workspaceCommand) {
911
924
  for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
912
- await this.#send(sessionWebhook, reply);
925
+ await this.#send(sessionWebhook, reply, this.#atUsersFor(message));
913
926
  }
914
927
  return;
915
928
  }
@@ -923,13 +936,21 @@ export class DingtalkHarnessBridge {
923
936
  )
924
937
  : null;
925
938
  if (compactCommand) {
926
- await this.#send(sessionWebhook, compactCommand.message);
939
+ await this.#send(sessionWebhook, compactCommand.message, this.#atUsersFor(message));
927
940
  return;
928
941
  }
929
942
 
930
- const content = hasImages
943
+ let content = hasImages
931
944
  ? await promptContentForMessage(promptMessage, { signal: this.#signal })
932
945
  : undefined;
946
+ const snapshot = this.#acceptedMessageIds.get(messageId);
947
+ if (snapshot) {
948
+ content = enhanceContextContent(content ?? text, snapshot, () => ({
949
+ channel: 'dingtalk',
950
+ senderId: sender,
951
+ senderName: message.senderNick,
952
+ }));
953
+ }
933
954
  if (typeof this.#api.createAiCard === 'function'
934
955
  && typeof this.#api.updateAiCard === 'function'
935
956
  && typeof this.#api.finishAiCard === 'function') {
@@ -947,7 +968,7 @@ export class DingtalkHarnessBridge {
947
968
  harness: this.#harness,
948
969
  state: this.#state,
949
970
  key,
950
- ...(hasImages ? { content } : { text }),
971
+ ...(content !== undefined ? { content } : { text }),
951
972
  createOptions: { signal: this.#signal },
952
973
  existsOptions: { signal: this.#signal },
953
974
  askOptions: {
@@ -988,7 +1009,7 @@ export class DingtalkHarnessBridge {
988
1009
  textReceipt = createDeliveryReceipt({
989
1010
  deliveryId: messageId,
990
1011
  presentation: 'dingtalk-text',
991
- providerMessageIds: await this.#send(sessionWebhook, answerText),
1012
+ providerMessageIds: await this.#send(sessionWebhook, answerText, this.#atUsersFor(message)),
992
1013
  });
993
1014
  }
994
1015
  } catch (error) {
@@ -1049,7 +1070,7 @@ export class DingtalkHarnessBridge {
1049
1070
  ? `${errorText}\n\n${batchFailureMessage}`
1050
1071
  : errorText;
1051
1072
  const streamed = cardStarted && await cardStream.finish(visibleError);
1052
- if (!streamed) await this.#send(sessionWebhook, visibleError);
1073
+ if (!streamed) await this.#send(sessionWebhook, visibleError, this.#atUsersFor(message));
1053
1074
  } catch {
1054
1075
  this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
1055
1076
  }
@@ -1102,7 +1123,7 @@ export class DingtalkHarnessBridge {
1102
1123
  const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
1103
1124
  if (!text) {
1104
1125
  try {
1105
- await this.#send(sessionWebhook, t('请用文字回答当前问题。'));
1126
+ await this.#send(sessionWebhook, t('请用文字回答当前问题。'), this.#atUsersFor(message));
1106
1127
  } catch {
1107
1128
  this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
1108
1129
  }
@@ -1113,7 +1134,7 @@ export class DingtalkHarnessBridge {
1113
1134
  if (!pending || pending !== expected || pending.submitting) {
1114
1135
  if (claimed && (!pending || pending !== expected)) {
1115
1136
  try {
1116
- await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
1137
+ await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
1117
1138
  } catch {
1118
1139
  this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
1119
1140
  }
@@ -1178,7 +1199,7 @@ export class DingtalkHarnessBridge {
1178
1199
  if (error?.code === 'interaction-not-pending') {
1179
1200
  this.#clearPendingInteraction(key, pending.interactionId);
1180
1201
  try {
1181
- await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
1202
+ await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
1182
1203
  } catch {
1183
1204
  this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
1184
1205
  }
@@ -1191,7 +1212,7 @@ export class DingtalkHarnessBridge {
1191
1212
  this.#status.lastError = t('回答提交失败。');
1192
1213
  this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
1193
1214
  try {
1194
- await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'));
1215
+ await this.#send(sessionWebhook, t('回答提交失败,请重新发送当前问题的答案。'), this.#atUsersFor(message));
1195
1216
  } catch {
1196
1217
  this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
1197
1218
  }
@@ -1323,7 +1344,7 @@ export class DingtalkHarnessBridge {
1323
1344
  return;
1324
1345
  }
1325
1346
  try {
1326
- await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT));
1347
+ await this.#send(sessionWebhook, t(INTERACTION_RESOLVED_TEXT), this.#atUsersFor(message));
1327
1348
  } catch {
1328
1349
  this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
1329
1350
  }
@@ -1367,7 +1388,14 @@ export class DingtalkHarnessBridge {
1367
1388
  }
1368
1389
  }
1369
1390
 
1370
- async #send(sessionWebhook, text) {
1391
+ #atUsersFor(message) {
1392
+ const sender = senderStaffId(message);
1393
+ return String(message?.conversationType) === '2' && sender
1394
+ ? { atUserIds: [sender] }
1395
+ : undefined;
1396
+ }
1397
+
1398
+ async #send(sessionWebhook, text, at) {
1371
1399
  const providerMessageIds = [];
1372
1400
  for (const chunk of splitDingtalkText(text, this.#maxMessageChars)) {
1373
1401
  this.#signal?.throwIfAborted();
@@ -1376,6 +1404,7 @@ export class DingtalkHarnessBridge {
1376
1404
  clientSecret: this.#clientSecret,
1377
1405
  sessionWebhook,
1378
1406
  text: chunk,
1407
+ at,
1379
1408
  signal: this.#signal,
1380
1409
  });
1381
1410
  providerMessageIds.push(...providerMessageIdsFor(result));
@@ -5,6 +5,7 @@ import {
5
5
  } from './dingtalk-bridge.mjs';
6
6
  import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
7
7
  import { t } from '../shared/i18n.mjs';
8
+ import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
8
9
 
9
10
  function nonEmptyString(value) {
10
11
  return typeof value === 'string' && value.trim() ? value.trim() : null;
@@ -128,6 +129,7 @@ export class DingtalkRuntime {
128
129
  #clientSecret;
129
130
  #harness;
130
131
  #state;
132
+ #contextEnhancement;
131
133
  #logger;
132
134
  #replyTimeoutMs;
133
135
  #maxMessageChars;
@@ -149,6 +151,7 @@ export class DingtalkRuntime {
149
151
  clientSecret,
150
152
  harness,
151
153
  state,
154
+ contextEnhancement,
152
155
  logger = console,
153
156
  replyTimeoutMs = 600_000,
154
157
  maxMessageChars = 4_000,
@@ -166,6 +169,7 @@ export class DingtalkRuntime {
166
169
  this.#clientSecret = clientSecret.trim();
167
170
  this.#harness = harness;
168
171
  this.#state = state;
172
+ this.#contextEnhancement = contextEnhancement;
169
173
  this.#logger = logger;
170
174
  this.#replyTimeoutMs = replyTimeoutMs;
171
175
  this.#maxMessageChars = maxMessageChars;
@@ -233,6 +237,7 @@ export class DingtalkRuntime {
233
237
  approvedSenders: this.#config.approvedSenders,
234
238
  harness: this.#harness,
235
239
  state: this.#state,
240
+ contextEnhancement: this.#contextEnhancement,
236
241
  status: this.#status,
237
242
  logger: this.#logger,
238
243
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -268,6 +273,15 @@ export class DingtalkRuntime {
268
273
  }
269
274
  }
270
275
 
276
+ // Retain the committed, read-only settings at receipt without moving
277
+ // JSON parsing out of the existing asynchronous callback path.
278
+ const contextEnhancement = this.#contextEnhancement;
279
+ let receivedSettings;
280
+ try {
281
+ receivedSettings = contextEnhancement?.getSettings?.();
282
+ } catch {
283
+ // Optional settings failures leave the original message path active.
284
+ }
271
285
  const task = Promise.resolve().then(async () => {
272
286
  if (this.#bridge !== bridge) return;
273
287
  let message;
@@ -282,7 +296,12 @@ export class DingtalkRuntime {
282
296
  }
283
297
  if (!message || typeof message !== 'object') return;
284
298
  this.#status.lastCallbackAt = Date.now();
285
- await bridge.accept(message);
299
+ const contextSnapshot = captureContextEnhancement({
300
+ getSettings: () => receivedSettings,
301
+ get botId() { return contextEnhancement?.botId; },
302
+ }, message.conversationType === '1' || message.conversationType === 1 ? 'direct'
303
+ : message.conversationType === '2' || message.conversationType === 2 ? 'group' : null);
304
+ await bridge.accept(message, { contextSnapshot });
286
305
  }).catch(() => {
287
306
  if (signal.aborted || this.#bridge !== bridge) return;
288
307
  this.#status.lastError = t('钉钉消息处理失败。');
@@ -2,6 +2,7 @@ import { createEditableMessageStream, splitMessageText } from '../shared/editabl
2
2
  import { fetchFileStream } from '../shared/file-download.mjs';
3
3
  import { fetchImageBuffer } from '../shared/image-prompt.mjs';
4
4
  import { t } from '../shared/i18n.mjs';
5
+ import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
5
6
  import { DiscordApi } from './discord-api.mjs';
6
7
  import { createDiscordBridgeStatus, DiscordHarnessBridge } from './discord-bridge.mjs';
7
8
 
@@ -213,6 +214,10 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
213
214
  return {
214
215
  messageId: String(message.id),
215
216
  senderId: String(message.author.id),
217
+ contextSource: () => ({
218
+ senderName: [message.member?.nick, message.author.global_name, message.author.username]
219
+ .find((value) => typeof value === 'string' && value.trim()),
220
+ }),
216
221
  senderIsBot: message.author.bot === true,
217
222
  kind: direct ? 'direct' : 'group',
218
223
  conversationId: String(message.channel_id),
@@ -427,6 +432,7 @@ export class DiscordRuntime {
427
432
  #token;
428
433
  #harness;
429
434
  #state;
435
+ #contextEnhancement;
430
436
  #logger;
431
437
  #replyTimeoutMs;
432
438
  #connectTimeoutMs;
@@ -457,6 +463,7 @@ export class DiscordRuntime {
457
463
  token,
458
464
  harness,
459
465
  state,
466
+ contextEnhancement,
460
467
  logger = console,
461
468
  replyTimeoutMs = 600_000,
462
469
  connectTimeoutMs = 20_000,
@@ -472,6 +479,7 @@ export class DiscordRuntime {
472
479
  this.#token = token;
473
480
  this.#harness = harness;
474
481
  this.#state = state;
482
+ this.#contextEnhancement = contextEnhancement;
475
483
  this.#logger = logger;
476
484
  this.#replyTimeoutMs = replyTimeoutMs;
477
485
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -534,6 +542,7 @@ export class DiscordRuntime {
534
542
  bot: client,
535
543
  harness: this.#harness,
536
544
  state: this.#state,
545
+ contextEnhancement: this.#contextEnhancement,
537
546
  status: this.#status,
538
547
  logger: this.#logger,
539
548
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -707,20 +716,25 @@ export class DiscordRuntime {
707
716
  if (!messageId || this.#state.hasSeen(messageId)) return;
708
717
  let route = this.#routing.get(messageId);
709
718
  if (!route) {
710
- route = resolveDiscordMessageRoute(message, this.#config.platformId, {
719
+ const contextSnapshot = captureContextEnhancement(
720
+ this.#contextEnhancement,
721
+ message.guild_id ? 'group' : 'direct',
722
+ );
723
+ const pendingRoute = resolveDiscordMessageRoute(message, this.#config.platformId, {
711
724
  api: this.#api,
712
725
  channel: this.#channels.get(String(message.channel_id)),
713
726
  signal: this.#abortController?.signal,
714
727
  onChannel: (resolved) => this.#rememberChannel(resolved),
715
728
  });
729
+ route = { pendingRoute, contextSnapshot };
716
730
  this.#routing.set(messageId, route);
717
- void route.finally(() => {
731
+ void pendingRoute.finally(() => {
718
732
  if (this.#routing.get(messageId) === route) this.#routing.delete(messageId);
719
733
  }).catch(() => undefined);
720
734
  }
721
735
  try {
722
- const normalized = await route;
723
- if (normalized) await bridge.accept(normalized);
736
+ const normalized = await route.pendingRoute;
737
+ if (normalized) await bridge.accept(normalized, { contextSnapshot: route.contextSnapshot });
724
738
  } catch (error) {
725
739
  if (error?.code === 'discord-thread-create-uncertain') {
726
740
  await this.#state.markSeen(messageId);
@@ -46,6 +46,7 @@ import {
46
46
  } from '../shared/preset-command.mjs';
47
47
  import { runWorkspaceCommand, resolveSessionListWorkspace, workspacePathSnapshot } from '../shared/workspace-command.mjs';
48
48
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
49
+ import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
49
50
  import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
50
51
  import {
51
52
  createDeliveryReceipt,
@@ -383,12 +384,14 @@ export class FeishuHarnessBridge {
383
384
  #channel;
384
385
  #harness;
385
386
  #state;
387
+ #contextEnhancement;
386
388
  #queues = new Map();
387
389
  #batchInputs = new BatchInputManager();
388
390
  #pendingInteractions = new Map();
389
391
  #interactionKeys = new Map();
390
392
  #resolvedQuestionReplies = new Map();
391
- #acceptedMessageIds = new Set();
393
+ // Keep the accepted configuration through the existing queue/reply lifecycle.
394
+ #acceptedMessageIds = new Map();
392
395
  #interactionTasks = new Set();
393
396
  #commandTasks = new Set();
394
397
  /** All accepted card work, including tasks waiting behind an earlier click. */
@@ -447,6 +450,7 @@ export class FeishuHarnessBridge {
447
450
  channel,
448
451
  harness,
449
452
  state,
453
+ contextEnhancement,
450
454
  status,
451
455
  allowedSenderOpenIds = new Set(),
452
456
  botId,
@@ -483,6 +487,7 @@ export class FeishuHarnessBridge {
483
487
  this.#channel = channel;
484
488
  this.#harness = harness;
485
489
  this.#state = state;
490
+ this.#contextEnhancement = contextEnhancement;
486
491
  this.#status = status;
487
492
  this.#allowedSenderOpenIds = allowedSenderOpenIds;
488
493
  this.#botId = nonEmptyString(botId);
@@ -551,7 +556,10 @@ export class FeishuHarnessBridge {
551
556
  if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
552
557
  }
553
558
 
554
- this.#acceptedMessageIds.add(messageId);
559
+ this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
560
+ this.#contextEnhancement,
561
+ event.message.chat_type === 'p2p' ? 'direct' : event.message.chat_type === 'group' ? 'group' : null,
562
+ ));
555
563
  const processingReaction = this.#beginReaction(messageId);
556
564
  const commandMessage = extractInboundMessage(event, this.#client);
557
565
  const commandText = nonEmptyString(commandMessage.content) ?? '';
@@ -3143,9 +3151,16 @@ export class FeishuHarnessBridge {
3143
3151
  askCompleted = true;
3144
3152
  onAskComplete?.();
3145
3153
  };
3146
- const content = hasInboundImages(message)
3154
+ let content = hasInboundImages(message)
3147
3155
  ? await promptContentForMessage(message, { signal: this.#signal })
3148
3156
  : undefined;
3157
+ const snapshot = this.#acceptedMessageIds.get(messageId);
3158
+ if (snapshot) {
3159
+ content = enhanceContextContent(content ?? text, snapshot, () => ({
3160
+ channel: 'feishu',
3161
+ senderId: senderOpenId(event),
3162
+ }));
3163
+ }
3149
3164
  if (!this.#channel?.stream) {
3150
3165
  const { answer, artifacts = [] } = await askInWorkspaceSession({
3151
3166
  harness: this.#harness,
@@ -104,6 +104,7 @@ export class FeishuRuntime {
104
104
  #ownerOpenIds;
105
105
  #harness;
106
106
  #state;
107
+ #contextEnhancement;
107
108
  #replyTimeoutMs;
108
109
  #connectTimeoutMs;
109
110
  #requestTimeoutMs;
@@ -131,6 +132,7 @@ export class FeishuRuntime {
131
132
  ownerOpenIds,
132
133
  harness,
133
134
  state,
135
+ contextEnhancement,
134
136
  repair,
135
137
  replyTimeoutMs = 600000,
136
138
  connectTimeoutMs = 15000,
@@ -162,6 +164,7 @@ export class FeishuRuntime {
162
164
  this.#ownerOpenIds = normalizedOwners;
163
165
  this.#harness = harness;
164
166
  this.#state = state;
167
+ this.#contextEnhancement = contextEnhancement;
165
168
  this.#repair = repair ?? null;
166
169
  this.#replyTimeoutMs = replyTimeoutMs;
167
170
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -260,6 +263,7 @@ export class FeishuRuntime {
260
263
  channel,
261
264
  harness: this.#harness,
262
265
  state: this.#state,
266
+ contextEnhancement: this.#contextEnhancement,
263
267
  status: this.#status,
264
268
  allowedSenderOpenIds: new Set(this.#ownerOpenIds),
265
269
  botId: this.#botId,
@@ -21,6 +21,7 @@ import {
21
21
  runPresetCommand,
22
22
  } from '../shared/preset-command.mjs';
23
23
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
24
+ import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
24
25
  import {
25
26
  BatchInputManager,
26
27
  batchInputBusyMessage,
@@ -365,6 +366,7 @@ export class QqHarnessBridge {
365
366
  #ownerUserOpenid;
366
367
  #harness;
367
368
  #state;
369
+ #contextEnhancement;
368
370
  #status;
369
371
  #logger;
370
372
  #replyTimeoutMs;
@@ -374,7 +376,8 @@ export class QqHarnessBridge {
374
376
  #queues = new Map();
375
377
  #pendingInteractions = new Map();
376
378
  #interactionKeys = new Map();
377
- #acceptedMessageIds = new Set();
379
+ // Keep the accepted configuration through the existing queue/reply lifecycle.
380
+ #acceptedMessageIds = new Map();
378
381
  #approvalTasks = new Set();
379
382
  #commandTasks = new Set();
380
383
  #approvals;
@@ -385,6 +388,7 @@ export class QqHarnessBridge {
385
388
  ownerUserOpenid,
386
389
  harness,
387
390
  state,
391
+ contextEnhancement,
388
392
  status = createQqBridgeStatus(),
389
393
  logger = console,
390
394
  replyTimeoutMs = 600_000,
@@ -403,6 +407,7 @@ export class QqHarnessBridge {
403
407
  this.#ownerUserOpenid = ownerUserOpenid;
404
408
  this.#harness = harness;
405
409
  this.#state = state;
410
+ this.#contextEnhancement = contextEnhancement;
406
411
  this.#status = status;
407
412
  this.#logger = logger;
408
413
  this.#replyTimeoutMs = replyTimeoutMs;
@@ -425,7 +430,10 @@ export class QqHarnessBridge {
425
430
  || this.#state.hasSeen(messageId)
426
431
  || this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
427
432
  const key = conversationKey(message);
428
- this.#acceptedMessageIds.add(messageId);
433
+ this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
434
+ this.#contextEnhancement,
435
+ message.kind === 'c2c' ? 'direct' : 'group',
436
+ ));
429
437
  if (message.kind === 'c2c'
430
438
  && (this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid)
431
439
  && message.replyTarget?.scope === 'c2c'
@@ -777,9 +785,17 @@ export class QqHarnessBridge {
777
785
  return;
778
786
  }
779
787
 
780
- const content = hasImages
788
+ let content = hasImages
781
789
  ? await promptContentForMessage(promptMessage, { signal: this.#signal })
782
790
  : undefined;
791
+ const snapshot = this.#acceptedMessageIds.get(messageId);
792
+ if (snapshot) {
793
+ content = enhanceContextContent(content ?? text, snapshot, () => ({
794
+ channel: 'qq',
795
+ senderId: sender,
796
+ senderName: message.kind === 'group' ? message.senderName : undefined,
797
+ }));
798
+ }
783
799
  // QQ stream_messages can acknowledge a final frame without rendering it in
784
800
  // some C2C clients. Standard Markdown delivery is the reliable reply path.
785
801
  const toolErrors = [];
@@ -793,7 +809,7 @@ export class QqHarnessBridge {
793
809
  harness: this.#harness,
794
810
  state: this.#state,
795
811
  key,
796
- ...(hasImages ? { content } : { text }),
812
+ ...(content !== undefined ? { content } : { text }),
797
813
  createOptions: { signal: this.#signal },
798
814
  existsOptions: { signal: this.#signal },
799
815
  askOptions: {
@@ -31,6 +31,7 @@ export class QqRuntime {
31
31
  #appSecret;
32
32
  #harness;
33
33
  #state;
34
+ #contextEnhancement;
34
35
  #logger;
35
36
  #replyTimeoutMs;
36
37
  #connectTimeoutMs;
@@ -48,6 +49,7 @@ export class QqRuntime {
48
49
  appSecret,
49
50
  harness,
50
51
  state,
52
+ contextEnhancement,
51
53
  logger = console,
52
54
  replyTimeoutMs = 600_000,
53
55
  connectTimeoutMs = 20_000,
@@ -61,6 +63,7 @@ export class QqRuntime {
61
63
  this.#appSecret = appSecret;
62
64
  this.#harness = harness;
63
65
  this.#state = state;
66
+ this.#contextEnhancement = contextEnhancement;
64
67
  this.#logger = logger;
65
68
  this.#replyTimeoutMs = replyTimeoutMs;
66
69
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -139,6 +142,7 @@ export class QqRuntime {
139
142
  ownerUserOpenid: this.#config.ownerUserOpenid,
140
143
  harness: this.#harness,
141
144
  state: this.#state,
145
+ contextEnhancement: this.#contextEnhancement,
142
146
  status: this.#status,
143
147
  logger: this.#logger,
144
148
  replyTimeoutMs: this.#replyTimeoutMs,