@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.
- package/README.en.md +16 -1
- package/README.md +16 -1
- package/lib/client.js +1653 -333
- package/lib/index.js +235 -226
- package/package.json +5 -1
- package/plugin-src/client/channels/dingtalk/api.js +3 -0
- package/plugin-src/client/channels/dingtalk/index.js +18 -5
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +18 -5
- package/plugin-src/client/channels/qq/api.js +3 -0
- package/plugin-src/client/channels/qq/index.js +13 -0
- package/plugin-src/client/channels/shared/token-api.js +3 -0
- package/plugin-src/client/channels/shared/token-channel.js +13 -1
- package/plugin-src/client/channels/wecom/api.js +3 -0
- package/plugin-src/client/channels/wecom/index.js +13 -0
- package/plugin-src/client/channels/weixin/api.js +3 -0
- package/plugin-src/client/channels/weixin/index.js +19 -5
- package/plugin-src/client/channels/whatsapp/api.js +3 -0
- package/plugin-src/client/channels/whatsapp/index.js +13 -0
- package/plugin-src/client/context-enhancement.js +275 -0
- package/plugin-src/client/i18n.js +54 -3
- package/plugin-src/client/styles.js +78 -0
- package/plugin-src/client/update-panel.js +108 -7
- package/plugin-src/host/channels/dingtalk/index.mjs +1 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/discord/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/index.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +13 -0
- package/plugin-src/host/channels/qq/index.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +1 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/context-enhancement-rpc.mjs +17 -0
- package/plugin-src/host/channels/shared/production.mjs +1 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +1 -0
- package/plugin-src/host/channels/slack/index.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/telegram/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/index.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/weixin/index.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +1 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/index.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/plugin-src/host/harness-connection.mjs +9 -4
- package/plugin-src/host/index.mjs +43 -29
- package/plugin-src/host/modern-harness-api.mjs +577 -0
- package/plugin-src/host/update-runtime.mjs +3 -2
- package/plugin-src/host/update-service.mjs +2 -0
- package/scripts/verify-package.mjs +15 -2
- package/src/channels/dingtalk/dingtalk-api.mjs +39 -16
- package/src/channels/dingtalk/dingtalk-bridge.mjs +52 -23
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -1
- package/src/channels/discord/discord-runtime.mjs +18 -4
- package/src/channels/feishu/bridge.mjs +18 -3
- package/src/channels/feishu/feishu-runtime.mjs +4 -0
- package/src/channels/qq/qq-bridge.mjs +20 -4
- package/src/channels/qq/qq-runtime.mjs +4 -0
- package/src/channels/shared/bot-workspace-store.mjs +103 -6
- package/src/channels/shared/context-enhancement.mjs +135 -0
- package/src/channels/shared/harness-client.mjs +18 -0
- package/src/channels/shared/text-harness-bridge.mjs +19 -4
- package/src/channels/slack/slack-runtime.mjs +4 -0
- package/src/channels/telegram/telegram-runtime.mjs +24 -2
- package/src/channels/wecom/wecom-bridge.mjs +18 -3
- package/src/channels/wecom/wecom-runtime.mjs +4 -0
- package/src/channels/weixin/weixin-bridge.mjs +19 -4
- package/src/channels/weixin/weixin-runtime.mjs +4 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +5 -0
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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
|
|
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
|
|
631
|
+
/** Observe durable removals and finish failed cleanup before a same-ID config save. */
|
|
573
632
|
export function observeBotWorkspaceRemovals(
|
|
574
633
|
configStore,
|
|
575
|
-
{
|
|
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, '<').replace(/>/g, '>')
|
|
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
|
+
}
|
|
@@ -389,6 +389,24 @@ function consumeInteractionOwnership(ownership, entries) {
|
|
|
389
389
|
}
|
|
390
390
|
}
|
|
391
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Decide whether the current Host has a live dsh-im interaction watcher for
|
|
394
|
+
* this Session. The modern Harness adapter uses the same ownership registry
|
|
395
|
+
* as HarnessClient so it never steals a browser-owned question or approval.
|
|
396
|
+
*/
|
|
397
|
+
export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = []) {
|
|
398
|
+
if (!scope || !['object', 'function', 'string'].includes(typeof scope)
|
|
399
|
+
|| typeof sessionId !== 'string' || !sessionId) return false;
|
|
400
|
+
const owners = interactionRegistry(scope).ownerships.get(sessionId);
|
|
401
|
+
if (!owners || owners.size === 0) return false;
|
|
402
|
+
for (const ownership of owners) consumeInteractionOwnership(ownership, entries);
|
|
403
|
+
return [...owners].some((ownership) => (
|
|
404
|
+
ownership.active
|
|
405
|
+
&& !ownership.completed
|
|
406
|
+
&& typeof ownership.reconnect === 'function'
|
|
407
|
+
));
|
|
408
|
+
}
|
|
409
|
+
|
|
392
410
|
export class HarnessReplyTracker {
|
|
393
411
|
#promptRpcId;
|
|
394
412
|
#lastSeq;
|
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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:`,
|