import type { MonitoringSourceItem } from "@geo/shared-types"; import { browser } from "wxt/browser"; import type { StoredMonitoringPlatformState } from "../monitoring-platforms"; import { closeSilentAutomationPage, hasAnyPlatformCookie, isPlatformReachable, normalizeDetectResult, openSilentAutomationPage, } from "./common"; import type { MonitoringAdapter, MonitoringAdapterQueryResult } from "./types"; 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_TAB_POLL_INTERVAL_MS = 500; type QwenPageQuestionSnapshot = { model?: string | null; deepSearch?: string | null; enableSearch?: boolean | null; cardText?: string | null; }; type QwenPageAnswerSnapshot = { reqId?: string | null; status?: string | null; content?: Record | null; extraInfo?: Record | null; communication?: Record | null; }; type QwenPageQuerySuccessResult = { ok: true; url: string; question: QwenPageQuestionSnapshot | null; answer: QwenPageAnswerSnapshot | null; }; type QwenPageQueryFailureResult = { ok: false; error: string; detail?: string | null; url?: string | null; question?: QwenPageQuestionSnapshot | null; answer?: QwenPageAnswerSnapshot | null; }; type QwenPageQueryResult = QwenPageQuerySuccessResult | QwenPageQueryFailureResult; function normalizeText(value: unknown): string | null { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed ? trimmed : null; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function readNestedString(source: unknown, path: string[]): string | null { let current: unknown = source; for (const segment of path) { if (!isRecord(current) || !(segment in current)) { return null; } current = current[segment]; } return normalizeText(current); } function normalizeUrl(value: unknown): string | null { const input = normalizeText(value); if (!input) { return null; } try { const url = new URL(input); url.hash = ""; return url.toString(); } catch { return input; } } function buildQwenSourceItem(source: unknown): MonitoringSourceItem | null { if (!isRecord(source)) { return null; } const url = normalizeUrl(source.url ?? source.href ?? source.link); if (!url) { return null; } let host: string | null = null; try { host = new URL(url).hostname || null; } catch { host = null; } return { url, title: normalizeText(source.title), site_name: normalizeText(source.name ?? source.site_name ?? source.authority), normalized_url: url, host, }; } function collectQwenSources(payload: unknown, result: MonitoringSourceItem[], depth = 0): void { if (depth > 12 || payload == null) { return; } if (Array.isArray(payload)) { for (const item of payload) { collectQwenSources(item, result, depth + 1); } return; } if (!isRecord(payload)) { return; } const sourceItem = buildQwenSourceItem(payload); if (sourceItem) { result.push(sourceItem); } for (const value of Object.values(payload)) { if (Array.isArray(value) || isRecord(value)) { collectQwenSources(value, result, depth + 1); } } } function dedupeSourceItems(items: MonitoringSourceItem[]): MonitoringSourceItem[] { const keyed = new Map(); for (const item of items) { const key = normalizeUrl(item.normalized_url ?? item.url); if (!key) { continue; } const existing = keyed.get(key); if (!existing) { keyed.set(key, { ...item, normalized_url: key, }); continue; } keyed.set(key, { ...existing, title: existing.title ?? item.title ?? null, site_name: existing.site_name ?? item.site_name ?? null, host: existing.host ?? item.host ?? null, }); } return Array.from(keyed.values()); } function resolveQwenProviderModel(pageResult: QwenPageQuerySuccessResult): string | null { return ( readNestedString(pageResult.answer?.extraInfo, ["chat_odps", "model_info", "model"]) ?? normalizeText(pageResult.question?.model) ?? (pageResult.question?.deepSearch === "1" ? "Qwen-deepThink" : "Qwen") ); } function formatQwenQueryError(result: QwenPageQueryFailureResult | QwenPageQuerySuccessResult): string { if (!result.ok) { const detail = normalizeText(result.detail); return detail ? `${result.error}: ${detail}` : result.error; } const status = normalizeText(result.answer?.status); return status ? `qwen query ended with status ${status}` : "qwen query failed"; } function hasQwenScriptingApi(): boolean { return typeof chrome !== "undefined" && Boolean(chrome.scripting?.executeScript); } function delay(ms: number): Promise { return new Promise((resolve) => { globalThis.setTimeout(resolve, ms); }); } async function waitForTabComplete(tabId: number, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() <= deadline) { const tab = await browser.tabs.get(tabId).catch(() => null); if (!tab) { return false; } if (tab.status === "complete") { return true; } await delay(QWEN_TAB_POLL_INTERVAL_MS); } const tab = await browser.tabs.get(tabId).catch(() => null); return tab?.status === "complete"; } async function runQwenMainWorldScript( tabId: number, func: (...args: TArgs) => TResult | Promise, args: TArgs, ): Promise { if (!hasQwenScriptingApi()) { throw new Error("qwen scripting api unavailable"); } const results = await chrome.scripting.executeScript({ target: { tabId }, world: "MAIN", func, args, }); return (results[0]?.result ?? null) as TResult; } function qwenQueryInPage(questionText: string, options?: { timeoutMs?: number; deepThink?: boolean }): Promise { const queryTimeoutMs = typeof options?.timeoutMs === "number" ? options.timeoutMs : 90_000; const deepThinkEnabled = options?.deepThink !== false; const wait = (ms: number) => new Promise((resolve) => { window.setTimeout(resolve, ms); }); const isObjectRecord = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); const normalizeInlineText = (value: unknown): string | null => { if (typeof value !== "string") { return null; } const trimmed = value.trim(); return trimmed ? trimmed : null; }; const toSerializable = (value: unknown): Record | null => { if (value == null) { return null; } try { return JSON.parse(JSON.stringify(value)) as Record; } catch { return null; } }; const locateManagers = () => { const bindingMap = (window as typeof window & { __TONGYI_PORTSL_GLOBAL_CONTAINER__?: { container?: { _bindingDictionary?: { _map?: Map } } } }) .__TONGYI_PORTSL_GLOBAL_CONTAINER__?.container?._bindingDictionary?._map; if (!(bindingMap instanceof Map)) { return null; } let textAreaManager: Record | null = null; let chatManager: Record | null = null; for (const bindings of bindingMap.values()) { if (!Array.isArray(bindings)) { continue; } for (const binding of bindings) { const candidate = isObjectRecord(binding) && "cache" in binding ? binding.cache : null; if (!isObjectRecord(candidate)) { continue; } const protoKeys = Object.getOwnPropertyNames(Object.getPrototypeOf(candidate) ?? {}); if (!textAreaManager && protoKeys.includes("setText") && protoKeys.includes("getWidget") && protoKeys.includes("enterMode")) { textAreaManager = candidate; } if (!chatManager && protoKeys.includes("textAreaSubmit") && protoKeys.includes("sendWithParams") && protoKeys.includes("submit")) { chatManager = candidate; } } } const state = ( window as typeof window & { __qianwenChatAPI?: { sharedValues?: { chatAPI?: { state?: Record; }; }; }; } ).__qianwenChatAPI?.sharedValues?.chatAPI?.state; if (!textAreaManager || !chatManager || !isObjectRecord(state)) { return null; } return { textAreaManager, chatManager, state, }; }; const waitForReadyManagers = async (timeoutMs: number) => { const deadline = Date.now() + timeoutMs; while (Date.now() <= deadline) { const managers = locateManagers(); if (managers) { return managers; } await wait(250); } return locateManagers(); }; const getLatestSnapshot = (minimumRoundCount: number) => { const state = ( window as typeof window & { __qianwenChatAPI?: { sharedValues?: { chatAPI?: { state?: { chatRounds?: Array>; }; }; }; }; } ).__qianwenChatAPI?.sharedValues?.chatAPI?.state; const chatRounds = Array.isArray(state?.chatRounds) ? state.chatRounds : []; const candidateRounds = chatRounds.length > minimumRoundCount ? chatRounds.slice(minimumRoundCount) : chatRounds; for (let index = candidateRounds.length - 1; index >= 0; index -= 1) { const round = candidateRounds[index]; if (!isObjectRecord(round)) { continue; } const questions = Array.isArray(round.questions) ? round.questions : []; const currentQuestionIndex = typeof round.currentQuestionIndex === "number" ? round.currentQuestionIndex : 0; const question = questions[currentQuestionIndex]; if (!isObjectRecord(question)) { continue; } const cards = Array.isArray(question.cards) ? question.cards : []; const firstCard = cards[0]; const cardText = isObjectRecord(firstCard) && "content" in firstCard ? normalizeInlineText(firstCard.content) : null; if (cardText !== questionText) { continue; } const answers = Array.isArray(question.answers) ? question.answers : []; const currentAnswerIndex = typeof question.currentAnswerIndex === "number" ? question.currentAnswerIndex : 0; const answer = answers[currentAnswerIndex]; return { question: { model: normalizeInlineText(question.model), deepSearch: normalizeInlineText(question.deepSearch), enableSearch: typeof question.enableSearch === "boolean" ? question.enableSearch : null, cardText, }, answer: isObjectRecord(answer) ? { reqId: normalizeInlineText(answer.reqId), status: normalizeInlineText(answer.status), content: toSerializable(answer.content), extraInfo: toSerializable(answer.extraInfo), communication: toSerializable(answer.communication), } : null, }; } return { question: null, answer: null, }; }; return (async (): Promise => { const managers = await waitForReadyManagers(20_000); if (!managers) { return { ok: false, error: "qwen_page_not_ready", url: window.location.href, }; } const { textAreaManager, chatManager, state } = managers; const initialRoundCount = Array.isArray(state.chatRounds) ? state.chatRounds.length : 0; try { if (typeof textAreaManager.reset === "function") { textAreaManager.reset(); } const widgetContainer = isObjectRecord(textAreaManager.inputWidgetContainer) ? textAreaManager.inputWidgetContainer : null; if (deepThinkEnabled && typeof widgetContainer?.activeWidget === "function") { await widgetContainer.activeWidget("deepThink"); } else if (typeof widgetContainer?.inactiveAllWidget === "function") { await widgetContainer.inactiveAllWidget(); } if (typeof textAreaManager.setText === "function") { textAreaManager.setText(questionText); } if (normalizeInlineText(textAreaManager.text) !== questionText && typeof textAreaManager.setTextImmediately === "function") { textAreaManager.setTextImmediately(questionText); } await wait(50); if (textAreaManager.isSubmitDisabled) { return { ok: false, error: "qwen_submit_disabled", detail: normalizeInlineText(textAreaManager.text) ?? "input_state_invalid", url: window.location.href, }; } if (typeof chatManager.textAreaSubmit !== "function") { return { ok: false, error: "qwen_submit_method_missing", url: window.location.href, }; } await chatManager.textAreaSubmit(); } catch (error) { return { ok: false, error: "qwen_submit_failed", detail: error instanceof Error ? error.message : String(error ?? ""), url: window.location.href, }; } const deadline = Date.now() + queryTimeoutMs; while (Date.now() <= deadline) { const snapshot = getLatestSnapshot(initialRoundCount); const answerStatus = normalizeInlineText(snapshot.answer?.status); if (answerStatus && ["finish", "failed", "interrupted"].includes(answerStatus)) { if (answerStatus === "finish") { return { ok: true, url: window.location.href, question: snapshot.question, answer: snapshot.answer, }; } return { ok: false, error: "qwen_query_finished_without_success", detail: answerStatus, url: window.location.href, question: snapshot.question, answer: snapshot.answer, }; } await wait(500); } const timeoutSnapshot = getLatestSnapshot(initialRoundCount); return { ok: false, error: "qwen_query_timeout", url: window.location.href, question: timeoutSnapshot.question, answer: timeoutSnapshot.answer, }; })(); } async function queryQwen(platform: StoredMonitoringPlatformState, questionText: string): Promise { const page = await openSilentAutomationPage(QWEN_BOOTSTRAP_URL); try { await waitForTabComplete(page.tabId, QWEN_PAGE_READY_TIMEOUT_MS); const pageResult = await runQwenMainWorldScript(page.tabId, qwenQueryInPage, [ questionText, { deepThink: true, timeoutMs: QWEN_QUERY_TIMEOUT_MS, }, ]); if (!pageResult?.ok || !pageResult.answer) { throw new Error(formatQwenQueryError(pageResult)); } const answerText = readNestedString(pageResult.answer.content, ["multiLoadIframe", "content"]) ?? readNestedString(pageResult.answer.content, ["barIframe", "content"]) ?? null; const requestId = normalizeText(pageResult.answer.reqId) ?? readNestedString(pageResult.answer.communication, ["reqid"]) ?? null; const sessionId = readNestedString(pageResult.answer.communication, ["sessionid"]); const providerModel = resolveQwenProviderModel(pageResult); const searchResults: MonitoringSourceItem[] = []; collectQwenSources(pageResult.answer.content, searchResults); const dedupedSearchResults = dedupeSourceItems(searchResults); if (!answerText && dedupedSearchResults.length === 0) { throw new Error("qwen empty response"); } return { provider_model: providerModel, provider_request_id: requestId, request_id: requestId ?? sessionId, answer: answerText, raw_response_json: { platform: platform.ai_platform_id, mode: pageResult.question?.deepSearch === "1" ? "deep_think" : "standard", page_url: pageResult.url, session_id: sessionId, provider_request_id: requestId, provider_model: providerModel, answer: answerText, question: pageResult.question ?? null, source_count: dedupedSearchResults.length, search_results: dedupedSearchResults, content: pageResult.answer.content ?? null, extra_info: pageResult.answer.extraInfo ?? null, communication: pageResult.answer.communication ?? null, status: "succeeded", }, citations: [], search_results: dedupedSearchResults, }; } finally { await closeSilentAutomationPage(page); } } export const qwenMonitoringAdapter: MonitoringAdapter = { async detect(platform) { const connected = await hasAnyPlatformCookie(platform); if (!hasQwenScriptingApi()) { return normalizeDetectResult(connected, false, "unsupported", "当前浏览器不支持千问静默采集"); } const reachable = await isPlatformReachable(platform); if (!reachable) { return normalizeDetectResult(connected, false, "unavailable", "平台当前不可访问"); } return normalizeDetectResult(connected, true, null, null); }, async canQuery(platform) { if (!hasQwenScriptingApi()) { return false; } return await isPlatformReachable(platform); }, async query(platform, task) { return await queryQwen(platform, task.question_text); }, };