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(
platformId: 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(
platformId: string,
label: string,
@@ -1224,6 +1302,10 @@ async function detectAIPlatformAccount(
return await detectQwenPlatform(platformId, label, context);
}
if (platformId === "kimi") {
return await detectKimiPlatform(platformId, label, context);
}
return await detectGenericAIPlatform(platformId, label, context);
}
@@ -2446,14 +2528,18 @@ function createBoundWindow(
sessionHandle: Session,
options: { show?: boolean } = {},
): BrowserWindow {
const show = options.show ?? true;
const window = new BrowserWindow({
show: options.show ?? true,
show,
width: 1320,
height: 900,
minWidth: 1100,
minHeight: 760,
title,
autoHideMenuBar: true,
skipTaskbar: !show,
focusable: show,
hiddenInMissionControl: !show,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
webPreferences: {
session: sessionHandle,
@@ -1,5 +1,7 @@
export * from "./base";
export * from "./doubao";
export * from "./kimi";
export * from "./qwen";
export * from "./toutiao";
export * from "./yuanbao";
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);
return window;
}
+79 -7
View File
@@ -8,6 +8,7 @@ const hiddenPlaywrightIdleTTLms = 5 * 60_000;
const hiddenPlaywrightReaperIntervalMs = 60_000;
const defaultConnectTimeoutMs = 10_000;
const defaultPageTimeoutMs = 45_000;
const hiddenBootstrapMetaName = "geo-hidden-playwright-token";
const cdpPort = Number.parseInt(process.env.GEO_ELECTRON_CDP_PORT ?? "9339", 10);
const cdpEndpointURL = `http://127.0.0.1:${cdpPort}`;
const cdpTargetsListURL = `${cdpEndpointURL}/json/list`;
@@ -79,8 +80,10 @@ export async function retainHiddenPlaywrightPage(options: {
const browser = await connectBrowserOverCDP();
const knownPages = new Set(defaultContext(browser).pages());
const window = createHiddenWindow(options.title, options.session);
const page = await waitForNewPage(browser, knownPages);
const bootstrapToken = createHiddenBootstrapToken();
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.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({
show: false,
width: 1320,
height: 900,
skipTaskbar: true,
focusable: false,
hiddenInMissionControl: true,
autoHideMenuBar: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? "#15191a" : "#f4f1ea",
title,
@@ -199,10 +209,41 @@ function createHiddenWindow(title: string, session: Session): ElectronBrowserWin
});
window.webContents.setUserAgent(STANDARD_USER_AGENT);
void window.loadURL("about:blank");
void window.loadURL(bootstrapURL);
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> {
record.targetURL = targetURL;
@@ -346,18 +387,21 @@ function isConnectOverCDPTimeout(error: unknown): boolean {
async function waitForNewPage(
browser: PlaywrightBrowser,
knownPages: Set<PlaywrightPage>,
bootstrapToken: string,
): Promise<PlaywrightPage> {
const context = defaultContext(browser);
const startedAt = Date.now();
while (Date.now() - startedAt < defaultPageTimeoutMs) {
const nextPage = context.pages().find((page) => !knownPages.has(page));
if (nextPage) {
const nextPages = context.pages().filter((page) => !knownPages.has(page));
for (const nextPage of nextPages) {
if (isDevtoolsPage(nextPage)) {
knownPages.add(nextPage);
continue;
}
return nextPage;
if (await matchesHiddenBootstrapPage(nextPage, bootstrapToken)) {
return nextPage;
}
}
await wait(100);
}
@@ -365,6 +409,34 @@ async function waitForNewPage(
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) {
const context = browser.contexts()[0];
if (!context) {
@@ -18,8 +18,10 @@ import { getAIPlatformCatalogItem, isAIPlatformId } from "@geo/shared-types";
import {
doubaoAdapter,
kimiAdapter,
qwenAdapter,
toutiaoAdapter,
yuanbaoAdapter,
zhihuAdapter,
type AdapterExecutionResult,
type MonitorAdapter,
@@ -2640,6 +2642,12 @@ function selectPublishAdapter(platform: string): PublishAdapter | null {
}
function selectMonitorAdapter(platform: string): MonitorAdapter | null {
if (platform === yuanbaoAdapter.provider) {
return yuanbaoAdapter;
}
if (platform === kimiAdapter.provider) {
return kimiAdapter;
}
if (platform === doubaoAdapter.provider) {
return doubaoAdapter;
}
+2 -2
View File
@@ -24,8 +24,8 @@ export const aiPlatformCatalog: readonly AIPlatformCatalogItem[] = [
shortName: "K",
accent: "#111827",
description: "Moonshot AI 账号绑定与静默监测",
loginUrl: "https://kimi.moonshot.cn/",
consoleUrl: "https://kimi.moonshot.cn/",
loginUrl: "https://www.kimi.com/",
consoleUrl: "https://www.kimi.com/",
},
{
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 {
enabledPlatforms, _ := json.Marshal([]string{"deepseek", "qwen", "doubao"})
enabledPlatforms, _ := json.Marshal([]string{"yuanbao", "kimi", "wenxin", "deepseek", "doubao", "qwen"})
_, err := tx.Exec(ctx, `
INSERT INTO tenant_monitoring_quotas (
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',
question_selection_mode VARCHAR(20) NOT NULL DEFAULT 'best_effort_all',
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,
task_daily_hard_cap INT NOT NULL DEFAULT 36,
plan_tier VARCHAR(20) NOT NULL DEFAULT 'free',