




















@@ -31,6 +31,18 @@ type ChromeMcpSession = {
3131ready: Promise<void>;
3232};
333334+type ChromeMcpCallOptions = {
35+ephemeral?: boolean;
36+timeoutMs?: number;
37+signal?: AbortSignal;
38+};
39+40+type ChromeMcpSessionLease = {
41+session: ChromeMcpSession;
42+cacheKey: string;
43+temporary: boolean;
44+};
45+3446type ChromeMcpSessionFactory = (
3547profileName: string,
3648userDataDir?: string,
@@ -332,7 +344,42 @@ async function createRealSession(
332344};
333345}
334346335-async function getSession(profileName: string, userDataDir?: string): Promise<ChromeMcpSession> {
347+async function waitForChromeMcpReady(
348+session: ChromeMcpSession,
349+profileName: string,
350+timeoutMs?: number,
351+): Promise<void> {
352+if (!timeoutMs || timeoutMs <= 0) {
353+await session.ready;
354+return;
355+}
356+357+let timer: ReturnType<typeof setTimeout> | undefined;
358+try {
359+await Promise.race([
360+session.ready,
361+new Promise<never>((_, reject) => {
362+timer = setTimeout(() => {
363+reject(
364+new BrowserProfileUnavailableError(
365+`Chrome MCP existing-session attach for profile "${profileName}" timed out after ${timeoutMs}ms.`,
366+),
367+);
368+}, timeoutMs);
369+}),
370+]);
371+} finally {
372+if (timer) {
373+clearTimeout(timer);
374+}
375+}
376+}
377+378+async function getSession(
379+profileName: string,
380+userDataDir?: string,
381+timeoutMs?: number,
382+): Promise<ChromeMcpSession> {
336383const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
337384await closeChromeMcpSessionsForProfile(profileName, cacheKey);
338385@@ -364,7 +411,7 @@ async function getSession(profileName: string, userDataDir?: string): Promise<Ch
364411}
365412}
366413try {
367-await session.ready;
414+await waitForChromeMcpReady(session, profileName, timeoutMs);
368415return session;
369416} catch (err) {
370417const current = sessions.get(cacheKey);
@@ -375,23 +422,110 @@ async function getSession(profileName: string, userDataDir?: string): Promise<Ch
375422}
376423}
377424425+async function getExistingSession(
426+cacheKey: string,
427+profileName: string,
428+timeoutMs?: number,
429+): Promise<ChromeMcpSession | null> {
430+let session = sessions.get(cacheKey);
431+if (session && session.transport.pid === null) {
432+sessions.delete(cacheKey);
433+session = undefined;
434+}
435+if (session) {
436+try {
437+await waitForChromeMcpReady(session, profileName, timeoutMs);
438+return session;
439+} catch (err) {
440+const current = sessions.get(cacheKey);
441+if (current?.transport === session.transport) {
442+sessions.delete(cacheKey);
443+}
444+throw err;
445+}
446+}
447+448+const pending = pendingSessions.get(cacheKey);
449+if (!pending) {
450+return null;
451+}
452+453+session = await pending;
454+try {
455+await waitForChromeMcpReady(session, profileName, timeoutMs);
456+return session;
457+} catch (err) {
458+const current = sessions.get(cacheKey);
459+if (current?.transport === session.transport) {
460+sessions.delete(cacheKey);
461+}
462+throw err;
463+}
464+}
465+466+async function createEphemeralSession(
467+profileName: string,
468+userDataDir?: string,
469+timeoutMs?: number,
470+): Promise<ChromeMcpSession> {
471+const session = await (sessionFactory ?? createRealSession)(profileName, userDataDir);
472+try {
473+await waitForChromeMcpReady(session, profileName, timeoutMs);
474+return session;
475+} catch (err) {
476+await session.client.close().catch(() => {});
477+throw err;
478+}
479+}
480+481+async function leaseSession(
482+profileName: string,
483+userDataDir?: string,
484+options: ChromeMcpCallOptions = {},
485+): Promise<ChromeMcpSessionLease> {
486+const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
487+if (!options.ephemeral) {
488+return {
489+session: await getSession(profileName, userDataDir, options.timeoutMs),
490+ cacheKey,
491+temporary: false,
492+};
493+}
494+495+// Status probes should avoid seeding the shared attach session cache, but they can safely
496+// reuse a real cached session if one already exists.
497+const existingSession = await getExistingSession(cacheKey, profileName, options.timeoutMs);
498+if (existingSession) {
499+return {
500+session: existingSession,
501+ cacheKey,
502+temporary: false,
503+};
504+}
505+506+return {
507+session: await createEphemeralSession(profileName, userDataDir, options.timeoutMs),
508+ cacheKey,
509+temporary: true,
510+};
511+}
512+378513async function callTool(
379514profileName: string,
380515userDataDir: string | undefined,
381516name: string,
382517args: Record<string, unknown> = {},
383-opts?: { timeoutMs?: number; signal?: AbortSignal },
518+options: ChromeMcpCallOptions = {},
384519): Promise<ChromeMcpToolResult> {
385-const cacheKey = buildChromeMcpSessionCacheKey(profileName, userDataDir);
386-const timeoutMs = opts?.timeoutMs;
387-const signal = opts?.signal;
520+const timeoutMs = options.timeoutMs;
521+const signal = options.signal;
388522if (signal?.aborted) {
389523throw signal.reason ?? new Error("aborted");
390524}
391525392526for (let attempt = 0; attempt < 2; attempt += 1) {
393-const session = await getSession(profileName, userDataDir);
394-const rawCall = session.client.callTool({
527+const lease = await leaseSession(profileName, userDataDir, options);
528+const rawCall = lease.session.client.callTool({
395529 name,
396530arguments: args,
397531}) as Promise<ChromeMcpToolResult>;
@@ -430,10 +564,12 @@ async function callTool(
430564void rawCall.catch(() => {});
431565// Transport/connection error, timeout, or abort: tear down session so it reconnects.
432566// Transport-identity check prevents clobbering a replacement session created concurrently.
433-const cur = sessions.get(cacheKey);
434-if (cur?.transport === session.transport) {
435-sessions.delete(cacheKey);
436-await session.client.close().catch(() => {});
567+if (!lease.temporary) {
568+const cur = sessions.get(lease.cacheKey);
569+if (cur?.transport === lease.session.transport) {
570+sessions.delete(lease.cacheKey);
571+await lease.session.client.close().catch(() => {});
572+}
437573}
438574throw err;
439575} finally {
@@ -443,17 +579,22 @@ async function callTool(
443579if (signal && abortListener) {
444580signal.removeEventListener("abort", abortListener);
445581}
582+if (lease.temporary) {
583+await lease.session.client.close().catch(() => {});
584+}
446585}
447586// Tool-level errors (element not found, script error, etc.) don't indicate a
448587// broken connection. A stale selected-page error does poison the Chrome MCP
449588// session, so reconnect and retry that one once.
450589if (result.isError) {
451590const message = extractToolErrorMessage(result, name);
452591if (shouldReconnectForToolError(name, message)) {
453-const cur = sessions.get(cacheKey);
454-if (cur?.transport === session.transport) {
455-sessions.delete(cacheKey);
456-await session.client.close().catch(() => {});
592+if (!lease.temporary) {
593+const cur = sessions.get(lease.cacheKey);
594+if (cur?.transport === lease.session.transport) {
595+sessions.delete(lease.cacheKey);
596+await lease.session.client.close().catch(() => {});
597+}
457598}
458599if (attempt === 0) {
459600continue;
@@ -492,8 +633,12 @@ async function findPageById(
492633export async function ensureChromeMcpAvailable(
493634profileName: string,
494635userDataDir?: string,
636+options: ChromeMcpCallOptions = {},
495637): Promise<void> {
496-await getSession(profileName, userDataDir);
638+const lease = await leaseSession(profileName, userDataDir, options);
639+if (lease.temporary) {
640+await lease.session.client.close().catch(() => {});
641+}
497642}
498643499644export function getChromeMcpPid(profileName: string): number | null {
@@ -519,16 +664,18 @@ export async function stopAllChromeMcpSessions(): Promise<void> {
519664export async function listChromeMcpPages(
520665profileName: string,
521666userDataDir?: string,
667+options: ChromeMcpCallOptions = {},
522668): Promise<ChromeMcpStructuredPage[]> {
523-const result = await callTool(profileName, userDataDir, "list_pages");
669+const result = await callTool(profileName, userDataDir, "list_pages", {}, options);
524670return extractStructuredPages(result);
525671}
526672527673export async function listChromeMcpTabs(
528674profileName: string,
529675userDataDir?: string,
676+options: ChromeMcpCallOptions = {},
530677): Promise<BrowserTab[]> {
531-return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir));
678+return toBrowserTabs(await listChromeMcpPages(profileName, userDataDir, options));
532679}
533680534681export async function openChromeMcpTab(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。