feat: split group and direct context enhancement

This commit is contained in:
xmanrui 2026-09-01 02:06:22 +08:00
parent 38096d1589
commit 75ebaebf2e
21 changed files with 1423 additions and 873 deletions

View file

@ -260,7 +260,8 @@ for (const scenario of [
const order = [];
const asked = [];
const configFor = ([directEnabled, groupEnabled], guidance) => ({
directEnabled, groupEnabled, fields: ['channel', 'botId'], guidance,
group: { enabled: groupEnabled, fields: ['channel', 'botId'], guidance },
direct: { enabled: directEnabled, fields: ['channel', 'botId'], guidance },
});
let config = configFor(scenario.before, 'before callback returned');
let callback;

View file

@ -1446,10 +1446,12 @@ test('Discord captures context settings before asynchronous Thread routing and u
let socket;
let reads = 0;
let config = {
groupEnabled: true,
directEnabled: false,
fields: ['channel', 'conversationType', 'senderId', 'senderName', 'botId'],
guidance: 'accepted before routing',
group: {
enabled: true,
fields: ['channel', 'conversationType', 'senderId', 'senderName', 'botId'],
guidance: 'accepted before routing',
},
direct: { enabled: false, fields: [], guidance: 'direct must not leak' },
};
const runtime = new DiscordRuntime({
config: { botId: 'discord_internal', platformId: botId, name: 'Harness Discord' },
@ -1502,7 +1504,7 @@ test('Discord captures context settings before asynchronous Thread routing and u
member: { nick: 'Group Nick' }, mentions: [{ id: botId }], content: `<@${botId}> first`,
} }) });
await routingStarted.promise;
config = { ...config, groupEnabled: false };
config = { ...config, group: { ...config.group, enabled: false } };
releaseRouting.resolve();
await eventually(() => runtime.status.messagesReplied === 1);
assert.match(prompts[0], /accepted before routing/);

View file

@ -25,7 +25,20 @@ const FILE = Buffer.from('unchanged file bytes');
const logger = { info() {}, warn() {}, error() {} };
function settings(overrides = {}) {
return { groupEnabled: true, directEnabled: true, fields: [...CONTEXT_ENHANCEMENT_FIELDS], guidance: '', ...overrides };
const {
groupEnabled = true,
directEnabled = true,
fields = [...CONTEXT_ENHANCEMENT_FIELDS],
guidance = '',
group = {},
direct = {},
...extra
} = overrides;
return {
group: { enabled: groupEnabled, fields: [...fields], guidance, ...group },
direct: { enabled: directEnabled, fields: [...fields], guidance, ...direct },
...extra,
};
}
function provider(channel, config) {
@ -304,6 +317,24 @@ for (const channel of CHANNELS) {
});
}
if (channel !== 'weixin') {
test(`${channel}: group and direct fields and guidance stay isolated`, async () => {
const config = settings({
group: { enabled: true, fields: ['channel'], guidance: 'GROUP-ONLY-TOKEN' },
direct: { enabled: true, fields: ['botId'], guidance: 'DIRECT-ONLY-TOKEN' },
});
const current = fixture(channel, { contextEnhancement: provider(channel, config) });
await current.bridge.accept(current.event(1, 'direct message', { kind: 'direct' }));
await current.bridge.accept(current.event(2, 'group message', { kind: 'group' }));
assert.deepEqual(sourceOf(current.prompts[0]), { botId: `${channel}_internal` });
assert.match(current.prompts[0], /DIRECT-ONLY-TOKEN/);
assert.doesNotMatch(current.prompts[0], /GROUP-ONLY-TOKEN|"channel"/);
assert.deepEqual(sourceOf(current.prompts[1]), { channel });
assert.match(current.prompts[1], /GROUP-ONLY-TOKEN/);
assert.doesNotMatch(current.prompts[1], /DIRECT-ONLY-TOKEN|"botId"/);
});
}
test(`${channel}: enabled image/file assembly adds one prefix and preserves the remaining payload`, async () => {
for (const media of ['image', 'file', 'mixed']) {
const plain = fixture(channel);
@ -338,7 +369,7 @@ for (const channel of CHANNELS) {
const first = current.bridge.accept(current.event(1, 'first'));
await started.promise;
const queued = current.bridge.accept(current.event(2, 'queued'));
config.guidance = 'mutated after acceptance';
config.direct.guidance = 'mutated after acceptance';
config = settings({ fields: ['botId'], guidance: 'version two' });
const newer = current.bridge.accept(current.event(3, 'newer'));
config = settings({ groupEnabled: false, directEnabled: false });
@ -409,8 +440,8 @@ for (const channel of CHANNELS) {
for (const config of [settings({ fields: ['channel'] }), settings({ fields: [], guidance: 'custom' }), settings({ fields: [], guidance: '' })]) {
const current = fixture(channel, { contextEnhancement: provider(channel, config) });
await current.bridge.accept(current.event(1, 'hello'));
if (config.fields.length) assert.deepEqual(sourceOf(current.prompts[0]), { channel });
else if (config.guidance) assert.equal(current.prompts[0], '<dsh_im_source_guidance>\ncustom\n</dsh_im_source_guidance>\n\nhello');
if (config.direct.fields.length) assert.deepEqual(sourceOf(current.prompts[0]), { channel });
else if (config.direct.guidance) assert.equal(current.prompts[0], '<dsh_im_source_guidance>\ncustom\n</dsh_im_source_guidance>\n\nhello');
else assert.equal(current.prompts[0], 'hello');
}
});

View file

@ -77,18 +77,24 @@ for (const channel of ['wecom', 'weixin', 'feishu', 'dingtalk', 'qq', 'slack', '
assert.equal(provider.getSettings(), DEFAULT_CONTEXT_ENHANCEMENT_CONFIG);
assert.equal(captureContextEnhancement(provider, 'direct'), null);
const generation = workspaces.generationFor(botIds[0]);
const selected = { groupEnabled: false, directEnabled: true, fields: [], guidance: 'runtime update' };
const selected = {
group: { enabled: false, fields: ['botId'], guidance: 'stored group guidance' },
direct: { enabled: true, fields: [], guidance: 'runtime update' },
};
const saved = await production.controller.updateContextEnhancement(botIds[0], selected);
assert.deepEqual(saved.bots[0].contextEnhancement, selected);
assert.deepEqual(provider.getSettings(), selected);
const beforeDisable = captureContextEnhancement(provider, 'direct');
assert.deepEqual(beforeDisable.config, selected);
assert.deepEqual(beforeDisable.config, selected.direct);
assert.equal(runtimes[0].contextEnhancement, provider, 'runtime does not need reconstruction');
assert.equal(runtimes[1].contextEnhancement.getSettings(), DEFAULT_CONTEXT_ENHANCEMENT_CONFIG);
assert.equal(runtimes[1].contextEnhancement.botId, botIds[1]);
await production.controller.updateContextEnhancement(botIds[0], { ...selected, directEnabled: false });
await production.controller.updateContextEnhancement(botIds[0], {
...selected,
direct: { ...selected.direct, enabled: false },
});
assert.equal(captureContextEnhancement(provider, 'direct'), null);
assert.equal(beforeDisable.config.directEnabled, true, 'an already received message keeps its snapshot');
assert.equal(beforeDisable.config.enabled, true, 'an already received message keeps its snapshot');
assert.equal(workspaces.generationFor(botIds[0]), generation);
assert.equal(runtimes.length, 2);
assert.equal(stateLoads, 2);

View file

@ -1556,7 +1556,8 @@ for (const scenario of [
let settingsReads = 0;
let nextMessageId = 500;
const configFor = ([directEnabled, groupEnabled], guidance) => ({
directEnabled, groupEnabled, fields: ['channel', 'botId'], guidance,
group: { enabled: groupEnabled, fields: ['channel', 'botId'], guidance },
direct: { enabled: directEnabled, fields: ['channel', 'botId'], guidance },
});
let config = configFor(scenario.before, 'before cursor write');
const update = (id, kind) => ({