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:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll("\"", """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user