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) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:55:08 +08:00
parent 04c89c6d4e
commit 6568d08a47
+45 -4
View File
@@ -6,6 +6,7 @@ import { normalizeText } from './common'
const QWEN_BOOTSTRAP_URL = 'https://www.qianwen.com/' const QWEN_BOOTSTRAP_URL = 'https://www.qianwen.com/'
const QWEN_PAGE_READY_TIMEOUT_MS = 20_000 const QWEN_PAGE_READY_TIMEOUT_MS = 20_000
const QWEN_QUERY_TIMEOUT_MS = 90_000 const QWEN_QUERY_TIMEOUT_MS = 90_000
const QWEN_INTERNAL_TAB_PLACEHOLDER_PATTERN = /^(?:\[\(tab_container_\d+\)\]\s*)+$/i
type QwenPageQuestionSnapshot = { type QwenPageQuestionSnapshot = {
model: string | null model: string | null
@@ -200,6 +201,30 @@ function extractQuestionText(payload: Record<string, unknown>): string {
throw new Error('qwen_question_text_missing') 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 { function toJsonValue(value: unknown, depth = 0): JsonValue {
if (value == null || depth > 16) { if (value == null || depth > 16) {
return null return null
@@ -630,10 +655,9 @@ export const qwenAdapter: MonitorAdapter = {
} }
} }
const answerText = const { answerText, unresolvedPlaceholder } = resolveQwenAnswerText(
readNestedString(pageResult.answer.content, ['multiLoadIframe', 'content']) ?? pageResult.answer.content,
readNestedString(pageResult.answer.content, ['barIframe', 'content']) ?? )
null
const requestId = const requestId =
normalizeText(pageResult.answer.reqId) ?? normalizeText(pageResult.answer.reqId) ??
readNestedString(pageResult.answer.communication, ['reqid']) ?? readNestedString(pageResult.answer.communication, ['reqid']) ??
@@ -645,6 +669,21 @@ export const qwenAdapter: MonitorAdapter = {
collectQwenSources(pageResult.answer.content, searchResults) collectQwenSources(pageResult.answer.content, searchResults)
const dedupedSearchResults = dedupeSourceItems(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) { if (!answerText && !dedupedSearchResults.length) {
return { return {
status: 'unknown', status: 'unknown',
@@ -689,4 +728,6 @@ export const qwenAdapter: MonitorAdapter = {
export const __qwenTestUtils = { export const __qwenTestUtils = {
extractQuestionText, extractQuestionText,
isQwenInternalPlaceholderOnly,
resolveQwenAnswerText,
} }