@xmanrui/dsh-im 3.1.0 → 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 (64) 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 -223
  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 +20 -4
  50. package/src/channels/feishu/feishu-channel.mjs +100 -44
  51. package/src/channels/feishu/feishu-runtime.mjs +4 -0
  52. package/src/channels/qq/qq-bridge.mjs +20 -4
  53. package/src/channels/qq/qq-runtime.mjs +4 -0
  54. package/src/channels/shared/bot-workspace-store.mjs +103 -6
  55. package/src/channels/shared/context-enhancement.mjs +135 -0
  56. package/src/channels/shared/i18n-en/feishu.mjs +2 -0
  57. package/src/channels/shared/text-harness-bridge.mjs +19 -4
  58. package/src/channels/slack/slack-runtime.mjs +4 -0
  59. package/src/channels/telegram/telegram-runtime.mjs +24 -2
  60. package/src/channels/wecom/wecom-bridge.mjs +18 -3
  61. package/src/channels/wecom/wecom-runtime.mjs +4 -0
  62. package/src/channels/weixin/weixin-bridge.mjs +19 -4
  63. package/src/channels/weixin/weixin-runtime.mjs +4 -0
  64. package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
@@ -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,
@@ -14,6 +14,11 @@ import {
14
14
  validateAgentPresetId,
15
15
  } from './agent-preset.mjs';
16
16
  import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
17
+ import {
18
+ DEFAULT_CONTEXT_ENHANCEMENT_CONFIG,
19
+ normalizeContextEnhancementConfig,
20
+ validateContextEnhancementConfig,
21
+ } from './context-enhancement.mjs';
17
22
  import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
18
23
 
19
24
  const EMPTY_DOCUMENT = Object.freeze({ version: 1, workspaces: Object.freeze({}) });
@@ -68,7 +73,17 @@ function normalizeDocument(value) {
68
73
  }
69
74
  }
70
75
  }
71
- return { version: 1, workspaces, agentPresets };
76
+ const contextEnhancement = Object.create(null);
77
+ // Enhancement damage is isolated from the existing workspace/preset document.
78
+ if (value.contextEnhancement && typeof value.contextEnhancement === 'object'
79
+ && !Array.isArray(value.contextEnhancement)) {
80
+ for (const [botId, config] of Object.entries(value.contextEnhancement)) {
81
+ if (/^[A-Za-z0-9_-]{1,128}$/.test(botId)) {
82
+ contextEnhancement[botId] = normalizeContextEnhancementConfig(config);
83
+ }
84
+ }
85
+ }
86
+ return { version: 1, workspaces, agentPresets, contextEnhancement };
72
87
  }
73
88
 
74
89
  export async function validateWorkspacePath(value) {
@@ -99,6 +114,7 @@ export class BotWorkspaceStore {
99
114
  #defaultWorkspace;
100
115
  #workspaces = {};
101
116
  #agentPresets = {};
117
+ #contextEnhancement = {};
102
118
  #generations = new Map();
103
119
  #nextGeneration = 1;
104
120
  #incarnations = new Map();
@@ -121,10 +137,12 @@ export class BotWorkspaceStore {
121
137
  if (!normalized) throw new Error('dsh-im workspace config is invalid');
122
138
  this.#workspaces = normalized.workspaces;
123
139
  this.#agentPresets = normalized.agentPresets;
140
+ this.#contextEnhancement = normalized.contextEnhancement;
124
141
  } catch (error) {
125
142
  if (error?.code !== 'ENOENT') throw error;
126
143
  this.#workspaces = {};
127
144
  this.#agentPresets = {};
145
+ this.#contextEnhancement = {};
128
146
  }
129
147
  this.#generations.clear();
130
148
  this.#nextGeneration = 1;
@@ -156,6 +174,13 @@ export class BotWorkspaceStore {
156
174
  return this.#agentPresets[botIdOf(botId)] ?? null;
157
175
  }
158
176
 
177
+ contextEnhancementFor(botId) {
178
+ const id = botIdOf(botId);
179
+ return this.has(id) && Object.hasOwn(this.#contextEnhancement, id)
180
+ ? this.#contextEnhancement[id]
181
+ : DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
182
+ }
183
+
159
184
  generationFor(botId) {
160
185
  return this.#generations.get(botIdOf(botId)) ?? null;
161
186
  }
@@ -270,6 +295,24 @@ export class BotWorkspaceStore {
270
295
  });
271
296
  }
272
297
 
298
+ async setContextEnhancement(botId, value, { incarnation } = {}) {
299
+ const id = botIdOf(botId);
300
+ const expectedIncarnation = incarnation === undefined ? this.incarnationFor(id) : incarnation;
301
+ const config = validateContextEnhancementConfig(value);
302
+ return this.#enqueue(id, async () => {
303
+ if (!this.has(id) || expectedIncarnation !== this.incarnationFor(id)) {
304
+ const error = new Error('找不到要修改的机器人。');
305
+ error.code = 'workspace-bot-not-found';
306
+ throw error;
307
+ }
308
+ const next = { ...this.#contextEnhancement, [id]: config };
309
+ // Messages keep the previous committed snapshot until rename succeeds.
310
+ await this.#persist(next);
311
+ this.#contextEnhancement = next;
312
+ return config;
313
+ });
314
+ }
315
+
273
316
  async bindWorkspaceSession(botId, value, {
274
317
  conversationKey,
275
318
  sessionId,
@@ -408,6 +451,14 @@ export class BotWorkspaceStore {
408
451
  });
409
452
  }
