feat: implement browser open functionality and related tests

This commit is contained in:
duanshuwen
2026-04-23 19:27:21 +08:00
parent c9617a3777
commit 979fb0a0f6
7 changed files with 567 additions and 18 deletions

View File

@@ -0,0 +1,159 @@
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);
}
}
}