feat: Implement Qwen adapter and enhance AI platform detection
- Added a new Qwen adapter to facilitate monitoring through Playwright, leveraging internal text/chat managers. - Updated account detection logic to recognize Qwen sessions based on persisted cookies, improving binding reliability. - Relaxed authentication requirements for monitor tasks, allowing anonymous execution for certain AI platforms. - Enhanced the generic AI platform detection to include Qwen-specific logic, ensuring accurate session identification. - Modified the monitoring dashboard to include runtime state indicating if the current user's desktop client is online. - Updated various files including `account-binder.ts`, `runtime-controller.ts`, and `monitoring_service.go` to support new features and improvements.
This commit is contained in:
@@ -1009,6 +1009,10 @@ function genericAIConversationPath(pathname: string): boolean {
|
||||
}
|
||||
|
||||
function isLikelyGenericAICredentialName(name: string): boolean {
|
||||
const normalized = name.trim().toLowerCase();
|
||||
if (/^(x[-_]?csrf[-_]?token|xsrf[-_]?token|csrf[-_]?token|csrf|_csrf)$/i.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(token|session|auth|login|passport|ticket|jwt|refresh|access)/i.test(name);
|
||||
}
|
||||
|
||||
@@ -1126,6 +1130,103 @@ async function detectGenericAIPlatform(
|
||||
});
|
||||
}
|
||||
|
||||
async function detectQwenFromSession(
|
||||
session: Session,
|
||||
hints: {
|
||||
displayName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
} = {},
|
||||
): Promise<DetectedAccount | null> {
|
||||
const cookies = await session.cookies.get({ url: "https://www.qianwen.com/" }).catch(() => []);
|
||||
if (!cookies.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookieMap = new Map<string, string>();
|
||||
for (const cookie of cookies) {
|
||||
if (!cookieMap.has(cookie.name)) {
|
||||
cookieMap.set(cookie.name, cookie.value);
|
||||
}
|
||||
}
|
||||
|
||||
const ticketHash = normalizeText(cookieMap.get("tongyi_sso_ticket_hash"));
|
||||
const ticket = normalizeText(cookieMap.get("tongyi_sso_ticket"));
|
||||
const platformUid = ticketHash ?? ticket;
|
||||
if (!platformUid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid,
|
||||
displayName: hints.displayName ?? "通义千问账号",
|
||||
avatarUrl: hints.avatarUrl ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async function detectQwenPlatform(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
const genericDetected = await detectGenericAIPlatform(platformId, label, context);
|
||||
if (genericDetected) {
|
||||
return genericDetected;
|
||||
}
|
||||
|
||||
if (!context.webContents || context.webContents.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentURL = context.webContents.getURL();
|
||||
const platformMeta = aiPlatformCatalog.find((item) => item.id === platformId) ?? null;
|
||||
if (!platformMeta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentURL && looksLikeLoginRedirect(currentURL, platformMeta.loginUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pageState = await readGenericAIPageState(context.webContents).catch(() => null);
|
||||
if ((pageState?.challengeSignalCount ?? 0) > 0) {
|
||||
return null;
|
||||
}
|
||||
if ((pageState?.loggedOutSignalCount ?? 0) > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fromSession = await detectQwenFromSession(context.session, {
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
});
|
||||
if (fromSession) {
|
||||
return fromSession;
|
||||
}
|
||||
|
||||
const fingerprint = await buildGenericAISessionFingerprint(platformId, context.session, pageState);
|
||||
if (!fingerprint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sanitizeDetectedAccount({
|
||||
platformUid: fingerprint,
|
||||
displayName: pageState?.displayName ?? `${label} 会话`,
|
||||
avatarUrl: pageState?.avatarUrl ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
async function detectAIPlatformAccount(
|
||||
platformId: string,
|
||||
label: string,
|
||||
context: DetectContext,
|
||||
): Promise<DetectedAccount | null> {
|
||||
if (platformId === "qwen") {
|
||||
return await detectQwenPlatform(platformId, label, context);
|
||||
}
|
||||
|
||||
return await detectGenericAIPlatform(platformId, label, context);
|
||||
}
|
||||
|
||||
async function detectToutiao({ session, webContents }: DetectContext): Promise<DetectedAccount | null> {
|
||||
const pageResponse = await pageFetchJson<ToutiaoMediaInfoResponse>(
|
||||
webContents,
|
||||
@@ -1537,7 +1638,7 @@ export async function probePublishAccountSession(
|
||||
};
|
||||
}
|
||||
|
||||
const detected = await detectGenericAIPlatform(account.platform, definition.label, {
|
||||
const detected = await detectAIPlatformAccount(account.platform, definition.label, {
|
||||
session: handle.session,
|
||||
webContents: window.webContents,
|
||||
});
|
||||
@@ -2329,7 +2430,7 @@ const aiBindingDefinitions: Record<string, PublishPlatformBindingDefinition> = O
|
||||
label: platform.label,
|
||||
loginUrl: platform.loginUrl,
|
||||
consoleUrl: platform.consoleUrl,
|
||||
detect: (context: DetectContext) => detectGenericAIPlatform(platform.id, platform.label, context),
|
||||
detect: (context: DetectContext) => detectAIPlatformAccount(platform.id, platform.label, context),
|
||||
},
|
||||
]),
|
||||
) as Record<string, PublishPlatformBindingDefinition>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./base";
|
||||
export * from "./doubao";
|
||||
export * from "./qwen";
|
||||
export * from "./toutiao";
|
||||
export * from "./zhihu";
|
||||
|
||||
@@ -0,0 +1,671 @@
|
||||
import type { JsonValue, MonitoringSourceItem } from "@geo/shared-types";
|
||||
|
||||
import { normalizeText } from "./common";
|
||||
import type { MonitorAdapter } from "./base";
|
||||
|
||||
const QWEN_BOOTSTRAP_URL = "https://www.qianwen.com/";
|
||||
const QWEN_PAGE_READY_TIMEOUT_MS = 20_000;
|
||||
const QWEN_QUERY_TIMEOUT_MS = 90_000;
|
||||
|
||||
type QwenPageQuestionSnapshot = {
|
||||
model: string | null;
|
||||
deepSearch: string | null;
|
||||
enableSearch: boolean | null;
|
||||
cardText: string | null;
|
||||
};
|
||||
|
||||
type QwenPageAnswerSnapshot = {
|
||||
reqId: string | null;
|
||||
status: string | null;
|
||||
content: Record<string, unknown> | null;
|
||||
extraInfo: Record<string, unknown> | null;
|
||||
communication: Record<string, unknown> | 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 isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
|
||||
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 normalizeOptionalString(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<string, MonitoringSourceItem>();
|
||||
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: QwenPageQueryResult): 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 extractQuestionText(payload: Record<string, unknown>): string {
|
||||
const candidates = [
|
||||
payload.question_text,
|
||||
payload.questionText,
|
||||
payload.query,
|
||||
payload.question,
|
||||
payload.prompt,
|
||||
payload.content,
|
||||
payload.title,
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const text = normalizeOptionalString(candidate);
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("qwen_question_text_missing");
|
||||
}
|
||||
|
||||
function toJsonValue(value: unknown, depth = 0): JsonValue {
|
||||
if (value == null || depth > 16) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "boolean") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === "number") {
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => toJsonValue(item, depth + 1));
|
||||
}
|
||||
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: Record<string, JsonValue> = {};
|
||||
for (const [key, item] of Object.entries(value)) {
|
||||
result[key] = toJsonValue(item, depth + 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function toJsonSources(items: MonitoringSourceItem[]): JsonValue[] {
|
||||
return items.map((item) => ({
|
||||
url: item.url,
|
||||
title: item.title ?? null,
|
||||
site_name: item.site_name ?? null,
|
||||
site_key: item.site_key ?? null,
|
||||
normalized_url: item.normalized_url ?? null,
|
||||
host: item.host ?? null,
|
||||
registrable_domain: item.registrable_domain ?? null,
|
||||
subdomain: item.subdomain ?? null,
|
||||
suffix: item.suffix ?? null,
|
||||
article_id: item.article_id ?? null,
|
||||
publish_record_id: item.publish_record_id ?? null,
|
||||
resolution_status: item.resolution_status ?? null,
|
||||
resolution_confidence: item.resolution_confidence ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildAdapterError(
|
||||
code: string,
|
||||
message: string,
|
||||
extras: Record<string, JsonValue> = {},
|
||||
): Record<string, JsonValue> {
|
||||
return {
|
||||
code,
|
||||
message,
|
||||
...extras,
|
||||
};
|
||||
}
|
||||
|
||||
function qwenQueryInPage(parameters: {
|
||||
questionText: string;
|
||||
timeoutMs: number;
|
||||
readyTimeoutMs: number;
|
||||
deepThink: boolean;
|
||||
}): Promise<QwenPageQueryResult> {
|
||||
const questionText = parameters.questionText;
|
||||
const queryTimeoutMs = parameters.timeoutMs;
|
||||
const readyTimeoutMs = parameters.readyTimeoutMs;
|
||||
const deepThinkEnabled = parameters.deepThink;
|
||||
|
||||
const wait = (timeMs: number) =>
|
||||
new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, timeMs);
|
||||
});
|
||||
|
||||
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
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<string, unknown> | null => {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildPageDetail = () => {
|
||||
const title = normalizeInlineText(document.title);
|
||||
const url = normalizeInlineText(window.location.href);
|
||||
const bodyText = normalizeInlineText(document.body?.innerText)?.slice(0, 120) ?? null;
|
||||
return [title, url, bodyText].filter(Boolean).join(" | ") || null;
|
||||
};
|
||||
|
||||
const locateManagers = () => {
|
||||
const bindingMap = (
|
||||
window as typeof window & {
|
||||
__TONGYI_PORTSL_GLOBAL_CONTAINER__?: {
|
||||
container?: {
|
||||
_bindingDictionary?: {
|
||||
_map?: Map<unknown, unknown[]>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
).__TONGYI_PORTSL_GLOBAL_CONTAINER__?.container?._bindingDictionary?._map;
|
||||
|
||||
if (!(bindingMap instanceof Map)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let textAreaManager: Record<string, unknown> | null = null;
|
||||
let chatManager: Record<string, unknown> | 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 prototype = Object.getPrototypeOf(candidate);
|
||||
const protoKeys = Object.getOwnPropertyNames(prototype ?? {});
|
||||
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<string, unknown>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
).__qianwenChatAPI?.sharedValues?.chatAPI?.state;
|
||||
|
||||
if (!textAreaManager || !chatManager || !isObjectRecord(state)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
textAreaManager,
|
||||
chatManager,
|
||||
state,
|
||||
};
|
||||
};
|
||||
|
||||
const waitForReadyManagers = async () => {
|
||||
const deadline = Date.now() + readyTimeoutMs;
|
||||
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<Record<string, unknown>>;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
).__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<QwenPageQueryResult> => {
|
||||
const managers = await waitForReadyManagers();
|
||||
if (!managers) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "qwen_page_not_ready",
|
||||
detail: buildPageDetail(),
|
||||
url: window.location.href || null,
|
||||
question: null,
|
||||
answer: null,
|
||||
};
|
||||
}
|
||||
|
||||
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) ?? buildPageDetail(),
|
||||
url: window.location.href || null,
|
||||
question: null,
|
||||
answer: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof chatManager.textAreaSubmit !== "function") {
|
||||
return {
|
||||
ok: false,
|
||||
error: "qwen_submit_method_missing",
|
||||
detail: buildPageDetail(),
|
||||
url: window.location.href || null,
|
||||
question: null,
|
||||
answer: null,
|
||||
};
|
||||
}
|
||||
|
||||
await chatManager.textAreaSubmit();
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "qwen_submit_failed",
|
||||
detail: error instanceof Error ? error.message : String(error ?? ""),
|
||||
url: window.location.href || null,
|
||||
question: null,
|
||||
answer: null,
|
||||
};
|
||||
}
|
||||
|
||||
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",
|
||||
detail: buildPageDetail(),
|
||||
url: window.location.href || null,
|
||||
question: timeoutSnapshot.question,
|
||||
answer: timeoutSnapshot.answer,
|
||||
};
|
||||
})();
|
||||
}
|
||||
|
||||
export const qwenAdapter: MonitorAdapter = {
|
||||
provider: "qwen",
|
||||
executionMode: "playwright",
|
||||
async query(context, payload) {
|
||||
if (!context.playwright?.page) {
|
||||
return {
|
||||
status: "failed",
|
||||
summary: "千问监测缺少 Playwright 页面上下文。",
|
||||
error: buildAdapterError("qwen_playwright_required", "qwen_playwright_required"),
|
||||
};
|
||||
}
|
||||
|
||||
const questionText = extractQuestionText(payload);
|
||||
const page = context.playwright.page;
|
||||
|
||||
context.reportProgress("qwen.page_ready");
|
||||
await page.waitForLoadState("domcontentloaded", {
|
||||
timeout: QWEN_PAGE_READY_TIMEOUT_MS,
|
||||
}).catch(() => undefined);
|
||||
|
||||
if (!page.url() || page.url() === "about:blank") {
|
||||
await page.goto(QWEN_BOOTSTRAP_URL, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: QWEN_PAGE_READY_TIMEOUT_MS,
|
||||
});
|
||||
}
|
||||
|
||||
context.reportProgress("qwen.query");
|
||||
const pageResult = await page.evaluate(qwenQueryInPage, {
|
||||
questionText,
|
||||
timeoutMs: QWEN_QUERY_TIMEOUT_MS,
|
||||
readyTimeoutMs: QWEN_PAGE_READY_TIMEOUT_MS,
|
||||
deepThink: true,
|
||||
});
|
||||
|
||||
if (!pageResult?.ok || !pageResult.answer) {
|
||||
const message = formatQwenQueryError(pageResult);
|
||||
return {
|
||||
status: "failed",
|
||||
summary: `千问请求失败:${message}`,
|
||||
error: buildAdapterError(
|
||||
"qwen_query_failed",
|
||||
message,
|
||||
{
|
||||
page_url: normalizeText(pageResult?.url) ?? page.url(),
|
||||
reason_code: pageResult?.ok ? "unknown" : pageResult.error,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
return {
|
||||
status: "unknown",
|
||||
summary: "千问返回为空,已回写 unknown 等待后续对账。",
|
||||
error: buildAdapterError("qwen_empty_response", "qwen returned no answer or sources"),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "succeeded",
|
||||
summary: "千问监控任务执行成功。",
|
||||
payload: {
|
||||
platform: "qwen",
|
||||
provider_model: providerModel,
|
||||
provider_request_id: requestId,
|
||||
request_id: requestId ?? sessionId,
|
||||
session_id: sessionId,
|
||||
answer: answerText,
|
||||
source_count: dedupedSearchResults.length,
|
||||
search_results: toJsonSources(dedupedSearchResults),
|
||||
citations: [],
|
||||
raw_response_json: {
|
||||
platform: "qwen",
|
||||
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: toJsonValue(pageResult.question),
|
||||
source_count: dedupedSearchResults.length,
|
||||
search_results: toJsonSources(dedupedSearchResults),
|
||||
content: toJsonValue(pageResult.answer.content),
|
||||
extra_info: toJsonValue(pageResult.answer.extraInfo),
|
||||
communication: toJsonValue(pageResult.answer.communication),
|
||||
status: "succeeded",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -15,6 +15,7 @@ import { getAIPlatformCatalogItem, isAIPlatformId } from "@geo/shared-types";
|
||||
|
||||
import {
|
||||
doubaoAdapter,
|
||||
qwenAdapter,
|
||||
toutiaoAdapter,
|
||||
zhihuAdapter,
|
||||
type AdapterExecutionResult,
|
||||
@@ -104,6 +105,7 @@ const pullIntervalMs = 60_000;
|
||||
const leaseExtendIntervalMs = 60_000;
|
||||
const maxActivityItems = 60;
|
||||
const monitorBusinessTimeZone = "Asia/Shanghai";
|
||||
const authRequiredMonitorPlatforms = new Set(["yuanbao", "kimi", "deepseek"]);
|
||||
|
||||
interface RuntimeTaskRecord {
|
||||
id: string;
|
||||
@@ -1131,7 +1133,7 @@ async function executeTaskAdapter(
|
||||
signal: AbortSignal,
|
||||
): Promise<AdapterExecutionResult> {
|
||||
const accountIdentity = accountIdentityFromTask(task);
|
||||
if (accountIdentity) {
|
||||
if (accountIdentity && shouldEnforceActiveAccount(task)) {
|
||||
const readiness = await ensureAccountReady(accountIdentity);
|
||||
if (readiness.authState !== "active") {
|
||||
return buildAccountBlockedResult(task, readiness);
|
||||
@@ -1261,6 +1263,18 @@ async function executeTaskAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
function shouldEnforceActiveAccount(task: RuntimeTaskRecord): boolean {
|
||||
if (task.kind === "publish") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isAIPlatformId(task.platform)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return authRequiredMonitorPlatforms.has(task.platform);
|
||||
}
|
||||
|
||||
function buildAccountBlockedResult(
|
||||
task: RuntimeTaskRecord,
|
||||
readiness: Awaited<ReturnType<typeof ensureAccountReady>>,
|
||||
@@ -1645,6 +1659,9 @@ function selectMonitorAdapter(platform: string): MonitorAdapter | null {
|
||||
if (platform === doubaoAdapter.provider) {
|
||||
return doubaoAdapter;
|
||||
}
|
||||
if (platform === qwenAdapter.provider) {
|
||||
return qwenAdapter;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user