@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.
- package/README.en.md +15 -0
- package/README.md +15 -0
- package/lib/client.js +1653 -333
- package/lib/index.js +235 -223
- 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/production.mjs +1 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- 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/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/production.mjs +1 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- 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/production.mjs +1 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -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 +20 -4
- package/src/channels/feishu/feishu-channel.mjs +100 -44
- 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/i18n-en/feishu.mjs +2 -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
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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,
|