513e71f9f1
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>
227 lines
7.5 KiB
TypeScript
227 lines
7.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { __doubaoTestUtils } from './doubao'
|
|
|
|
const {
|
|
buildSourceItem,
|
|
dedupeSourceItems,
|
|
isNonCitationAssetDomain,
|
|
isNonCitationAssetUrl,
|
|
parseDoubaoStreamBody,
|
|
resolveDoubaoSubmissionAnswer,
|
|
} = __doubaoTestUtils
|
|
|
|
describe('doubao adapter helpers', () => {
|
|
it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
|
|
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
|
|
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
|
|
expect(isNonCitationAssetUrl('https://p11-spider-image-sign.byteimg.com/obj/logo.png')).toBe(
|
|
true,
|
|
)
|
|
expect(isNonCitationAssetUrl('https://m.sohu.com/a/1012750625_122649842/')).toBe(false)
|
|
|
|
expect(
|
|
buildSourceItem({
|
|
url: 'https://p3-spider-image-sign.byteimg.com/img/logo.jpeg',
|
|
title: 'logo',
|
|
site_name: 'byteimg.com',
|
|
}),
|
|
).toBeNull()
|
|
expect(
|
|
buildSourceItem({
|
|
url: 'https://example.com/article',
|
|
title: '真实来源',
|
|
}),
|
|
).toMatchObject({
|
|
url: 'https://example.com/article',
|
|
host: 'example.com',
|
|
})
|
|
})
|
|
|
|
it('dedupes and drops asset CDN links parsed from Doubao streams', () => {
|
|
const body = [
|
|
'event: STREAM_CHUNK',
|
|
'data: {"references":[{"url":"https://p11-spider-image-sign.byteimg.com/logo.jpeg","title":"logo"},{"url":"https://m.sohu.com/a/1012750625_122649842/","title":"真实来源"}],"text_block":{"text":"回答"}}',
|
|
'',
|
|
'',
|
|
].join('\n')
|
|
|
|
const summary = parseDoubaoStreamBody(body)
|
|
expect(summary.search_results).toHaveLength(1)
|
|
expect(summary.search_results[0]?.url).toBe('https://m.sohu.com/a/1012750625_122649842/')
|
|
|
|
expect(
|
|
dedupeSourceItems([
|
|
{
|
|
url: 'https://static.bytednsdoc.com/logo.png',
|
|
normalized_url: 'https://static.bytednsdoc.com/logo.png',
|
|
},
|
|
{ url: 'https://example.com/a', normalized_url: 'https://example.com/a' },
|
|
]),
|
|
).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,
|
|
})
|
|
})
|
|
})
|