@xmanrui/dsh-im 3.1.1 → 4.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 (75) hide show
  1. package/README.en.md +16 -1
  2. package/README.md +16 -1
  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/index.mjs +1 -1
  25. package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
  26. package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
  27. package/plugin-src/host/channels/discord/index.mjs +1 -1
  28. package/plugin-src/host/channels/feishu/index.mjs +1 -1
  29. package/plugin-src/host/channels/feishu/production.mjs +3 -0
  30. package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
  31. package/plugin-src/host/channels/qq/index.mjs +1 -1
  32. package/plugin-src/host/channels/qq/production.mjs +1 -0
  33. package/plugin-src/host/channels/qq/rpc.mjs +11 -0
  34. package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
  35. package/plugin-src/host/channels/shared/production.mjs +1 -0
  36. package/plugin-src/host/channels/shared/rpc.mjs +9 -0
  37. package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
  38. package/plugin-src/host/channels/slack/index.mjs +1 -1
  39. package/plugin-src/host/channels/slack/production.mjs +1 -0
  40. package/plugin-src/host/channels/slack/rpc.mjs +10 -0
  41. package/plugin-src/host/channels/telegram/index.mjs +1 -1
  42. package/plugin-src/host/channels/wecom/index.mjs +1 -1
  43. package/plugin-src/host/channels/wecom/production.mjs +1 -0
  44. package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
  45. package/plugin-src/host/channels/weixin/index.mjs +1 -1
  46. package/plugin-src/host/channels/weixin/production.mjs +1 -0
  47. package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
  48. package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
  49. package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
  50. package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
  51. package/plugin-src/host/harness-connection.mjs +9 -4
  52. package/plugin-src/host/index.mjs +43 -29
  53. package/plugin-src/host/modern-harness-api.mjs +577 -0
  54. package/plugin-src/host/update-runtime.mjs +3 -2
  55. package/plugin-src/host/update-service.mjs +2 -0
  56. package/scripts/verify-package.mjs +15 -2
  57. package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
  58. package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
  59. package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
  60. package/src/channels/discord/discord-runtime.mjs +18 -4
  61. package/src/channels/feishu/bridge.mjs +18 -3
  62. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  63. package/src/channels/qq/qq-bridge.mjs +20 -4
  64. package/src/channels/qq/qq-runtime.mjs +4 -0
  65. package/src/channels/shared/bot-workspace-store.mjs +103 -6
  66. package/src/channels/shared/context-enhancement.mjs +135 -0
  67. package/src/channels/shared/harness-client.mjs +18 -0
  68. package/src/channels/shared/text-harness-bridge.mjs +19 -4
  69. package/src/channels/slack/slack-runtime.mjs +4 -0
  70. package/src/channels/telegram/telegram-runtime.mjs +24 -2
  71. package/src/channels/wecom/wecom-bridge.mjs +18 -3
  72. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  73. package/src/channels/weixin/weixin-bridge.mjs +19 -4
  74. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  75. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
@@ -316,16 +316,30 @@ function normalizeCardTarget(target) {
316
316
  }
317
317
  if (target?.type === 'group') {
318
318
  const openConversationId = nonEmptyString(target.openConversationId);
319
- if (openConversationId) return { type: 'group', openConversationId };
319
+ if (openConversationId) {
320
+ return { type: 'group', openConversationId, atUserIds: target.atUserIds };
321
+ }
320
322
  }
321
323
  throw new TypeError('DingTalk AI Card target is invalid');
322
324
  }
323
325
 
