fix(doubao): submit only verified SSE answers and harden source extraction
Rework the Doubao monitor adapter so success answers come exclusively from CHUNK_DELTA SSE text and require an SSE_REPLY_END finish event — DOM fallbacks and FULL_MSG_NOTIFY user echoes can no longer pose as the model reply. Also auto-switch to the 思考 answer mode, repair mixed-mojibake fragments per run, unwrap Doubao redirect URLs, scrub "unknown" source metadata, recover from mid-query navigation by returning unknown for retry, and surface stream/finish counters in diagnostics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ const {
|
||||
isNonCitationAssetDomain,
|
||||
isNonCitationAssetUrl,
|
||||
parseDoubaoStreamBody,
|
||||
resolveDoubaoSubmissionAnswer,
|
||||
} = __doubaoTestUtils
|
||||
|
||||
describe('doubao adapter helpers', () => {
|
||||
@@ -59,4 +60,167 @@ describe('doubao adapter helpers', () => {
|
||||
]),
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('uses thinking-chain reference metadata to fill unknown Doubao source titles', () => {
|
||||
const body = [
|
||||
'event: STREAM_CHUNK',
|
||||
'data: {"references":[{"url":"https://example.com/report","title":"unkown"}],"thinking":{"references":[{"url":"https://example.com/report#section","title":"思考链路里的真实标题","site_name":"示例站点"}]},"text_block":{"text":"回答"}}',
|
||||
'',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const summary = parseDoubaoStreamBody(body)
|
||||
|
||||
expect(summary.search_results).toHaveLength(1)
|
||||
expect(summary.search_results[0]?.url).toBe('https://example.com/report')
|
||||
expect(summary.search_results[0]?.title).toBe('思考链路里的真实标题')
|
||||
expect(summary.search_results[0]?.site_name).toBe('示例站点')
|
||||
})
|
||||
|
||||
it('unwraps Doubao redirect URLs before storing citation sources', () => {
|
||||
const item = buildSourceItem({
|
||||
url: 'https://www.doubao.com/link?target=https%3A%2F%2Fexample.com%2Fsource%23ref',
|
||||
title: 'unknown',
|
||||
})
|
||||
|
||||
expect(item).toMatchObject({
|
||||
url: 'https://example.com/source',
|
||||
normalized_url: 'https://example.com/source',
|
||||
host: 'example.com',
|
||||
title: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('parses Doubao answer deltas and source cards from the completion SSE stream', () => {
|
||||
const sourceChunkPayload = {
|
||||
patch_op: [
|
||||
{
|
||||
patch_value: {
|
||||
content_block: [
|
||||
{
|
||||
block_type: 10025,
|
||||
content: {
|
||||
search_query_result_block: {
|
||||
results: [
|
||||
{
|
||||
text_card: {
|
||||
url: 'https://k.sina.cn/article_3244012052_c15bb21400101k0hg.html',
|
||||
title: '2026年合肥全屋定制品牌综合实力排名出炉',
|
||||
sitename: '新浪',
|
||||
logo_url: 'https://p11-spider-image-sign.byteimg.com/logo.jpeg',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
const body = [
|
||||
'event: SSE_ACK',
|
||||
'data: {"ack_client_meta":{"conversation_id":"38427686224603138","section_id":"38427686224603394"}}',
|
||||
'',
|
||||
'event: FULL_MSG_NOTIFY',
|
||||
'data: {"message":{"content":"用户问题 echo,不是答案","conversation_id":"38427686224603138"}}',
|
||||
'',
|
||||
'event: STREAM_CHUNK',
|
||||
`data: ${JSON.stringify(sourceChunkPayload)}`,
|
||||
'',
|
||||
'event: CHUNK_DELTA',
|
||||
'data: {"text":"直接给你"}',
|
||||
'',
|
||||
'event: CHUNK_DELTA',
|
||||
'data: {"text":"一份清单。"}',
|
||||
'',
|
||||
'event: SSE_REPLY_END',
|
||||
'data: {"end_type":2,"answer_finish_attr":{"has_suggest":true}}',
|
||||
'',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const summary = parseDoubaoStreamBody(body)
|
||||
|
||||
expect(summary.answer).toBe('直接给你一份清单。')
|
||||
expect(summary.conversation_id).toBe('38427686224603138')
|
||||
expect(summary.full_message_event_count).toBe(1)
|
||||
expect(summary.reply_end_event_count).toBe(1)
|
||||
expect(summary.answer_finish_event_count).toBe(1)
|
||||
expect(summary.search_results).toHaveLength(1)
|
||||
expect(summary.search_results[0]).toMatchObject({
|
||||
url: 'https://k.sina.cn/article_3244012052_c15bb21400101k0hg.html',
|
||||
title: '2026年合肥全屋定制品牌综合实力排名出炉',
|
||||
site_name: '新浪',
|
||||
host: 'k.sina.cn',
|
||||
})
|
||||
})
|
||||
|
||||
it('repairs mixed Doubao mojibake fragments before returning the SSE answer', () => {
|
||||
const body = [
|
||||
'event: CHUNK_DELTA',
|
||||
'data: {"text":"需要åˆè‚¥地区全屋定制推è,"}',
|
||||
'',
|
||||
'event: CHUNK_DELTA',
|
||||
'data: {"text":"综合考量性价比ã€å£ç¢‘服务ç‰å› ç´ 。2000㎡展厅,核验批å·。"}',
|
||||
'',
|
||||
'event: SSE_REPLY_END',
|
||||
'data: {"end_type":2,"answer_finish_attr":{"has_suggest":true}}',
|
||||
'',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const summary = parseDoubaoStreamBody(body)
|
||||
|
||||
expect(summary.answer).toBe(
|
||||
'需要合肥地区全屋定制推荐,综合考量性价比、口碑服务等因素。2000㎡展厅,核验批号。',
|
||||
)
|
||||
expect(summary.answer).not.toMatch(/[åæèéäãï][\u0080-\u00ff\u02c0-\u02ff\u2000-\u20ff]/)
|
||||
expect(resolveDoubaoSubmissionAnswer({ answer: summary.answer })).toEqual({
|
||||
answer:
|
||||
'需要合肥地区全屋定制推荐,综合考量性价比、口碑服务等因素。2000㎡展厅,核验批号。',
|
||||
source: 'sse',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not treat FULL_MSG_NOTIFY user echo as the Doubao answer', () => {
|
||||
const body = [
|
||||
'event: FULL_MSG_NOTIFY',
|
||||
'data: {"message":{"content":"合肥全屋定制哪家好"}}',
|
||||
'',
|
||||
'event: SSE_REPLY_END',
|
||||
'data: {"end_type":2,"answer_finish_attr":{"has_suggest":true}}',
|
||||
'',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
const summary = parseDoubaoStreamBody(body)
|
||||
|
||||
expect(summary.answer).toBeNull()
|
||||
expect(summary.full_message_event_count).toBe(1)
|
||||
expect(summary.answer_finish_event_count).toBe(1)
|
||||
})
|
||||
|
||||
it('submits only SSE answer text, without reasoning fallback', () => {
|
||||
expect(
|
||||
resolveDoubaoSubmissionAnswer({
|
||||
answer: 'SSE 正文答案',
|
||||
}),
|
||||
).toEqual({
|
||||
answer: 'SSE 正文答案',
|
||||
source: 'sse',
|
||||
})
|
||||
})
|
||||
|
||||
it('does not submit a success answer when the SSE stream has no answer text', () => {
|
||||
expect(
|
||||
resolveDoubaoSubmissionAnswer({
|
||||
answer: null,
|
||||
}),
|
||||
).toEqual({
|
||||
answer: null,
|
||||
source: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user