0f3a9a977e
Desktop Client Build / Resolve Build Metadata (push) Has been cancelled
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Frontend CI / Frontend (push) Successful in 6m6s
K2.6 introduced new UI surfaces that polluted the answer extraction and
the busy-signal probe, breaking monitor runs even though Kimi rendered a
correct answer:
- collectCandidates now drops elements that contain a chat composer child
(<textarea>, non-hidden <input>, [contenteditable]). This filters
wrapper containers like .chat-detail-content that the wide
[class*="content"] fallback would otherwise pick, pulling editor
placeholder text ("可快捷使用技能", "Kimi K2.6 已就位") into the answer.
- isAuxiliaryPanelElement skips elements with role="dialog" /
"alertdialog", aria-modal="true", or the <dialog> tag, so the K2.6
launch-announcement popover ("Kimi K2.6, 已就位 / 一键部署 OpenClaw")
no longer counts as answer content.
- Drop the class-based loading-indicator probe entirely. The previous
global [class*="loading"]/"stream"/"typing"/"spinner" sweep matched
K2.6's always-on text-stream wrapper and held busy=true forever, which
caused kimi_query_timeout even after the assistant turn had fully
rendered. The textual busySignalsPattern (思考中/搜索中/生成中…) is
the actual semantic signal and survives any UI rename.
- Surface a capacityNotice from the assistant turn when Kimi shows its
short capacity-exhausted message ("算力不够 / 请稍后再试"). The top
level falls back to it when answerText is empty so the SaaS pipeline
records the user-visible message instead of an unknown row, with
capacity_limited / capacity_notice flagged in raw_response_json.
All filters lean on HTML form / ARIA semantics and visible text rather
than Kimi-specific BEM classes, so they survive future redesigns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
171 lines
5.2 KiB
TypeScript
171 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { __kimiTestUtils } from './kimi'
|
||
|
||
const {
|
||
buildSourceItem,
|
||
classifyKimiSources,
|
||
isKimiAnswerComplete,
|
||
isKimiPromotionalAnswerNoise,
|
||
mergeKimiSourceLinksIntoContentSnapshot,
|
||
normalizeUrl,
|
||
} = __kimiTestUtils
|
||
|
||
function buildSnapshot(
|
||
overrides: Partial<Parameters<typeof classifyKimiSources>[0]>,
|
||
): Parameters<typeof classifyKimiSources>[0] {
|
||
return {
|
||
url: 'https://www.kimi.com/chat/test',
|
||
title: 'Kimi',
|
||
currentModelLabel: 'K2.6 思考',
|
||
loginRequired: false,
|
||
loginReason: null,
|
||
busy: false,
|
||
busySignals: [],
|
||
sendDisabled: null,
|
||
answerText: '答案',
|
||
answerBlocks: ['答案'],
|
||
reasoningText: null,
|
||
reasoningBlocks: [],
|
||
capacityNotice: null,
|
||
questionMatched: true,
|
||
candidateAnswerCount: 1,
|
||
candidateReasoningCount: 0,
|
||
citationLinks: [],
|
||
panelCitationLinks: [],
|
||
inlineCitationCandidateLinks: [],
|
||
searchResultLinks: [],
|
||
signature: 'sig',
|
||
...overrides,
|
||
}
|
||
}
|
||
|
||
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')
|
||
|
||
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',
|
||
})
|
||
})
|
||
|
||
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: '内容站点',
|
||
},
|
||
],
|
||
}),
|
||
)
|
||
|
||
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')
|
||
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
|
||
'https://content.example.com/cited',
|
||
'https://content.example.com/site',
|
||
])
|
||
})
|
||
|
||
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: '内容站点',
|
||
},
|
||
],
|
||
}),
|
||
)
|
||
|
||
expect(classified.searchResults).toHaveLength(0)
|
||
expect(classified.citations).toHaveLength(1)
|
||
expect(classified.citations[0]?.url).toBe('https://content.example.com/cited')
|
||
})
|
||
|
||
it('keeps the answer text from the content snapshot when merging side-panel sources', () => {
|
||
const merged = mergeKimiSourceLinksIntoContentSnapshot(
|
||
buildSnapshot({
|
||
answerText: '真实回答正文',
|
||
answerBlocks: ['真实回答正文'],
|
||
}),
|
||
buildSnapshot({
|
||
answerText: '搜索网页 48\nhttps://example.com/a 很长的侧栏文本',
|
||
answerBlocks: ['搜索网页 48\nhttps://example.com/a 很长的侧栏文本'],
|
||
searchResultLinks: [
|
||
{
|
||
url: 'https://example.com/a',
|
||
title: '搜索结果 A',
|
||
text: '搜索结果 A',
|
||
siteName: 'Example',
|
||
},
|
||
],
|
||
}),
|
||
)
|
||
|
||
expect(merged.answerText).toBe('真实回答正文')
|
||
expect(merged.answerBlocks).toEqual(['真实回答正文'])
|
||
expect(merged.searchResultLinks).toHaveLength(1)
|
||
expect(merged.searchResultLinks[0]?.url).toBe('https://example.com/a')
|
||
})
|
||
|
||
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)
|
||
})
|
||
})
|