Merge pull request #60 from LAN-SHH/fix/qq-c2c-markdown-replies

fix(qq): deliver C2C replies as markdown
This commit is contained in:
xiemanR 2026-08-25 23:40:41 +08:00 • committed by GitHub
commit 8f03c66775
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 8 additions and 53 deletions

File diff suppressed because one or more lines are too long

View file

@ -776,16 +776,8 @@ export class QqHarnessBridge {
const content = hasImages const content = hasImages
? await promptContentForMessage(promptMessage, { signal: this.#signal }) ? await promptContentForMessage(promptMessage, { signal: this.#signal })
: undefined; : undefined;
// QQ C2C keeps one stream bubble. Progress is collected but never submitted: // QQ stream_messages can acknowledge a final frame without rendering it in
// some clients reject replacing an already visible stream frame, which would // some C2C clients. Standard Markdown delivery is the reliable reply path.
// otherwise leave a stale progress bubble plus a separate fallback answer.
if (message.kind === 'c2c' && target?.msgId && typeof this.#bot.openStream === 'function') {
try {
stream = this.#bot.openStream({ target });
} catch (error) {
this.#logger.warn?.('[dsh-im:qq] unable to start a QQ stream; using markdown fallback:', error);
}
}
const toolErrors = []; const toolErrors = [];
let answer; let answer;
let artifacts = []; let artifacts = [];

View file

@ -808,18 +808,14 @@ test('QQ remembers any authorized private inbound as a connection-test target',
assert.equal(sent.length, 2); assert.equal(sent.length, 2);
}); });
test('QQ private messages produce one final stream bubble', async () => { test('QQ private messages deliver the final answer as Markdown without opening a stream', async () => {
const sent = []; const sent = [];
const frames = []; let streamCalls = 0;
const seen = new Set(); const seen = new Set();
const bridge = new QqHarnessBridge({ const bridge = new QqHarnessBridge({
bot: { bot: {
sendText: async (_target, text) => sent.push(text), sendText: async (_target, text) => sent.push(text),
openStream: () => ({ openStream: () => { streamCalls += 1; },
update: async (text) => frames.push(text),
complete: async () => frames.push('DONE'),
cancel() {},
}),
}, },
ownerUserOpenid: 'owner-openid', ownerUserOpenid: 'owner-openid',
harness: { harness: {
@ -843,45 +839,12 @@ test('QQ private messages produce one final stream bubble', async () => {
}); });
await bridge.accept(message()); await bridge.accept(message());
assert.deepEqual(frames, ['最终回答', 'DONE']); assert.deepEqual(sent, ['最终回答']);
assert.deepEqual(sent, []); assert.equal(streamCalls, 0);
assert.equal(seen.has('msg-1'), true); assert.equal(seen.has('msg-1'), true);
assert.equal(bridge.status.messagesReplied, 1); assert.equal(bridge.status.messagesReplied, 1);
}); });
test('QQ does not duplicate a visible private answer when stream completion fails', async () => {
const frames = [];
const sent = [];
const bridge = new QqHarnessBridge({
bot: {
sendText: async (_target, text) => sent.push(text),
openStream: () => ({
update: async (text) => frames.push(text),
complete: async () => { throw new Error('already submitted'); },
cancel() {},
}),
},
ownerUserOpenid: 'owner-openid',
harness: {
sessionExists: async () => true,
ask: async () => '最终回答',
},
state: {
hasSeen: () => false,
markSeen: async () => {},
sessionFor: () => 'session-stream-complete-failure',
setSession: async () => {},
clearSession: async () => {},
},
logger: { warn() {}, error() {} },
});
await bridge.accept(message({ messageId: 'msg-stream-complete-failure' }));
assert.deepEqual(frames, ['最终回答']);
assert.deepEqual(sent, []);
assert.equal(bridge.status.messagesReplied, 1);
});
test('QQ group messages suppress every successful progress update', async () => { test('QQ group messages suppress every successful progress update', async () => {
const sent = []; const sent = [];
let streamCalls = 0; let streamCalls = 0;