324
- function cardData(text, flowStatus) {
326
+ function cardMarkdown(text, target) {
327
+ const escape = (value) => String(value).replace(/[&<>"']/g, (char) => ({
328
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
329
+ })[char]);
330
+ const mentions = target?.type === 'group'
331
+ ? Object.entries(target.atUserIds ?? {}).map(([userId, name]) => (
332
+ `<a atId="${escape(userId)}">${escape(name)}</a>`
333
+ )).join(' ')
334
+ : '';
335
+ return normalizeDingtalkCardMarkdown(mentions ? `${mentions}\n\n${text}` : text);
336
+ }
337
+
338
+ function cardData(text, flowStatus, target) {
325
339
  return {
326
340
  cardParamMap: {
327
341
  flowStatus,
328
- msgContent: normalizeDingtalkCardMarkdown(text),
342
+ msgContent: cardMarkdown(text, target),
329
343
  staticMsgContent: '',
330
344
  sys_full_json_obj: JSON.stringify({ order: ['msgContent'] }),
331
345
  config: JSON.stringify({ autoLayout: true }),
@@ -339,7 +353,7 @@ function cardDeliverBody(cardInstanceId, target, robotCode) {
339
353
  return {
340
354
  ...base,
341
355
  openSpaceId: `dtv1.card//IM_GROUP.${target.openConversationId}`,
342
- imGroupOpenDeliverModel: { robotCode },
356
+ imGroupOpenDeliverModel: { robotCode, atUserIds: target.atUserIds },
343
357
  };
344
358
  }
345
359
  return {
@@ -575,7 +589,7 @@ export function createDingtalkApi({
575
589
  }
576
590
  }
577
591
 
578
- async function failCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
592
+ async function failCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
579
593
  const instanceId = nonEmptyString(cardInstanceId);
580
594
  const content = nonEmptyString(text);
581
595
  if (!instanceId) throw new TypeError('cardInstanceId is required');
@@ -589,7 +603,7 @@ export function createDingtalkApi({
589
603
  outTrackId: instanceId,
590
604
  guid: randomUUID(),
591
605
  key: 'msgContent',
592
- content: normalizeDingtalkCardMarkdown(content),
606
+ content: cardMarkdown(content, target),
593
607
  isFull: true,
594
608
  isFinalize: false,
595
609
  isError: true,
@@ -602,7 +616,7 @@ export function createDingtalkApi({
602
616
  method: 'PUT',
603
617
  body: {
604
618
  outTrackId: instanceId,
605
- cardData: cardData(content, '5'),
619
+ cardData: cardData(content, '5', target),
606
620
  cardUpdateOptions: { updateCardDataByKey: true },
607
621
  },
608
622
  headers,
@@ -883,6 +897,9 @@ export function createDingtalkApi({
883
897
  cardParamMap: { config: JSON.stringify({ autoLayout: true }) },
884
898
  },
885
899
  callbackType: 'STREAM',
900
+ cardAtUserIds: normalizedTarget.atUserIds
901
+ ? Object.keys(normalizedTarget.atUserIds)
902
+ : undefined,
886
903
  imGroupOpenSpaceModel: { supportForward: true },
887
904
  imRobotOpenSpaceModel: { supportForward: true },
888
905
  },
@@ -899,7 +916,7 @@ export function createDingtalkApi({
899
916
  delivered = true;
900
917
  await cardRequest('v1.0/card/instances', {
901
918
  method: 'PUT',
902
- body: { outTrackId: cardInstanceId, cardData: cardData(content, '2') },
919
+ body: { outTrackId: cardInstanceId, cardData: cardData(content, '2', normalizedTarget) },
903
920
  headers,
904
921
  signal,
905
922
  action: 'AI Card 启动',
@@ -910,7 +927,7 @@ export function createDingtalkApi({
910
927
  outTrackId: cardInstanceId,
911
928
  guid: randomUUID(),
912
929
  key: 'msgContent',
913
- content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ''),
930
+ content: cardMarkdown(content, normalizedTarget).replace(/\n+$/, ''),
914
931
  isFull: true,
915
932
  isFinalize: false,
916
933
  isError: false,
@@ -927,6 +944,7 @@ export function createDingtalkApi({
927
944
  clientSecret: appSecret,
928
945
  cardInstanceId,
929
946
  text: t('卡片已结束,请查看后续消息。'),
947
+ target: normalizedTarget,
930
948
  signal: cleanupSignal,
931
949
  }).catch(() => undefined);
932
950
  }
@@ -935,7 +953,7 @@ export function createDingtalkApi({
935
953
  return { cardInstanceId };
936
954
  },
937
955
 
938
- async updateAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
956
+ async updateAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
939
957
  const instanceId = nonEmptyString(cardInstanceId);
940
958
  const content = nonEmptyString(text);
941
959
  if (!instanceId) throw new TypeError('cardInstanceId is required');
@@ -947,7 +965,7 @@ export function createDingtalkApi({
947
965
  outTrackId: instanceId,
948
966
  guid: randomUUID(),
949
967
  key: 'msgContent',
950
- content: normalizeDingtalkCardMarkdown(content).replace(/\n+$/, ''),
968
+ content: cardMarkdown(content, target).replace(/\n+$/, ''),
951
969
  isFull: true,
952
970
  isFinalize: false,
953
971
  isError: false,
@@ -959,14 +977,14 @@ export function createDingtalkApi({
959
977
  return true;
960
978
  },
961
979
 
962
- async finishAiCard({ clientId, clientSecret, cardInstanceId, text, signal }) {
980
+ async finishAiCard({ clientId, clientSecret, cardInstanceId, text, target, signal }) {
963
981
  const instanceId = nonEmptyString(cardInstanceId);
964
982
  const content = nonEmptyString(text);
965
983
  if (!instanceId) throw new TypeError('cardInstanceId is required');
966
984
  if (!content) throw new TypeError('text is required');
967
985
  const token = await accessToken({ clientId, clientSecret, signal });
968
986
  const headers = { 'x-acs-dingtalk-access-token': token };
969
- const normalizedContent = normalizeDingtalkCardMarkdown(content);
987
+ const normalizedContent = cardMarkdown(content, target);
970
988
  await cardRequest('v1.0/card/streaming', {
971
989
  method: 'PUT',
972
990
  body: {
@@ -987,7 +1005,7 @@ export function createDingtalkApi({
987
1005
  method: 'PUT',
988
1006
  body: {
989
1007
  outTrackId: instanceId,
990
- cardData: cardData(content, '3'),
1008
+ cardData: cardData(content, '3', target),
991
1009
  cardUpdateOptions: { updateCardDataByKey: true },
992
1010
  },
993
1011
  headers,
@@ -1008,13 +1026,18 @@ export function createDingtalkApi({
1008
1026
 
1009
1027
  failAiCard: failCard,
1010
1028
 
1011
- async sendText({ clientId, clientSecret, sessionWebhook, text, signal }) {
1029
+ async sendText({ clientId, clientSecret, sessionWebhook, text, at, signal }) {
1012
1030
  const content = nonEmptyString(text);
1013
1031
  if (!content) throw new TypeError('text is required');
1014
1032
  const webhook = normalizeDingtalkSessionWebhook(sessionWebhook);
1015
1033
  const token = await accessToken({ clientId, clientSecret, signal });
1034
+ const replyAt = at && typeof at === 'object'
1035
+ && ((Array.isArray(at.atUserIds) && at.atUserIds.length > 0)
1036
+ || (Array.isArray(at.atMobiles) && at.atMobiles.length > 0)
1037
+ || at.isAtAll === true)
1038
+ ? { at } : {};
1016
1039
  const response = await requestJson(fetchImpl, webhook, {
1017
- body: { msgtype: 'text', text: { content } },
1040
+ body: { msgtype: 'text', text: { content }, ...replyAt },
1018
1041
  headers: { 'x-acs-dingtalk-access-token': token },
1019
1042
  signal,
1020
1043
  action: '消息回复',
@@ -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,