@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
@@ -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
+ }
@@ -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,
@@ -3,6 +3,7 @@ import { randomInt } from 'node:crypto';
3
3
  import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
4
4
  import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
5
5
  import { t } from '../shared/i18n.mjs';
6
+ import { captureContextEnhancement } from '../shared/context-enhancement.mjs';
6
7
  import { COMMANDS_MENU_BUTTON, TelegramApi } from './telegram-api.mjs';
7
8
  import { createTelegramBridgeStatus, TelegramHarnessBridge } from './telegram-bridge.mjs';
8
9
  import {
@@ -159,6 +160,11 @@ export function normalizeTelegramUpdate(update, {
159
160
  return {
160
161
  messageId: String(update.update_id),
161
162
  senderId: String(senderId),
163
+ contextSource: () => ({
164
+ senderName: [message.from?.first_name, message.from?.last_name]
165
+ .filter((value) => typeof value === 'string' && value.trim())
166
+ .map((value) => value.trim()).join(' ') || message.from?.username,
167
+ }),
162
168
  senderIsBot: message.from?.is_bot === true,
163
169
  kind: direct ? 'direct' : 'group',
164
170
  conversationId: messageThreadId === undefined
@@ -659,6 +665,7 @@ export class TelegramRuntime {
659
665
  #token;
660
666
  #harness;
661
667
  #state;
668
+ #contextEnhancement;
662
669
  #logger;
663
670
  #replyTimeoutMs;
664
671
  #createApi;
@@ -676,6 +683,7 @@ export class TelegramRuntime {
676
683
  token,
677
684
  harness,
678
685
  state,
686
+ contextEnhancement,
679
687
  logger = console,
680
688
  replyTimeoutMs = 600_000,
681
689
  createApi = (options) => new TelegramApi(options),
@@ -687,6 +695,7 @@ export class TelegramRuntime {
687
695
  this.#token = token;
688
696
  this.#harness = harness;
689
697
  this.#state = state;
698
+ this.#contextEnhancement = contextEnhancement;
690
699
  this.#logger = logger;
691
700
  this.#replyTimeoutMs = replyTimeoutMs;
692
701
  this.#createApi = createApi;
@@ -758,6 +767,7 @@ export class TelegramRuntime {
758
767
  bot: client,
759
768
  harness: this.#harness,
760
769
  state: this.#state,
770
+ contextEnhancement: this.#contextEnhancement,
761
771
  status: this.#status,
762
772
  logger: this.#logger,
763
773
  replyTimeoutMs: this.#replyTimeoutMs,
@@ -798,7 +808,19 @@ export class TelegramRuntime {
798
808
  while (!signal.aborted) {
799
809
  const updates = await this.#api.getUpdates({ offset: cursor, timeout: 25, signal });
800
810
  this.#status.lastCheckedAt = Date.now();
801
- for (const update of updates) {
811
+ if (signal.aborted) return;
812
+ // All updates have arrived together; cursor persistence must not move the
813
+ // settings boundary for the later messages in this received batch.
814
+ const received = updates.map((update) => {
815
+ const chatType = update?.message?.chat?.type;
816
+ return {
817
+ update,
818
+ contextSnapshot: captureContextEnhancement(this.#contextEnhancement,
819
+ chatType === 'private' ? 'direct'
820
+ : chatType === 'group' || chatType === 'supergroup' ? 'group' : null),
821
+ };
822
+ });
823
+ for (const { update, contextSnapshot } of received) {
802
824
  if (signal.aborted) return;
803
825
  const message = normalizeTelegramUpdate(update, {
804
826
  botId: this.#config.platformId,
@@ -810,7 +832,7 @@ export class TelegramRuntime {
810
832
  accessMode: this.#accessMode,
811
833
  allowedPrivateUserIds: this.#allowedPrivateUserIds,
812
834
  })) {
813
- void this.#bridge.accept(message).catch((error) => {
835
+ void this.#bridge.accept(message, { contextSnapshot }).catch((error) => {
814
836
  if (signal.aborted) return;
815
837
  this.#logger.error?.(
816
838
  `[dsh-im:telegram] bot ${this.#config.botId} message handling failed:`,
@@ -27,6 +27,7 @@ import {
27
27
  } from '../shared/preset-command.mjs';
28
28
  import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
29
29
  import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
30
+ import { captureContextEnhancement, enhanceContextContent } from '../shared/context-enhancement.mjs';
30
31
  import {
31
32
  hasInboundImages,
32
33
  ImagePromptError,
@@ -488,6 +489,7 @@ export class WecomHarnessBridge {
488
489
  #client;
489
490
  #harness;
490
491
  #state;
492
+ #contextEnhancement;
491
493
  #status;
492
494
  #logger;
493
495
  #replyTimeoutMs;
@@ -497,7 +499,8 @@ export class WecomHarnessBridge {
497
499
  #queues = new Map();
498
500
  #pendingInteractions = new Map();
499
501
  #interactionKeys = new Map();
500
- #acceptedMessageIds = new Set();
502
+ // Keep the accepted configuration through the existing queue/reply lifecycle.
503
+ #acceptedMessageIds = new Map();
501
504
  #approvalTasks = new Set();
502
505
  #commandTasks = new Set();
503
506
  #approvals;
@@ -508,6 +511,7 @@ export class WecomHarnessBridge {
508
511
  client,
509
512
  harness,
510
513
  state,
514
+ contextEnhancement,
511
515
  status = createWecomBridgeStatus(),
512
516
  logger = console,
513
517
  replyTimeoutMs = 600_000,
@@ -525,6 +529,7 @@ export class WecomHarnessBridge {
525
529
  this.#client = client;
526
530
  this.#harness = harness;
527
531
  this.#state = state;
532
+ this.#contextEnhancement = contextEnhancement;
528
533
  this.#status = status;
529
534
  this.#logger = logger;
530
535
  this.#replyTimeoutMs = replyTimeoutMs;
@@ -552,7 +557,10 @@ export class WecomHarnessBridge {
552
557
  || this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
553
558
 
554
559
  const key = conversationKey(frame);
555
- this.#acceptedMessageIds.add(messageId);
560
+ this.#acceptedMessageIds.set(messageId, captureContextEnhancement(
561
+ this.#contextEnhancement,
562
+ body.chattype === 'single' ? 'direct' : 'group',
563
+ ));
556
564
  if (body.chattype === 'single') {
557
565
  rememberConnectionTestTarget(this.#state, { chatId });
558
566
  }
@@ -948,9 +956,16 @@ export class WecomHarnessBridge {
948
956
  this.#logger.warn?.('[dsh-im:wecom] unable to start a stream; using an active reply:', error);
949
957
  }
950
958
 
951
- const content = hasImages
959
+ let content = hasImages
952
960
  ? await promptContentForMessage(message, { signal: this.#signal })
953
961
  : undefined;
962
+ const snapshot = this.#acceptedMessageIds.get(messageId);
963
+ if (snapshot) {
964
+ content = enhanceContextContent(content ?? text, snapshot, () => ({
965
+ channel: 'wecom',
966
+ senderId,
967
+ }));
968
+ }
954
969
  await this.#state.markSeen(messageId);
955
970
  promptRecorded = true;
956
971
  const { answer, artifacts = [] } = await askInWorkspaceSession({
@@ -28,6 +28,7 @@ export class WecomRuntime {
28
28
  #secret;
29
29
  #harness;
30
30
  #state;
31
+ #contextEnhancement;
31
32
  #logger;
32
33
  #replyTimeoutMs;
33
34
  #connectTimeoutMs;
@@ -45,6 +46,7 @@ export class WecomRuntime {
45
46
  secret,
46
47
  harness,
47
48
  state,
49
+ contextEnhancement,
48
50
  logger = console,
49
51
  replyTimeoutMs = 600_000,
50
52
  connectTimeoutMs = 20_000,
@@ -58,6 +60,7 @@ export class WecomRuntime {
58
60
  this.#secret = secret;
59
61
  this.#harness = harness;
60
62
  this.#state = state;
63
+ this.#contextEnhancement = contextEnhancement;
61
64
  this.#logger = logger;
62
65
  this.#replyTimeoutMs = replyTimeoutMs;
63
66
  this.#connectTimeoutMs = connectTimeoutMs;
@@ -107,6 +110,7 @@ export class WecomRuntime {
107
110
  client,
108
111
  harness: this.#harness,
109
112
  state: this.#state,
113
+ contextEnhancement: this.#contextEnhancement,
110
114
  status: this.#status,
111
115
  logger: this.#logger,
112
116
  replyTimeoutMs: this.#replyTimeoutMs,