fix(desktop): guard Kimi adapter against incomplete and keyword-blob answers
Add heuristics to distinguish keyword fragments from substantive answers, enforce a grace period before returning incomplete content, and mark timed-out or reasoning-only captures as unknown so later passes can re-collect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user