diff --git a/apps/desktop-client/src/main/adapters/kimi.ts b/apps/desktop-client/src/main/adapters/kimi.ts index be2b0ce..f2bb1ce 100644 --- a/apps/desktop-client/src/main/adapters/kimi.ts +++ b/apps/desktop-client/src/main/adapters/kimi.ts @@ -10,6 +10,7 @@ const KIMI_MODE_SWITCH_TIMEOUT_MS = 8_000; const KIMI_QUERY_TIMEOUT_MS = 120_000; const KIMI_QUERY_POLL_INTERVAL_MS = 1_200; const KIMI_STABLE_POLLS_REQUIRED = 5; +const KIMI_INCOMPLETE_ANSWER_GRACE_MS = 18_000; const KIMI_EDITOR_SELECTOR = "div[role=\"textbox\"].chat-input-editor, .chat-input-editor[role=\"textbox\"], [role=\"textbox\"][class*=\"chat-input-editor\"]"; const KIMI_SEND_BUTTON_SELECTOR = ".send-button-container"; const KIMI_THINKING_MODE_LABEL = "K2.6 思考"; @@ -404,6 +405,145 @@ async function ensureKimiCitationPanelOpen(page: PlaywrightPage, signal?: AbortS } } +function countMatches(input: string, pattern: RegExp): number { + const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`; + return input.match(new RegExp(pattern.source, flags))?.length ?? 0; +} + +function hasStructuredAnswerMarkers(text: string): boolean { + return ( + /(^|\n)\s*(?:[-*+]\s+|\d+\.\s+|#{1,6}\s+)/m.test(text) + || /(?:^|\n)\|.+\|(?:\n|$)/m.test(text) + || /(?:^|\n)\s*[^:\n]{1,24}[::]\s+\S+/m.test(text) + ); +} + +function isKimiTerminalShortAnswer(text: string): boolean { + const normalized = text.replace(/\s+/g, " ").trim(); + if (!normalized || normalized.length > 48) { + return false; + } + + return /(未找到|暂无|没有|无法|不能|抱歉|不确定|未检索到|未发现|无明显结果)/.test(normalized); +} + +function isKimiKeywordBlob(text: string): boolean { + const normalized = text.replace(/\s+/g, " ").trim(); + if (!normalized) { + return false; + } + + const punctuationCount = countMatches(normalized, /[。!?;:,、,.!?;:]/g); + const lineCount = normalized.split(/\n+/).filter(Boolean).length; + const tokenCount = normalized.split(/\s+/).filter(Boolean).length; + + return ( + !isKimiTerminalShortAnswer(normalized) + && !hasStructuredAnswerMarkers(normalized) + && lineCount <= 1 + && punctuationCount === 0 + && tokenCount >= 3 + && normalized.length <= 140 + ); +} + +function isKimiSubstantiveAnswer(answer: string, answerBlocks: string[]): boolean { + const normalized = answer.trim(); + if (!normalized) { + return false; + } + + const punctuationCount = countMatches(normalized, /[。!?;:,、,.!?;:]/g); + const lineCount = normalized.split(/\n+/).filter(Boolean).length; + if (hasStructuredAnswerMarkers(normalized)) { + return true; + } + if (answerBlocks.length >= 2) { + return true; + } + if (normalized.length >= 140) { + return true; + } + if (normalized.length >= 80 && punctuationCount >= 2) { + return true; + } + if (normalized.length >= 60 && lineCount >= 3) { + return true; + } + return false; +} + +function isKimiAnswerComplete(snapshot: KimiPageSnapshot): boolean { + const answer = normalizeOptionalString(snapshot.answerText); + if (!answer) { + return false; + } + if (isKimiKeywordBlob(answer)) { + return false; + } + if (isKimiTerminalShortAnswer(answer)) { + return true; + } + if (isKimiSubstantiveAnswer(answer, snapshot.answerBlocks)) { + return true; + } + + const punctuationCount = countMatches(answer, /[。!?;:,、,.!?;:]/g); + if (snapshot.citationLinks.length >= 1 && answer.length >= 50 && punctuationCount >= 1) { + return true; + } + if (snapshot.searchResultLinks.length >= 3 && answer.length >= 70 && punctuationCount >= 1) { + return true; + } + if (snapshot.reasoningBlocks.length >= 2 && answer.length >= 70 && punctuationCount >= 1) { + return true; + } + return false; +} + +function shouldReturnStableKimiAnswer( + snapshot: KimiPageSnapshot, + firstChangedContentAt: number | null, + now: number, +): boolean { + if (isKimiAnswerComplete(snapshot)) { + return true; + } + + const answer = normalizeOptionalString(snapshot.answerText); + if (!answer || isKimiKeywordBlob(answer) || firstChangedContentAt === null) { + return false; + } + + const changedContentAge = now - firstChangedContentAt; + if (changedContentAge < KIMI_INCOMPLETE_ANSWER_GRACE_MS) { + return false; + } + + const punctuationCount = countMatches(answer, /[。!?;:,、,.!?;:]/g); + return ( + answer.length >= 48 + || punctuationCount >= 1 + || snapshot.answerBlocks.length >= 2 + || isKimiTerminalShortAnswer(answer) + ); +} + +function hasKimiObservedContent(snapshot: KimiPageSnapshot | null): boolean { + if (!snapshot) { + return false; + } + + return Boolean( + normalizeOptionalString(snapshot.answerText) + || normalizeOptionalString(snapshot.reasoningText) + || snapshot.answerBlocks.length > 0 + || snapshot.reasoningBlocks.length > 0 + || snapshot.citationLinks.length > 0 + || snapshot.searchResultLinks.length > 0, + ); +} + function answerQualityScore(snapshot: KimiPageSnapshot | null): number { if (!snapshot) { return Number.NEGATIVE_INFINITY; @@ -428,6 +568,15 @@ function answerQualityScore(snapshot: KimiPageSnapshot | null): number { if (snapshot.questionMatched) { score += 600; } + if (!hasKimiObservedContent(snapshot)) { + score -= 2_000; + } + if (answer && isKimiKeywordBlob(answer)) { + score -= 1_000; + } + if (isKimiAnswerComplete(snapshot)) { + score += 1_500; + } if (snapshot.busy) { score -= 120; } @@ -442,6 +591,15 @@ function pickBetterSnapshot(current: KimiPageSnapshot | null, candidate: KimiPag return candidate; } + const currentHasContent = hasKimiObservedContent(current); + const candidateHasContent = hasKimiObservedContent(candidate); + if (candidateHasContent && !currentHasContent) { + return candidate; + } + if (currentHasContent && !candidateHasContent) { + return current; + } + return answerQualityScore(candidate) >= answerQualityScore(current) ? candidate : current; } @@ -1851,9 +2009,11 @@ async function waitForKimiAnswer( await pinKimiConversationToBottom(page); let stablePollCount = 0; let bestSnapshot = await readKimiPageSnapshot(page, questionText); + let bestContentSnapshot = hasKimiObservedContent(bestSnapshot) ? bestSnapshot : null; const initialSignature = bestSnapshot.signature; let lastSignature: string | null = initialSignature; let sawChange = false; + let firstChangedContentAt: number | null = null; while (Date.now() - startedAt <= KIMI_QUERY_TIMEOUT_MS) { if (signal.aborted) { @@ -1863,6 +2023,9 @@ async function waitForKimiAnswer( await pinKimiConversationToBottom(page); const snapshot = await readKimiPageSnapshot(page, questionText); bestSnapshot = pickBetterSnapshot(bestSnapshot, snapshot); + if (hasKimiObservedContent(snapshot)) { + bestContentSnapshot = pickBetterSnapshot(bestContentSnapshot, snapshot); + } if (snapshot.loginRequired) { return { @@ -1882,6 +2045,9 @@ async function waitForKimiAnswer( if (snapshot.signature && hasContent) { if (snapshot.signature !== initialSignature) { sawChange = true; + if (firstChangedContentAt === null) { + firstChangedContentAt = Date.now(); + } } if (snapshot.signature === lastSignature) { stablePollCount = snapshot.busy ? 0 : stablePollCount + 1; @@ -1891,9 +2057,19 @@ async function waitForKimiAnswer( } } - if (sawChange && hasContent && !snapshot.busy && stablePollCount >= KIMI_STABLE_POLLS_REQUIRED) { + const now = Date.now(); + if ( + sawChange + && hasContent + && !snapshot.busy + && stablePollCount >= KIMI_STABLE_POLLS_REQUIRED + && shouldReturnStableKimiAnswer(snapshot, firstChangedContentAt, now) + ) { await ensureKimiCitationPanelOpen(page, signal); - const finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText)); + let finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText)); + if (!hasKimiObservedContent(finalSnapshot) && bestContentSnapshot) { + finalSnapshot = pickBetterSnapshot(finalSnapshot, bestContentSnapshot); + } return { ok: true, finalSnapshot, @@ -1907,7 +2083,10 @@ async function waitForKimiAnswer( await pinKimiConversationToBottom(page); await ensureKimiCitationPanelOpen(page, signal); - const finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText)); + let finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText)); + if (!hasKimiObservedContent(finalSnapshot) && bestContentSnapshot) { + finalSnapshot = pickBetterSnapshot(finalSnapshot, bestContentSnapshot); + } return { ok: false, error: "kimi_query_timeout", @@ -2006,6 +2185,8 @@ export const kimiAdapter: MonitorAdapter = { ); const answer = normalizeText(finalSnapshot.answerText); const reasoning = normalizeText(finalSnapshot.reasoningText); + const answerComplete = isKimiAnswerComplete(finalSnapshot); + const keywordBlobDetected = answer ? isKimiKeywordBlob(answer) : false; const providerModel = normalizeText(finalSnapshot.currentModelLabel) ?? KIMI_THINKING_MODE_LABEL; const conversationID = extractConversationID(finalSnapshot.url); @@ -2024,6 +2205,28 @@ export const kimiAdapter: MonitorAdapter = { } if (!answer && !searchResults.length && !citations.length) { + const timedOut = !queryResult.ok && queryResult.error === "kimi_query_timeout"; + const hasReasoningOnly = Boolean(reasoning); + if (timedOut || hasReasoningOnly) { + const incompleteDetail = !queryResult.ok + ? queryResult.detail ?? queryResult.error + : "kimi returned no final answer before completion"; + return { + status: "unknown", + summary: "Kimi 超时未拿到完整答案,已回写 unknown 等待后续补采。", + error: buildAdapterError( + timedOut ? queryResult.error : "kimi_incomplete_response", + incompleteDetail, + { + page_url: finalSnapshot.url, + current_model_label: finalSnapshot.currentModelLabel, + reasoning_present: Boolean(reasoning), + busy: finalSnapshot.busy, + }, + ), + }; + } + return queryResult.ok ? { status: "unknown", @@ -2072,6 +2275,8 @@ export const kimiAdapter: MonitorAdapter = { busy_signals: finalSnapshot.busySignals, send_disabled: finalSnapshot.sendDisabled, answer: answer ?? null, + answer_complete: answerComplete, + answer_keyword_blob_detected: keywordBlobDetected, answer_block_count: finalSnapshot.answerBlocks.length, answer_blocks: finalSnapshot.answerBlocks, reasoning: reasoning ?? null,