feat(desktop): add Kimi and Yuanbao monitoring adapters

- implement kimi/yuanbao monitor adapters with dedicated citation panel detection
- detect Kimi session via kimi-auth cookie during account binding
- harden hidden Playwright and bound windows with skipTaskbar/focusable/hiddenInMissionControl to stop stealing focus
- tag hidden bootstrap windows with a unique token so CDP retention resolves the correct page
- enable yuanbao/kimi/wenxin platforms in dev-seed and default monitoring quota
- update kimi loginUrl to www.kimi.com

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 09:09:20 +08:00
parent 4142c53fa6
commit f91d2645d3
10 changed files with 4035 additions and 12 deletions
+87 -1
View File
@@ -1163,6 +1163,32 @@ async function detectQwenFromSession(
}); });
} }
async function detectKimiFromSession(
session: Session,
hints: {
displayName?: string | null;
avatarUrl?: string | null;
} = {},
): Promise<DetectedAccount | null> {
const cookies = await session.cookies.get({ url: "https://www.kimi.com/" }).catch(() => []);
if (!cookies.length) {
return null;
}
const authCookie = cookies.find((cookie) =>
cookie.name === "kimi-auth" && isLikelyGenericAICredentialValue(cookie.value),
);
if (!authCookie) {
return null;
}
return sanitizeDetectedAccount({
platformUid: boundedIdentity("kimi-auth", authCookie.value),
displayName: hints.displayName ?? "Kimi 会话",
avatarUrl: hints.avatarUrl ?? null,
});
}
async function detectQwenPlatform( async function detectQwenPlatform(
platformId: string, platformId: string,
label: string, label: string,
@@ -1215,6 +1241,58 @@ async function detectQwenPlatform(
}); });
} }
async function detectKimiPlatform(
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 detectKimiFromSession(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( async function detectAIPlatformAccount(
platformId: string, platformId: string,
label: string, label: string,
@@ -1224,6 +1302,10 @@ async function detectAIPlatformAccount(
return await detectQwenPlatform(platformId, label, context); return await detectQwenPlatform(platformId, label, context);
} }
if (platformId === "kimi") {
return await detectKimiPlatform(platformId, label, context);
}
return await detectGenericAIPlatform(platformId, label, context); return await detectGenericAIPlatform(platformId, label, context);
} }
@@ -2446,14 +2528,18 @@ function createBoundWindow(
sessionHandle: Session, sessionHandle: Session,
options: { show?: boolean } = {}, options: { show?: boolean } = {},
): BrowserWindow { ): BrowserWindow {
const show = options.show ?? true;
const window = new BrowserWindow({ const window = new BrowserWindow({
show: options.show ?? true, show,
width: 1320, width: 1320,
height: 900, height: 900,
minWidth: 1100, minWidth: 1100,
minHeight: 760, minHeight: 760,
title, title,
autoHideMenuBar: true, autoHideMenuBar: true,
skipTaskbar: !show,
focusable: show,
hiddenInMissionControl: !show,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea", backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
webPreferences: { webPreferences: {
session: sessionHandle, session: sessionHandle,
@@ -1,5 +1,7 @@
export * from "./base"; export * from "./base";
export * from "./doubao"; export * from "./doubao";
export * from "./kimi";
export * from "./qwen"; export * from "./qwen";
export * from "./toutiao"; export * from "./toutiao";
export * from "./yuanbao";
export * from "./zhihu"; export * from "./zhihu";
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+20
View File
@@ -233,6 +233,26 @@ async function createMainWindow(): Promise<ElectronBrowserWindow> {
} }
}); });
window.on("minimize", () => {
console.warn("[desktop-main] main window minimize event", {
at: new Date().toISOString(),
stack: new Error("trace").stack,
});
});
window.on("hide", () => {
console.warn("[desktop-main] main window hide event", {
at: new Date().toISOString(),
stack: new Error("trace").stack,
});
});
window.on("blur", () => {
console.warn("[desktop-main] main window blur event", {
at: new Date().toISOString(),
visible: window.isVisible(),
minimized: window.isMinimized(),
});
});
await mountRendererView(window); await mountRendererView(window);
return window; return window;
} }
+79 -7
View File
@@ -8,6 +8,7 @@ const hiddenPlaywrightIdleTTLms = 5 * 60_000;
const hiddenPlaywrightReaperIntervalMs = 60_000; const hiddenPlaywrightReaperIntervalMs = 60_000;
const defaultConnectTimeoutMs = 10_000; const defaultConnectTimeoutMs = 10_000;
const defaultPageTimeoutMs = 45_000; const defaultPageTimeoutMs = 45_000;
const hiddenBootstrapMetaName = "geo-hidden-playwright-token";
const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10); const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10);
const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`; const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`;
const cdpTargetsListURL = `${cdpEndpointURL}/json/list`; const cdpTargetsListURL = `${cdpEndpointURL}/json/list`;
@@ -79,8 +80,10 @@ export async function retainHiddenPlaywrightPage(options: {
const browser = await connectBrowserOverCDP(); const browser = await connectBrowserOverCDP();
const knownPages = new Set(defaultContext(browser).pages()); const knownPages = new Set(defaultContext(browser).pages());
const window = createHiddenWindow(options.title, options.session); const bootstrapToken = createHiddenBootstrapToken();
const page = await waitForNewPage(browser, knownPages); const bootstrapURL = buildHiddenWindowBootstrapURL(options.title, bootstrapToken);
const window = createHiddenWindow(options.title, options.session, bootstrapURL);
const page = await waitForNewPage(browser, knownPages, bootstrapToken);
page.setDefaultTimeout(defaultPageTimeoutMs); page.setDefaultTimeout(defaultPageTimeoutMs);
page.setDefaultNavigationTimeout(defaultPageTimeoutMs); page.setDefaultNavigationTimeout(defaultPageTimeoutMs);
@@ -181,11 +184,18 @@ function createLease(record: HiddenPlaywrightRecord): HiddenPlaywrightLease {
}; };
} }
function createHiddenWindow(title: string, session: Session): ElectronBrowserWindow { function createHiddenWindow(
title: string,
session: Session,
bootstrapURL: string,
): ElectronBrowserWindow {
const window = new BrowserWindow({ const window = new BrowserWindow({
show: false, show: false,
width: 1320, width: 1320,
height: 900, height: 900,
skipTaskbar: true,
focusable: false,
hiddenInMissionControl: true,
autoHideMenuBar: true, autoHideMenuBar: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea", backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
title, title,
@@ -199,10 +209,41 @@ function createHiddenWindow(title: string, session: Session): ElectronBrowserWin
}); });
window.webContents.setUserAgent(STANDARD_USER_AGENT); window.webContents.setUserAgent(STANDARD_USER_AGENT);
void window.loadURL("about:blank"); void window.loadURL(bootstrapURL);
return window; return window;
} }
function createHiddenBootstrapToken(): string {
if (typeof globalThis.crypto?.randomUUID === "function") {
return globalThis.crypto.randomUUID();
}
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}
function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;")
.replaceAll("'", "&#39;");
}
function buildHiddenWindowBootstrapURL(title: string, token: string): string {
const html = [
"<!doctype html>",
"<html lang=\"en\">",
"<head>",
" <meta charset=\"utf-8\">",
` <meta name=\"${hiddenBootstrapMetaName}\" content=\"${escapeHtml(token)}\">`,
` <title>${escapeHtml(title)} · hidden-playwright</title>`,
"</head>",
` <body data-${hiddenBootstrapMetaName}=\"${escapeHtml(token)}\"></body>`,
"</html>",
].join("");
return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
}
async function ensureRecordTarget(record: HiddenPlaywrightRecord, targetURL: string): Promise<void> { async function ensureRecordTarget(record: HiddenPlaywrightRecord, targetURL: string): Promise<void> {
record.targetURL = targetURL; record.targetURL = targetURL;
@@ -346,18 +387,21 @@ function isConnectOverCDPTimeout(error: unknown): boolean {
async function waitForNewPage( async function waitForNewPage(
browser: PlaywrightBrowser, browser: PlaywrightBrowser,
knownPages: Set<PlaywrightPage>, knownPages: Set<PlaywrightPage>,
bootstrapToken: string,
): Promise<PlaywrightPage> { ): Promise<PlaywrightPage> {
const context = defaultContext(browser); const context = defaultContext(browser);
const startedAt = Date.now(); const startedAt = Date.now();
while (Date.now() - startedAt < defaultPageTimeoutMs) { while (Date.now() - startedAt < defaultPageTimeoutMs) {
const nextPage = context.pages().find((page) => !knownPages.has(page)); const nextPages = context.pages().filter((page) => !knownPages.has(page));
if (nextPage) { for (const nextPage of nextPages) {
if (isDevtoolsPage(nextPage)) { if (isDevtoolsPage(nextPage)) {
knownPages.add(nextPage); knownPages.add(nextPage);
continue; continue;
} }
return nextPage; if (await matchesHiddenBootstrapPage(nextPage, bootstrapToken)) {
return nextPage;
}
} }
await wait(100); await wait(100);
} }
@@ -365,6 +409,34 @@ async function waitForNewPage(
throw new Error("playwright_hidden_page_unavailable"); throw new Error("playwright_hidden_page_unavailable");
} }
async function matchesHiddenBootstrapPage(
page: PlaywrightPage,
bootstrapToken: string,
): Promise<boolean> {
const url = safePageURL(page);
if (url.includes(bootstrapToken) || url.includes(encodeURIComponent(bootstrapToken))) {
return true;
}
try {
return await page.evaluate(
({ token, metaName }) => {
const metaToken =
document.querySelector(`meta[name="${metaName}"]`)?.getAttribute("content")
?? document.body?.getAttribute(`data-${metaName}`)
?? null;
return metaToken === token;
},
{
token: bootstrapToken,
metaName: hiddenBootstrapMetaName,
},
);
} catch {
return false;
}
}
function defaultContext(browser: PlaywrightBrowser) { function defaultContext(browser: PlaywrightBrowser) {
const context = browser.contexts()[0]; const context = browser.contexts()[0];
if (!context) { if (!context) {
@@ -18,8 +18,10 @@ import { getAIPlatformCatalogItem, isAIPlatformId } from "@geo/shared-types";
import { import {
doubaoAdapter, doubaoAdapter,
kimiAdapter,
qwenAdapter, qwenAdapter,
toutiaoAdapter, toutiaoAdapter,
yuanbaoAdapter,
zhihuAdapter, zhihuAdapter,
type AdapterExecutionResult, type AdapterExecutionResult,
type MonitorAdapter, type MonitorAdapter,
@@ -2640,6 +2642,12 @@ function selectPublishAdapter(platform: string): PublishAdapter | null {
} }
function selectMonitorAdapter(platform: string): MonitorAdapter | null { function selectMonitorAdapter(platform: string): MonitorAdapter | null {
if (platform === yuanbaoAdapter.provider) {
return yuanbaoAdapter;
}
if (platform === kimiAdapter.provider) {
return kimiAdapter;
}
if (platform === doubaoAdapter.provider) { if (platform === doubaoAdapter.provider) {
return doubaoAdapter; return doubaoAdapter;
} }
+2 -2
View File
@@ -24,8 +24,8 @@ export const aiPlatformCatalog: readonly AIPlatformCatalogItem[] = [
shortName: "K", shortName: "K",
accent: "#111827", accent: "#111827",
description: "Moonshot AI 账号绑定与静默监测", description: "Moonshot AI 账号绑定与静默监测",
loginUrl: "https://kimi.moonshot.cn/", loginUrl: "https://www.kimi.com/",
consoleUrl: "https://kimi.moonshot.cn/", consoleUrl: "https://www.kimi.com/",
}, },
{ {
id: "wenxin", id: "wenxin",
+1 -1
View File
@@ -693,7 +693,7 @@ func ensureDesktopClient(ctx context.Context, tx pgx.Tx, tenantID, workspaceID,
} }
func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) error { func ensureMonitoringQuota(ctx context.Context, tx pgx.Tx, state *seedState) error {
enabledPlatforms, _ := json.Marshal([]string{"deepseek", "qwen", "doubao"}) enabledPlatforms, _ := json.Marshal([]string{"yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"})
_, err := tx.Exec(ctx, ` _, err := tx.Exec(ctx, `
INSERT INTO tenant_monitoring_quotas ( INSERT INTO tenant_monitoring_quotas (
tenant_id, workspace_id, max_brands, collection_mode, question_selection_mode, collect_frequency, tenant_id, workspace_id, max_brands, collection_mode, question_selection_mode, collect_frequency,
@@ -4,7 +4,7 @@ CREATE TABLE tenant_monitoring_quotas (
collection_mode VARCHAR(20) NOT NULL DEFAULT 'desktop', collection_mode VARCHAR(20) NOT NULL DEFAULT 'desktop',
question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all', question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all',
collect_frequency VARCHAR(20) NOT NULL DEFAULT 'daily', collect_frequency VARCHAR(20) NOT NULL DEFAULT 'daily',
enabled_platforms JSONB NOT NULL DEFAULT '["deepseek","qwen","doubao"]'::jsonb, enabled_platforms JSONB NOT NULL DEFAULT '["yuanbao","kimi","wenxin","deepseek","doubao","qwen"]'::jsonb,
primary_client_id UUID, primary_client_id UUID,
task_daily_hard_cap INT NOT NULL DEFAULT 36, task_daily_hard_cap INT NOT NULL DEFAULT 36,
plan_tier VARCHAR(20) NOT NULL DEFAULT 'free', plan_tier VARCHAR(20) NOT NULL DEFAULT 'free',