2026-05-01 20:39:09 +08:00
|
|
|
|
import { describe, expect, it } from 'vitest'
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
import { __kimiTestUtils } from './kimi'
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
buildSourceItem,
|
|
|
|
|
|
classifyKimiSources,
|
2026-05-08 21:51:49 +08:00
|
|
|
|
isKimiAnswerComplete,
|
|
|
|
|
|
isKimiPromotionalAnswerNoise,
|
2026-04-25 17:38:32 +08:00
|
|
|
|
mergeKimiSourceLinksIntoContentSnapshot,
|
|
|
|
|
|
normalizeUrl,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
} = __kimiTestUtils
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
function buildSnapshot(
|
|
|
|
|
|
overrides: Partial<Parameters<typeof classifyKimiSources>[0]>,
|
|
|
|
|
|
): Parameters<typeof classifyKimiSources>[0] {
|
2026-04-25 17:38:32 +08:00
|
|
|
|
return {
|
2026-05-01 20:39:09 +08:00
|
|
|
|
url: 'https://www.kimi.com/chat/test',
|
|
|
|
|
|
title: 'Kimi',
|
|
|
|
|
|
currentModelLabel: 'K2.6 思考',
|
2026-04-25 17:38:32 +08:00
|
|
|
|
loginRequired: false,
|
|
|
|
|
|
loginReason: null,
|
|
|
|
|
|
busy: false,
|
|
|
|
|
|
busySignals: [],
|
|
|
|
|
|
sendDisabled: null,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
answerText: '答案',
|
|
|
|
|
|
answerBlocks: ['答案'],
|
2026-04-25 17:38:32 +08:00
|
|
|
|
reasoningText: null,
|
|
|
|
|
|
reasoningBlocks: [],
|
2026-05-08 21:51:49 +08:00
|
|
|
|
capacityNotice: null,
|
2026-04-25 17:38:32 +08:00
|
|
|
|
questionMatched: true,
|
|
|
|
|
|
candidateAnswerCount: 1,
|
|
|
|
|
|
candidateReasoningCount: 0,
|
|
|
|
|
|
citationLinks: [],
|
|
|
|
|
|
panelCitationLinks: [],
|
|
|
|
|
|
inlineCitationCandidateLinks: [],
|
|
|
|
|
|
searchResultLinks: [],
|
2026-05-01 20:39:09 +08:00
|
|
|
|
signature: 'sig',
|
2026-04-25 17:38:32 +08:00
|
|
|
|
...overrides,
|
2026-05-01 20:39:09 +08:00
|
|
|
|
}
|
2026-04-25 17:38:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
describe('kimi adapter helpers', () => {
|
|
|
|
|
|
it('unwraps Kimi redirect URLs before storing source items', () => {
|
|
|
|
|
|
expect(
|
|
|
|
|
|
normalizeUrl('https://www.kimi.com/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref'),
|
|
|
|
|
|
).toBe('https://example.com/source')
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
expect(
|
|
|
|
|
|
buildSourceItem({
|
|
|
|
|
|
url: 'https://www.kimi.com/redirect?target=https%3A%2F%2Fsource.example.com%2Fnews%3Fid%3D1',
|
|
|
|
|
|
title: '搜索结果标题',
|
|
|
|
|
|
text: null,
|
|
|
|
|
|
siteName: '来源站点',
|
|
|
|
|
|
}),
|
|
|
|
|
|
).toMatchObject({
|
|
|
|
|
|
url: 'https://source.example.com/news?id=1',
|
|
|
|
|
|
normalized_url: 'https://source.example.com/news?id=1',
|
|
|
|
|
|
title: '搜索结果标题',
|
|
|
|
|
|
site_name: '来源站点',
|
|
|
|
|
|
host: 'source.example.com',
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
it('uses search web results as citation sources and footer references as content citation candidates', () => {
|
|
|
|
|
|
const classified = classifyKimiSources(
|
|
|
|
|
|
buildSnapshot({
|
|
|
|
|
|
searchResultLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: 'https://search.example.com/a',
|
|
|
|
|
|
title: '搜索网页 A',
|
|
|
|
|
|
text: '搜索网页 A',
|
|
|
|
|
|
siteName: '搜索站点',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
panelCitationLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: 'https://content.example.com/cited',
|
|
|
|
|
|
title: '页尾引用文章',
|
|
|
|
|
|
text: '页尾引用文章',
|
|
|
|
|
|
siteName: '内容站点',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
inlineCitationCandidateLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: 'https://content.example.com/site',
|
|
|
|
|
|
title: null,
|
|
|
|
|
|
text: '内容站点',
|
|
|
|
|
|
siteName: '内容站点',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
expect(classified.citations).toHaveLength(1)
|
|
|
|
|
|
expect(classified.citations[0]?.url).toBe('https://search.example.com/a')
|
|
|
|
|
|
expect(classified.searchResults[0]?.url).toBe('https://search.example.com/a')
|
|
|
|
|
|
expect(classified.panelCitations[0]?.url).toBe('https://content.example.com/cited')
|
2026-04-25 17:38:32 +08:00
|
|
|
|
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
|
2026-05-01 20:39:09 +08:00
|
|
|
|
'https://content.example.com/cited',
|
|
|
|
|
|
'https://content.example.com/site',
|
|
|
|
|
|
])
|
|
|
|
|
|
})
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
it('falls back to footer references as citation sources when no search web results are visible', () => {
|
|
|
|
|
|
const classified = classifyKimiSources(
|
|
|
|
|
|
buildSnapshot({
|
|
|
|
|
|
panelCitationLinks: [
|
|
|
|
|
|
{
|
|
|
|
|
|
url: 'https://content.example.com/cited',
|
|
|
|
|
|
title: '页尾引用文章',
|
|
|
|
|
|
text: '页尾引用文章',
|
|
|
|
|
|
siteName: '内容站点',
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
)
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
expect(classified.searchResults).toHaveLength(0)
|
|
|
|
|
|
expect(classified.citations).toHaveLength(1)
|
|
|
|
|
|
expect(classified.citations[0]?.url).toBe('https://content.example.com/cited')
|
|
|
|
|
|
})
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
it('keeps the answer text from the content snapshot when merging side-panel sources', () => {
|
2026-04-25 17:38:32 +08:00
|
|
|
|
const merged = mergeKimiSourceLinksIntoContentSnapshot(
|
|
|
|
|
|
buildSnapshot({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
answerText: '真实回答正文',
|
|
|
|
|
|
answerBlocks: ['真实回答正文'],
|
2026-04-25 17:38:32 +08:00
|
|
|
|
}),
|
|
|
|
|
|
buildSnapshot({
|
2026-05-01 20:39:09 +08:00
|
|
|
|
answerText: '搜索网页 48\nhttps://example.com/a 很长的侧栏文本',
|
|
|
|
|
|
answerBlocks: ['搜索网页 48\nhttps://example.com/a 很长的侧栏文本'],
|
2026-04-25 17:38:32 +08:00
|
|
|
|
searchResultLinks: [
|
|
|
|
|
|
{
|
2026-05-01 20:39:09 +08:00
|
|
|
|
url: 'https://example.com/a',
|
|
|
|
|
|
title: '搜索结果 A',
|
|
|
|
|
|
text: '搜索结果 A',
|
|
|
|
|
|
siteName: 'Example',
|
2026-04-25 17:38:32 +08:00
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
}),
|
2026-05-01 20:39:09 +08:00
|
|
|
|
)
|
2026-04-25 17:38:32 +08:00
|
|
|
|
|
2026-05-01 20:39:09 +08:00
|
|
|
|
expect(merged.answerText).toBe('真实回答正文')
|
|
|
|
|
|
expect(merged.answerBlocks).toEqual(['真实回答正文'])
|
|
|
|
|
|
expect(merged.searchResultLinks).toHaveLength(1)
|
|
|
|
|
|
expect(merged.searchResultLinks[0]?.url).toBe('https://example.com/a')
|
|
|
|
|
|
})
|
2026-05-08 21:51:49 +08:00
|
|
|
|
|
|
|
|
|
|
it('rejects Kimi K2.6 marketing copy as an answer', () => {
|
|
|
|
|
|
const promotionalText = [
|
|
|
|
|
|
'Kimi K2.6,已就位!',
|
|
|
|
|
|
'Coding、Agent 集群、Kimi Claw 同步升级',
|
|
|
|
|
|
'Agent 集群,升级了',
|
|
|
|
|
|
'多技能调用,多产物类型一次性交付',
|
|
|
|
|
|
'Kimi Claw 已就位!',
|
|
|
|
|
|
'一键部署 OpenClaw,Kimi 24小时为你干活',
|
|
|
|
|
|
].join('\n')
|
|
|
|
|
|
|
|
|
|
|
|
expect(isKimiPromotionalAnswerNoise(promotionalText)).toBe(true)
|
|
|
|
|
|
expect(
|
|
|
|
|
|
isKimiAnswerComplete(
|
|
|
|
|
|
buildSnapshot({
|
|
|
|
|
|
answerText: promotionalText,
|
|
|
|
|
|
answerBlocks: [promotionalText],
|
|
|
|
|
|
}),
|
|
|
|
|
|
),
|
|
|
|
|
|
).toBe(false)
|
|
|
|
|
|
})
|
2026-05-01 20:39:09 +08:00
|
|
|
|
})
|