From 6568d08a47a5300c49ede71fa7cd338a516295f8 Mon Sep 17 00:00:00 2001 From: liangxu Date: Sun, 12 Jul 2026 11:55:08 +0800 Subject: [PATCH] feat(qwen-adapter): reject internal tab-container placeholder as answer When Qwen returns an unresolved `[(tab_container_N)]` placeholder in multiLoadIframe/barIframe content, treat it as an incomplete response: fall back to a resolved sibling field when available, otherwise return status `unknown` with a `qwen_incomplete_response` error so the record is retried on a later collection pass instead of persisting garbage. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/desktop-client/src/main/adapters/qwen.ts | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/apps/desktop-client/src/main/adapters/qwen.ts b/apps/desktop-client/src/main/adapters/qwen.ts index e01a5de..f61b85d 100644 --- a/apps/desktop-client/src/main/adapters/qwen.ts +++ b/apps/desktop-client/src/main/adapters/qwen.ts @@ -6,6 +6,7 @@ import { normalizeText } from './common' const QWEN_BOOTSTRAP_URL = 'https://www.qianwen.com/' const QWEN_PAGE_READY_TIMEOUT_MS = 20_000 const QWEN_QUERY_TIMEOUT_MS = 90_000 +const QWEN_INTERNAL_TAB_PLACEHOLDER_PATTERN = /^(?:\[\(tab_container_\d+\)\]\s*)+$/i type QwenPageQuestionSnapshot = { model: string | null @@ -200,6 +201,30 @@ function extractQuestionText(payload: Record): string { throw new Error('qwen_question_text_missing') } +function isQwenInternalPlaceholderOnly(value: unknown): boolean { + const text = normalizeOptionalString(value) + return Boolean(text && QWEN_INTERNAL_TAB_PLACEHOLDER_PATTERN.test(text)) +} + +function resolveQwenAnswerText(content: unknown): { + answerText: string | null + unresolvedPlaceholder: string | null +} { + const candidates = [ + readNestedString(content, ['multiLoadIframe', 'content']), + readNestedString(content, ['barIframe', 'content']), + ] + const answerText = candidates.find( + (candidate) => candidate && !isQwenInternalPlaceholderOnly(candidate), + ) + const unresolvedPlaceholder = candidates.find(isQwenInternalPlaceholderOnly) + + return { + answerText: answerText ?? null, + unresolvedPlaceholder: unresolvedPlaceholder ?? null, + } +} + function toJsonValue(value: unknown, depth = 0): JsonValue { if (value == null || depth > 16) { return null @@ -630,10 +655,9 @@ export const qwenAdapter: MonitorAdapter = { } } - const answerText = - readNestedString(pageResult.answer.content, ['multiLoadIframe', 'content']) ?? - readNestedString(pageResult.answer.content, ['barIframe', 'content']) ?? - null + const { answerText, unresolvedPlaceholder } = resolveQwenAnswerText( + pageResult.answer.content, + ) const requestId = normalizeText(pageResult.answer.reqId) ?? readNestedString(pageResult.answer.communication, ['reqid']) ?? @@ -645,6 +669,21 @@ export const qwenAdapter: MonitorAdapter = { collectQwenSources(pageResult.answer.content, searchResults) const dedupedSearchResults = dedupeSourceItems(searchResults) + if (!answerText && unresolvedPlaceholder) { + return { + status: 'unknown', + summary: '千问仅返回内部内容占位符,未拿到完整答案,已回写 unknown 等待后续补采。', + error: buildAdapterError( + 'qwen_incomplete_response', + 'qwen returned an unresolved internal tab-container placeholder', + { + placeholder: unresolvedPlaceholder, + source_count: dedupedSearchResults.length, + }, + ), + } + } + if (!answerText && !dedupedSearchResults.length) { return { status: 'unknown', @@ -689,4 +728,6 @@ export const qwenAdapter: MonitorAdapter = { export const __qwenTestUtils = { extractQuestionText, + isQwenInternalPlaceholderOnly, + resolveQwenAnswerText, }