410
453
 
454
+ /** A failed retirement must reach disk before the same config ID can be rebound. */
455
+ flushPendingRemoval(botId) {
456
+ if (!this.#dirtyRemovals.has(botId)) return undefined;
457
+ return this.#enqueue(botId, async () => {
458
+ if (this.#dirtyRemovals.has(botId)) await this.#persistCurrentDocument();
459
+ });
460
+ }
461
+
411
462
  async remove(botId) {
412
463
  const result = await this.retireAfterConfigCommit(botId);
413
464
  if (result.error) throw result.error;
@@ -419,6 +470,7 @@ export class BotWorkspaceStore {
419
470
  const candidates = new Set([
420
471
  ...Object.keys(this.#workspaces),
421
472
  ...Object.keys(this.#agentPresets),
473
+ ...Object.keys(this.#contextEnhancement),
422
474
  ...this.#dirtyRemovals,
423
475
  ]);
424
476
  for (const botId of candidates) {
@@ -435,6 +487,7 @@ export class BotWorkspaceStore {
435
487
  ...bot,
436
488
  workspace: this.workspaceFor(bot.botId),
437
489
  agentPreset: this.agentPresetFor(bot.botId),
490
+ contextEnhancement: this.contextEnhancementFor(bot.botId),
438
491
  }
439
492
  : bot),
440
493
  };
@@ -464,9 +517,11 @@ export class BotWorkspaceStore {
464
517
  async #retireCurrentIncarnation(id) {
465
518
  const hadWorkspace = Object.hasOwn(this.#workspaces, id);
466
519
  const hadPreset = Object.hasOwn(this.#agentPresets, id);
467
- const needsCleanup = hadWorkspace || hadPreset || this.#dirtyRemovals.has(id);
520
+ const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
521
+ const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement || this.#dirtyRemovals.has(id);
468
522
  delete this.#workspaces[id];
469
523
  delete this.#agentPresets[id];
524
+ delete this.#contextEnhancement[id];
470
525
  this.#generations.delete(id);
471
526
  this.#incarnations.delete(id);
472
527
  if (!needsCleanup) return {
@@ -496,11 +551,14 @@ export class BotWorkspaceStore {
496
551
  return queued;
497
552
  }
498
553
 
499
- async #persist() {
554
+ async #persist(contextEnhancement = this.#contextEnhancement) {
500
555
  const document = { version: 1, workspaces: this.#workspaces };
501
556
  if (Object.keys(this.#agentPresets).length > 0) {
502
557
  document.agentPresets = this.#agentPresets;
503
558
  }
559
+ if (Object.keys(contextEnhancement).length > 0) {
560
+ document.contextEnhancement = contextEnhancement;
561
+ }
504
562
  await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
505
563
  const temporary = `${this.#path}.tmp`;
506
564
  await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
@@ -513,7 +571,8 @@ export class BotWorkspaceStore {
513
571
 
514
572
  async #persistCurrentDocument() {
515
573
  if (Object.keys(this.#workspaces).length > 0
516
- || Object.keys(this.#agentPresets).length > 0) {
574
+ || Object.keys(this.#agentPresets).length > 0
575
+ || Object.keys(this.#contextEnhancement).length > 0) {
517
576
  await this.#persist();
518
577
  return;
519
578
  }
@@ -569,10 +628,16 @@ function targetStatus(controller) {
569
628
  return Promise.resolve(controller.status());
570
629
  }
571
630
 
572
- /** Observe the config store's durable removal commit without changing its API. */
631
+ /** Observe durable removals and finish failed cleanup before a same-ID config save. */
573
632
  export function observeBotWorkspaceRemovals(
574
633
  configStore,
575
- { workspaces, method = 'remove', botIdFromRemoved = (removed) => removed?.botId },
634
+ {
635
+ workspaces,
636
+ method = 'remove',
637
+ botIdFromRemoved = (removed) => removed?.botId,
638
+ saveMethod = 'save',
639
+ botIdFromSave = (config) => config?.botId,
640
+ },
576
641
  ) {
577
642
  if (!configStore || !workspaces || typeof configStore[method] !== 'function') {
578
643
  throw new TypeError('configStore removal observer dependencies are required');
@@ -588,6 +653,12 @@ export function observeBotWorkspaceRemovals(
588
653
  return removed;
589
654
  };
590
655
  }
656
+ if (property === saveMethod && typeof value === 'function') {
657
+ return (...args) => {
658
+ const cleanup = workspaces.flushPendingRemoval(botIdFromSave(args[0], args));
659
+ return cleanup ? cleanup.then(() => value.apply(target, args)) : value.apply(target, args);
660
+ };
661
+ }
591
662
  return typeof value === 'function' ? value.bind(target) : value;
592
663
  },
593
664
  });
@@ -998,6 +1069,31 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
998
1069
  );
999
1070
  });
1000
1071
  };
1072
+ const updateContextEnhancement = (botId, value, projectStatus) => {
1073
+ const incarnation = workspaces.incarnationFor(botId);
1074
+ const config = validateContextEnhancementConfig(value);
1075
+ return withBotTransition(botId, async () => {
1076
+ const snapshot = await controller.status();
1077
+ if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
1078
+ const error = new Error('找不到要修改的机器人。');
1079
+ error.code = 'workspace-bot-not-found';
1080
+ throw error;
1081
+ }
1082
+ const catalog = await resolveAgentPresetCatalog(agentPresetCatalog);
1083
+ const decorated = workspaces.decorateStatus(snapshot);
1084
+ const updated = {
1085
+ ...decorated,
1086
+ bots: decorated.bots.map((bot) => bot?.botId === botId
1087
+ ? { ...bot, contextEnhancement: config } : bot),
1088
+ ...(catalog ? { agentPresetCatalog: catalog } : {}),
1089
+ };
1090
+ // QR/status projection can fail too. Prepare the complete response before
1091
+ // commit so a failed save never publishes new running settings.
1092
+ const result = projectStatus ? await projectStatus(updated) : updated;
1093
+ await workspaces.setContextEnhancement(botId, config, { incarnation });
1094
+ return result;
1095
+ });
1096
+ };
1001
1097
  const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
1002
1098
  // Fence the old runtime without changing the durable mapping. A crash
1003
1099
  // before the controller removes its config therefore keeps the bot's
@@ -1037,6 +1133,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
1037
1133
  get(target, property) {
1038
1134
  if (property === 'updateWorkspace') return updateWorkspace;
1039
1135
  if (property === 'updateAgentPreset') return updateAgentPreset;
1136
+ if (property === 'updateContextEnhancement') return updateContextEnhancement;
1040
1137
  const value = Reflect.get(target, property, target);
1041
1138
  if (typeof value !== 'function') return value;
1042
1139
  if (property === 'deleteBot') {
@@ -0,0 +1,135 @@
1
+ // Shared by the Host and settings UI; keep this module browser-compatible.
2
+ export const CONTEXT_ENHANCEMENT_FIELDS = Object.freeze([
3
+ 'channel', 'conversationType', 'senderId', 'senderName', 'botId',
4
+ ]);
5
+
6
+ export const CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH = 8_000;
7
+ export const CONTEXT_GUIDANCE_EXAMPLE = `仅依据当前消息的 <dsh_im_source> 中实际提供的字段理解来源;没有提供的字段不要猜测或补全。
8
+ conversationType是群聊时回复严肃一点,conversationType是私聊时回复一定要幽默搞笑,像周星驰的电影一样搞笑`;
9
+
10
+ // Kept as an alias for integrations that imported the original template name.
11
+ export const DEFAULT_CONTEXT_GUIDANCE = CONTEXT_GUIDANCE_EXAMPLE;
12
+
13
+ export const DEFAULT_CONTEXT_ENHANCEMENT_CONFIG = Object.freeze({
14
+ groupEnabled: false,
15
+ directEnabled: false,
16
+ fields: Object.freeze(['senderId']),
17
+ guidance: '',
18
+ });
19
+
20
+ const CONFIG_KEYS = ['groupEnabled', 'directEnabled', 'fields', 'guidance'];
21
+ const CHANNELS = new Set([
22
+ 'wecom', 'weixin', 'feishu', 'dingtalk', 'qq',
23
+ 'slack', 'telegram', 'discord', 'whatsapp',
24
+ ]);
25
+ const SOURCE_LIMITS = { channel: 16, conversationType: 6, senderId: 256, senderName: 256, botId: 128 };
26
+ const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g;
27
+
28
+ function invalidConfig(message) {
29
+ const error = new TypeError(message);
30
+ error.code = 'context-enhancement-invalid';
31
+ return error;
32
+ }
33
+
34
+ /** Validate the complete atomic save, preserving explicit empty selections/text. */
35
+ export function validateContextEnhancementConfig(input) {
36
+ if (!input || typeof input !== 'object' || Array.isArray(input)
37
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(input))
38
+ || Reflect.ownKeys(input).length !== CONFIG_KEYS.length
39
+ || !CONFIG_KEYS.every((key) => Object.hasOwn(input, key))) {
40
+ throw invalidConfig('请提交完整的上下文增强设置。');
41
+ }
42
+ const { groupEnabled, directEnabled, fields, guidance } = input;
43
+ if (typeof groupEnabled !== 'boolean' || typeof directEnabled !== 'boolean') {
44
+ throw invalidConfig('群聊和私聊开关必须是布尔值。');
45
+ }
46
+ if (!Array.isArray(fields) || ![...fields].every((field) => CONTEXT_ENHANCEMENT_FIELDS.includes(field))) {
47
+ throw invalidConfig('来源字段只能选择已定义的五个字段。');
48
+ }
49
+ if (typeof guidance !== 'string' || guidance.length > CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH) {
50
+ throw invalidConfig(`增强提示词不得超过 ${CONTEXT_ENHANCEMENT_GUIDANCE_MAX_LENGTH} 个字符。`);
51
+ }
52
+ return Object.freeze({
53
+ groupEnabled,
54
+ directEnabled,
55
+ fields: Object.freeze(CONTEXT_ENHANCEMENT_FIELDS.filter((field) => fields.includes(field))),
56
+ guidance: guidance.trim() ? guidance : '',
57
+ });
58
+ }
59
+
60
+ /** Missing or damaged enhancement settings must never break an existing bot. */
61
+ export function normalizeContextEnhancementConfig(input) {
62
+ try {
63
+ return validateContextEnhancementConfig(input);
64
+ } catch {
65
+ return DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
66
+ }
67
+ }
68
+
69
+ /** Capture before queueing. The off path reads only the applicable switch. */
70
+ export function captureContextEnhancement(provider, conversationType) {
71
+ if (conversationType !== 'group' && conversationType !== 'direct') return null;
72
+ try {
73
+ const settings = provider?.getSettings?.();
74
+ const enabledKey = conversationType === 'group' ? 'groupEnabled' : 'directEnabled';
75
+ if (settings?.[enabledKey] !== true) return null;
76
+ const config = normalizeContextEnhancementConfig(settings);
77
+ if (config[enabledKey] !== true) return null;
78
+ return Object.freeze({ config, botId: provider.botId, conversationType });
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function sourceString(value, field) {
85
+ if (field === 'senderId' && (typeof value === 'bigint' || Number.isFinite(value))) {
86
+ value = String(value);
87
+ }
88
+ if (typeof value !== 'string') return undefined;
89
+ const normalized = value.replace(CONTROL_CHARACTERS, '').trim().slice(0, SOURCE_LIMITS[field]);
90
+ if (!normalized || (field === 'channel' && !CHANNELS.has(normalized))) return undefined;
91
+ return normalized;
92
+ }
93
+
94
+ function sourceBlock(snapshot, sourceFactory) {
95
+ const { fields } = snapshot.config;
96
+ const needsSource = fields.some((field) => ['channel', 'senderId', 'senderName'].includes(field));
97
+ const source = needsSource ? sourceFactory?.() : null;
98
+ const projected = {};
99
+ for (const field of fields) {
100
+ const value = field === 'botId' || field === 'conversationType'
101
+ ? snapshot[field] : source?.[field];
102
+ const normalized = sourceString(value, field);
103
+ if (normalized !== undefined) projected[field] = normalized;
104
+ }
105
+ if (Object.keys(projected).length === 0) return '';
106
+ const json = JSON.stringify(projected).replace(/[<>&]/g, (character) => ({
107
+ '<': '\\u003c', '>': '\\u003e', '&': '\\u0026',
108
+ })[character]);
109
+ return `<dsh_im_source>${json}</dsh_im_source>`;
110
+ }
111
+
112
+ function guidanceBlock(guidance) {
113
+ if (!guidance.trim()) return '';
114
+ const body = guidance.replace(/<\/?dsh_im_source_guidance\b[^>]*(?:>|$)/gi, (tag) => (
115
+ tag.replace(/</g, '&lt;').replace(/>/g, '&gt;')
116
+ ));
117
+ return `<dsh_im_source_guidance>\n${body}\n</dsh_im_source_guidance>`;
118
+ }
119
+
120
+ /** Add one text prefix; never inspect sources, format or copy content when off. */
121
+ export function enhanceContextContent(content, snapshot, sourceFactory) {
122
+ if (!snapshot) return content;
123
+ try {
124
+ const blocks = [sourceBlock(snapshot, sourceFactory), guidanceBlock(snapshot.config.guidance)]
125
+ .filter(Boolean);
126
+ if (blocks.length === 0) return content;
127
+ const prefix = blocks.join('\n\n');
128
+ if (typeof content === 'string') return `${prefix}\n\n${content}`;
129
+ if (Array.isArray(content)) return [{ type: 'text', text: prefix }, ...content];
130
+ return content;
131
+ } catch {
132
+ // Only enhancement errors are isolated; the caller's original flow proceeds.
133
+ return content;
134
+ }
135
+ }
@@ -335,6 +335,8 @@ export default {
335
335
  // feishu/feishu-channel.mjs
336
336
  '正在生成…': 'Generating…',
337
337
  '回答完成': 'Answer complete',
338
+ '内容较长,生成完成后将分段发送完整回答。':
339
+ 'This response is long. The complete answer will be sent in parts when generation finishes.',
338
340
  '飞书机器人': 'Feishu bot',
339
341
 
340
342
  // feishu/message-utils.mjs
@@ -1,4 +1,5 @@
1
1
  import { t } from './i18n.mjs';
2
+ import { captureContextEnhancement, enhanceContextContent } from './context-enhancement.mjs';
2
3
  import { runWorkspaceCommand } from './workspace-command.mjs';
3
4
  import { runCompactCommand } from './compact-command.mjs';
4
5
  import { isHistoryCommand, runHistoryCommand } from './history-command.mjs';
@@ -127,6 +128,7 @@ export class TextHarnessBridge {
127
128
  #bot;
128
129
  #harness;
129
130
  #state;
131
+ #contextEnhancement;
130
132
  #status;
131
133
  #logger;
132
134
  #replyTimeoutMs;
@@ -134,7 +136,8 @@ export class TextHarnessBridge {
134
136
  #queues = new Map();
135
137
  #pendingInteractions = new Map();
136
138
  #interactionKeys = new Map();
137
- #acceptedMessageIds = new Set();
139
+ // Keep the accepted configuration through the existing queue/reply lifecycle.
140
+ #acceptedMessageIds = new Map();
138
141
  #approvalTasks = new Set();
139
142
  #commandTasks = new Set();
140
143
  #approvals;
@@ -145,6 +148,7 @@ export class TextHarnessBridge {
145
148
  bot,
146
149
  harness,
147
150
  state,
151
+ contextEnhancement,
148
152
  status = createTextBridgeStatus(),
149
153
  logger = console,
150
154
  replyTimeoutMs = 600_000,
@@ -157,6 +161,7 @@ export class TextHarnessBridge {
157
161
  this.#bot = bot;
158
162
  this.#harness = harness;
159
163
  this.#state = state;
164
+ this.#contextEnhancement = contextEnhancement;
160
165
  this.#status = status;
161
166
  this.#logger = logger;
162
167
  this.#replyTimeoutMs = replyTimeoutMs;
@@ -171,7 +176,7 @@ export class TextHarnessBridge {
171
176
  return structuredClone(this.#status);
172
177
  }
173
178
 
174
- accept(message) {
179
+ accept(message, { contextSnapshot } = {}) {
175
180
  if (this.#signal?.aborted) return Promise.resolve();
176
181
  const conversationId = cleanText(message?.conversationId);
177
182
  const kind = message?.kind === 'group' ? 'group' : 'direct';
@@ -182,7 +187,9 @@ export class TextHarnessBridge {
182
187
  || this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
183
188
  return Promise.resolve();
184
189
  }
185
- this.#acceptedMessageIds.add(messageId);
190
+ this.#acceptedMessageIds.set(messageId, contextSnapshot === undefined
191
+ ? captureContextEnhancement(this.#contextEnhancement, message?.kind)
192
+ : contextSnapshot);
186
193
  const statusReaction = beginStatusReaction({
187
194
  adapter: this.#bot,
188
195
  target: normalized.kind === 'direct' || normalized.addressed === true
@@ -614,9 +621,17 @@ export class TextHarnessBridge {
614
621
  );
615
622
  }
616
623
  }
617
- const content = hasImages
624
+ let content = hasImages
618
625
  ? await promptContentForMessage(message, { signal: this.#signal })
619
626
  : undefined;
627
+ const snapshot = this.#acceptedMessageIds.get(messageId);
628
+ if (snapshot) {
629
+ content = enhanceContextContent(content ?? text, snapshot, () => ({
630
+ channel: this.#descriptor.key,
631
+ senderId,
632
+ senderName: message.contextSource?.()?.senderName,
633
+ }));
634
+ }
620
635
  const { answer, artifacts = [] } = await askInWorkspaceSession({
621
636
  harness: this.#harness,
622
637
  state: this.#state,
@@ -346,6 +346,7 @@ export class SlackRuntime {
346
346
  #appToken;
347
347
  #harness;
348
348
  #state;
349
+ #contextEnhancement;
349
350
  #logger;
350
351
  #replyTimeoutMs;
351
352
  #connectTimeoutMs;
@@ -369,6 +370,7 @@ export class SlackRuntime {
369
370
  appToken,
370
371
  harness,
371
372
  state,
373
+ contextEnhancement,
372
374
  logger = console,
373
375
  replyTimeoutMs = 600_000,
374
376
  connectTimeoutMs = 20_000,
@@ -384,6 +386,7 @@ export class SlackRuntime {
384
386
  this.#appToken = appToken;
385
387
  this.#harness = harness;
386
388
  this.#state = state;
389
+ this.#contextEnhancement = contextEnhancement;
387
390
  this.#logger = logger;
388
391
  this.#replyTimeoutMs = replyTimeoutMs;
389
392
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -436,6 +439,7 @@ export class SlackRuntime {
436
439
  bot: client,
437
440
  harness: this.#harness,
438
441
  state: this.#state,
442
+ contextEnhancement: this.#contextEnhancement,
439
443
  status: this.#status,
440
444
  logger: this.#logger,
441
445
  replyTimeoutMs: this.#replyTimeoutMs,