Files
zn-ai/electron/service/browser-open-service.ts

160 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { chromium } from 'playwright';
import logManager from '@electron/service/logger';
import { launchLocalChrome } from '@electron/utils/chrome/launchLocalChrome';
const CDP_ENDPOINT = 'http://127.0.0.1:9222';
const NAVIGATION_TIMEOUT_MS = 15_000;
const OPEN_URL_PATTERNS = [
/^\s*(?:||)\s*[:]?\s*(https?:\/\/\S+)\s*$/iu,
/^\s*(?:open|visit)\s*[:]?\s*(https?:\/\/\S+)\s*$/iu,
] as const;
export interface BrowserOpenIntent {
url: string;
}
export interface BrowserOpenResult {
url: string;
pageUrl: string;
title?: string;
}
function stripTrailingPunctuation(value: string): string {
return value.trim().replace(/[)\]}>.,!?;:'"]+$/u, '');
}
function normalizeHttpUrl(value: string): string {
const raw = stripTrailingPunctuation(value);
const parsed = new URL(raw);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(`Unsupported URL protocol: ${parsed.protocol}`);
}
return parsed.toString();
}
function isBlankLikePage(url: string): boolean {
const normalized = String(url || '').trim().toLowerCase();
if (!normalized) return true;
return normalized === 'about:blank'
|| normalized.startsWith('about:blank#')
|| normalized.startsWith('about:blank?')
|| normalized === 'chrome://newtab/'
|| normalized.startsWith('chrome://newtab')
|| normalized.startsWith('chrome://new-tab-page');
}
function isSameTarget(currentUrl: string, targetUrl: string): boolean {
if (!currentUrl || !targetUrl) return false;
if (currentUrl.startsWith(targetUrl)) return true;
try {
const current = new URL(currentUrl);
const target = new URL(targetUrl);
return current.origin === target.origin && current.pathname.startsWith(target.pathname);
} catch {
return false;
}
}
function assertNotAborted(signal?: AbortSignal): void {
if (signal?.aborted) {
throw new Error('Browser open aborted');
}
}
function toBrowserConnectError(error: unknown): Error {
const message = error instanceof Error ? error.message : String(error);
if (
message.includes('connect ECONNREFUSED')
|| message.includes('retrieving websocket url from http://127.0.0.1:9222')
) {
return new Error('Chrome 已启动但远程调试端口 9222 不可用,请稍后重试或重启本地 Chrome');
}
return error instanceof Error ? error : new Error(message);
}
export function extractBrowserOpenIntent(text: string): BrowserOpenIntent | null {
for (const pattern of OPEN_URL_PATTERNS) {
const match = pattern.exec(text);
if (!match?.[1]) {
continue;
}
try {
return {
url: normalizeHttpUrl(match[1]),
};
} catch {
return null;
}
}
return null;
}
export async function openUrlInBrowser(
url: string,
options?: { signal?: AbortSignal },
): Promise<BrowserOpenResult> {
const normalizedUrl = normalizeHttpUrl(url);
assertNotAborted(options?.signal);
await launchLocalChrome();
assertNotAborted(options?.signal);
const browser = await chromium.connectOverCDP(CDP_ENDPOINT).catch((error) => {
throw toBrowserConnectError(error);
});
try {
const context = browser.contexts()[0];
if (!context) {
throw new Error('No browser context available');
}
let page = context.pages().find((candidate) => isSameTarget(candidate.url(), normalizedUrl));
if (!page) {
page = context.pages().find((candidate) => isBlankLikePage(candidate.url()));
}
if (!page) {
page = await context.newPage();
}
await page.bringToFront();
assertNotAborted(options?.signal);
if (!isSameTarget(page.url(), normalizedUrl)) {
await page.goto(normalizedUrl, {
waitUntil: 'domcontentloaded',
timeout: NAVIGATION_TIMEOUT_MS,
});
}
const pageUrl = page.url() || normalizedUrl;
const title = (await page.title().catch(() => '')).trim() || undefined;
logManager.info('Browser open request completed', {
requestedUrl: normalizedUrl,
pageUrl,
title,
});
return {
url: normalizedUrl,
pageUrl,
title,
};
} finally {
try {
if (typeof browser.disconnect === 'function') {
await browser.disconnect();
} else {
await browser.close();
}
} catch (error) {
logManager.warn('Failed to disconnect browser after open request:', error);
}
}
}