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