fix: preserve quoted message context across channels

This commit is contained in:
xmanrui 2026-09-02 01:27:35 +08:00
parent c2be2389b0
commit 94e114b9b3
46 changed files with 4242 additions and 321 deletions

View file

@ -9,6 +9,7 @@ import {
weixinInboundMessage,
WeixinHarnessBridge,
} from '../../../src/channels/weixin/weixin-bridge.mjs';
import { WeixinStateStore } from '../../../src/channels/weixin/state-store.mjs';
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
import {
OUTBOUND_ARTIFACT_TOOL,
@ -65,6 +66,157 @@ function message(id, text, overrides = {}) {
};
}
test('Weixin bridge sends ref_msg context to Harness but does not execute quoted commands', async () => {
const fixture = stateFixture();
fixture.sessions.set('p2p:owner-user', 'session-quote');
let clears = 0;
let prompt;
fixture.state.clearSession = async () => { clears += 1; };
const bridge = new WeixinHarnessBridge({
api: { sendText: async () => ({ messageId: 'weixin-quote-answer' }) },
baseUrl: 'https://ilinkai.weixin.qq.com/',
token: 'host-token',
ownerUserId: 'owner-user',
harness: {
sessionExists: async () => true,
ask: async (_sessionId, content) => { prompt = content; return '已处理'; },
},
state: fixture.state,
});
await bridge.accept(message('weixin-quote-prompt', '这条指令是什么意思?', {
item_list: [{
type: 1,
text_item: { text: '这条指令是什么意思?' },
ref_msg: {
message_item: {
type: 1,
msg_id: 'quoted-command',
text_item: { text: '/new' },
},
},
}],
}));
assert.equal(clears, 0);
assert.equal(Array.isArray(prompt), true);
assert.match(prompt[0].text, /<dsh_im_reply_to>/);
assert.match(prompt[0].text, /"content":"\/new"/);
assert.deepEqual(prompt.at(-1), { type: 'text', text: '这条指令是什么意思?' });
});
test('Weixin bridge restores a metadata-only bot quote from its recent outbound index', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-quote-index-'));
const state = await new WeixinStateStore(join(root, 'state.json')).load();
await state.setSession('p2p:owner-user', 'session-quote-index');
const prompts = [];
let sends = 0;
const bridge = new WeixinHarnessBridge({
api: {
sendText: async () => ({ messageId: `outbound-${++sends}` }),
},
baseUrl: 'https://ilinkai.weixin.qq.com/',
token: 'host-token',
ownerUserId: 'owner-user',
harness: {
sessionExists: async () => true,
ask: async (_sessionId, content) => {
prompts.push(content);
return prompts.length === 1 ? 'deepseek-v4-flash' : '已识别引用';
},
},
state,
});
await bridge.accept(message('weixin-original-question', '这是什么模型?'));
const remembered = state.snapshot().recentOutboundMessages.at(-1);
assert.equal(remembered.text, 'deepseek-v4-flash');
await bridge.accept(message('weixin-metadata-quote', '这里说的是什么模型?', {
item_list: [{
type: 1,
text_item: { text: '这里说的是什么模型?' },
ref_msg: {
message_item: {
type: 8,
create_time_ms: remembered.sentAt,
update_time_ms: remembered.completedAt,
},
},
}],
}));
assert.match(prompts[1][0].text, /<dsh_im_reply_to>/);
assert.match(prompts[1][0].text, /"content":"deepseek-v4-flash"/);
await rm(root, { recursive: true, force: true });
});
test('Weixin bridge recovers a pre-index bot quote from bounded Session history', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-weixin-quote-history-'));
const state = await new WeixinStateStore(join(root, 'state.json')).load();
await state.setSession('p2p:owner-user', 'session-quote-history');
const quotedAt = Date.now() - 2_000;
const quotedMessageId = ((BigInt(quotedAt) << 22n) | 456n).toString();
const prompts = [];
const historyReads = [];
const session = {
sessionExists: async () => true,
readHistory: async (options) => {
historyReads.push(options);
return {
events: [{ event: {
type: 'assistant/message',
seq: 10,
time: quotedAt - 7_400,
data: {
turn: 1,
message: {
role: 'assistant',
content: [{ type: 'text', text: 'DeepSeek V4 Flash' }],
},
},
} }, { event: {
type: 'turn/end',
seq: 11,
time: quotedAt - 7_399,
data: { turn: 1, reason: { kind: 'completed' } },
} }],
hasMore: false,
};
},
ask: async (content) => {
prompts.push(content);
return '已识别旧引用';
},
};
const bridge = new WeixinHarnessBridge({
api: { sendText: async () => ({ messageId: 'history-answer' }) },
baseUrl: 'https://ilinkai.weixin.qq.com/',
token: 'host-token',
ownerUserId: 'owner-user',
harness: { workspaceSession: () => session },
state,
});
await bridge.accept(message('weixin-history-quote', '说的是什么模型?', {
item_list: [{
type: 1,
text_item: { text: '说的是什么模型?' },
ref_msg: {
message_item: { type: 7, msg_id: quotedMessageId },
},
}],
}));
assert.equal(historyReads.length, 1);
assert.match(prompts[0][0].text, /"content":"DeepSeek V4 Flash"/);
assert.doesNotMatch(prompts[0][0].text, /unavailableReason/);
assert.equal(state.recentOutboundTextFor({
toUserId: 'owner-user', messageId: quotedMessageId,
}), 'DeepSeek V4 Flash');
await rm(root, { recursive: true, force: true });
});
function stateFixture() {
const sessions = new Map();
const seen = new Set();
@ -2217,6 +2369,13 @@ test('Weixin batch input collects up to ten native text messages and submits one
});
await bridge.accept(message('batch-start', '/batch'));
await bridge.accept(message('batch-quote', '微信引用不能收录', {
item_list: [{
type: 1,
text_item: { text: '微信引用不能收录' },
ref_msg: { message_item: { type: 1, text_item: { text: '被引用内容' } } },
}],
}));
await bridge.accept(message('batch-voice', '', {
item_list: [{ type: 3, voice_item: { text: '语音转写不能收录' } }],
}));
@ -2239,7 +2398,7 @@ test('Weixin batch input collects up to ten native text messages and submits one
assert.equal(prompts[0].sessionId, 'session-batch');
assert.match(prompts[0].prompt, /\[消息 1\]\n内容 1/);
assert.match(prompts[0].prompt, /\[消息 10\]\n内容 10/);
assert.doesNotMatch(prompts[0].prompt, /语音转写不能收录|不会收录/);
assert.doesNotMatch(prompts[0].prompt, /微信引用不能收录|语音转写不能收录|不会收录/);
assert.equal(sent.at(-1).text, '批量完成');
await bridge.accept(message('after-batch', '恢复普通聊天'));