|
1 | 1 | import type { OpenClawConfig } from "../config/config.js"; |
2 | | -import { |
3 | | - parseBrowserMajorVersion, |
4 | | - readBrowserVersion, |
5 | | - resolveGoogleChromeExecutableForPlatform, |
6 | | -} from "../plugin-sdk/browser-host-inspection.js"; |
7 | | -import { asNullableRecord } from "../shared/record-coerce.js"; |
8 | | -import { normalizeOptionalString } from "../shared/string-coerce.js"; |
| 2 | +import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-loader.js"; |
9 | 3 | import { note } from "../terminal/note.js"; |
10 | 4 |
|
11 | | -const CHROME_MCP_MIN_MAJOR = 144; |
12 | | -const REMOTE_DEBUGGING_PAGES = [ |
13 | | - "chrome://inspect/#remote-debugging", |
14 | | - "brave://inspect/#remote-debugging", |
15 | | - "edge://inspect/#remote-debugging", |
16 | | -].join(", "); |
17 | | - |
18 | | -type ExistingSessionProfile = { |
19 | | - name: string; |
20 | | - userDataDir?: string; |
| 5 | +type BrowserDoctorDeps = { |
| 6 | + platform?: NodeJS.Platform; |
| 7 | + noteFn?: typeof note; |
| 8 | + resolveChromeExecutable?: (platform: NodeJS.Platform) => { path: string } | null; |
| 9 | + readVersion?: (executablePath: string) => string | null; |
21 | 10 | }; |
22 | 11 |
|
23 | | -function collectChromeMcpProfiles(cfg: OpenClawConfig): ExistingSessionProfile[] { |
24 | | - const browser = asNullableRecord(cfg.browser); |
25 | | - if (!browser) { |
26 | | - return []; |
27 | | - } |
28 | | - |
29 | | - const profiles = new Map<string, ExistingSessionProfile>(); |
30 | | - const defaultProfile = normalizeOptionalString(browser.defaultProfile) ?? ""; |
31 | | - if (defaultProfile === "user") { |
32 | | - profiles.set("user", { name: "user" }); |
33 | | - } |
34 | | - |
35 | | - const configuredProfiles = asNullableRecord(browser.profiles); |
36 | | - if (!configuredProfiles) { |
37 | | - return [...profiles.values()].toSorted((a, b) => a.name.localeCompare(b.name)); |
38 | | - } |
39 | | - |
40 | | - for (const [profileName, rawProfile] of Object.entries(configuredProfiles)) { |
41 | | - const profile = asNullableRecord(rawProfile); |
42 | | - const driver = normalizeOptionalString(profile?.driver) ?? ""; |
43 | | - if (driver === "existing-session") { |
44 | | - profiles.set(profileName, { |
45 | | - name: profileName, |
46 | | - userDataDir: normalizeOptionalString(profile?.userDataDir), |
47 | | - }); |
48 | | - } |
49 | | - } |
| 12 | +type BrowserDoctorSurface = { |
| 13 | + noteChromeMcpBrowserReadiness: (cfg: OpenClawConfig, deps?: BrowserDoctorDeps) => Promise<void>; |
| 14 | +}; |
50 | 15 |
|
51 | | - return [...profiles.values()].toSorted((a, b) => a.name.localeCompare(b.name)); |
| 16 | +function loadBrowserDoctorSurface(): BrowserDoctorSurface { |
| 17 | + return loadBundledPluginPublicSurfaceModuleSync<BrowserDoctorSurface>({ |
| 18 | + dirName: "browser", |
| 19 | + artifactBasename: "browser-doctor.js", |
| 20 | + }); |
52 | 21 | } |
53 | 22 |
|
54 | | -export async function noteChromeMcpBrowserReadiness( |
55 | | - cfg: OpenClawConfig, |
56 | | - deps?: { |
57 | | - platform?: NodeJS.Platform; |
58 | | - noteFn?: typeof note; |
59 | | - resolveChromeExecutable?: (platform: NodeJS.Platform) => { path: string } | null; |
60 | | - readVersion?: (executablePath: string) => string | null; |
61 | | - }, |
62 | | -) { |
63 | | - const profiles = collectChromeMcpProfiles(cfg); |
64 | | - if (profiles.length === 0) { |
65 | | - return; |
| 23 | +export async function noteChromeMcpBrowserReadiness(cfg: OpenClawConfig, deps?: BrowserDoctorDeps) { |
| 24 | + try { |
| 25 | + await loadBrowserDoctorSurface().noteChromeMcpBrowserReadiness(cfg, deps); |
| 26 | + } catch (error) { |
| 27 | + const noteFn = deps?.noteFn ?? note; |
| 28 | + const message = error instanceof Error ? error.message : String(error); |
| 29 | + noteFn(`- Browser health check is unavailable: ${message}`, "Browser"); |
66 | 30 | } |
67 | | - |
68 | | - const noteFn = deps?.noteFn ?? note; |
69 | | - const platform = deps?.platform ?? process.platform; |
70 | | - const resolveChromeExecutable = |
71 | | - deps?.resolveChromeExecutable ?? resolveGoogleChromeExecutableForPlatform; |
72 | | - const readVersion = deps?.readVersion ?? readBrowserVersion; |
73 | | - const explicitProfiles = profiles.filter((profile) => profile.userDataDir); |
74 | | - const autoConnectProfiles = profiles.filter((profile) => !profile.userDataDir); |
75 | | - const profileLabel = profiles.map((profile) => profile.name).join(", "); |
76 | | - |
77 | | - if (autoConnectProfiles.length === 0) { |
78 | | - noteFn( |
79 | | - [ |
80 | | - `- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`, |
81 | | - "- These profiles use an explicit Chromium user data directory instead of Chrome's default auto-connect path.", |
82 | | - `- Verify the matching Chromium-based browser is version ${CHROME_MCP_MIN_MAJOR}+ on the same host as the Gateway or node.`, |
83 | | - `- Enable remote debugging in that browser's inspect page (${REMOTE_DEBUGGING_PAGES}).`, |
84 | | - "- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.", |
85 | | - ].join("\n"), |
86 | | - "Browser", |
87 | | - ); |
88 | | - return; |
89 | | - } |
90 | | - |
91 | | - const chrome = resolveChromeExecutable(platform); |
92 | | - const autoProfileLabel = autoConnectProfiles.map((profile) => profile.name).join(", "); |
93 | | - |
94 | | - if (!chrome) { |
95 | | - const lines = [ |
96 | | - `- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`, |
97 | | - `- Google Chrome was not found on this host for auto-connect profile(s): ${autoProfileLabel}. OpenClaw does not bundle Chrome.`, |
98 | | - `- Install Google Chrome ${CHROME_MCP_MIN_MAJOR}+ on the same host as the Gateway or node, or set browser.profiles.<name>.userDataDir for a different Chromium-based browser.`, |
99 | | - `- Enable remote debugging in the browser inspect page (${REMOTE_DEBUGGING_PAGES}).`, |
100 | | - "- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.", |
101 | | - "- Docker, headless, and sandbox browser flows stay on raw CDP; this check only applies to host-local Chrome MCP attach.", |
102 | | - ]; |
103 | | - if (explicitProfiles.length > 0) { |
104 | | - lines.push( |
105 | | - `- Profiles with explicit userDataDir skip Chrome auto-detection: ${explicitProfiles |
106 | | - .map((profile) => profile.name) |
107 | | - .join(", ")}.`, |
108 | | - ); |
109 | | - } |
110 | | - noteFn(lines.join("\n"), "Browser"); |
111 | | - return; |
112 | | - } |
113 | | - |
114 | | - const versionRaw = readVersion(chrome.path); |
115 | | - const major = parseBrowserMajorVersion(versionRaw); |
116 | | - const lines = [ |
117 | | - `- Chrome MCP existing-session is configured for profile(s): ${profileLabel}.`, |
118 | | - `- Chrome path: ${chrome.path}`, |
119 | | - ]; |
120 | | - |
121 | | - if (!versionRaw || major === null) { |
122 | | - lines.push( |
123 | | - `- Could not determine the installed Chrome version. Chrome MCP requires Google Chrome ${CHROME_MCP_MIN_MAJOR}+ on this host.`, |
124 | | - ); |
125 | | - } else if (major < CHROME_MCP_MIN_MAJOR) { |
126 | | - lines.push( |
127 | | - `- Detected Chrome ${versionRaw}, which is too old for Chrome MCP existing-session attach. Upgrade to Chrome ${CHROME_MCP_MIN_MAJOR}+.`, |
128 | | - ); |
129 | | - } else { |
130 | | - lines.push(`- Detected Chrome ${versionRaw}.`); |
131 | | - } |
132 | | - |
133 | | - lines.push(`- Enable remote debugging in the browser inspect page (${REMOTE_DEBUGGING_PAGES}).`); |
134 | | - lines.push( |
135 | | - "- Keep the browser running and accept the attach consent prompt the first time OpenClaw connects.", |
136 | | - ); |
137 | | - if (explicitProfiles.length > 0) { |
138 | | - lines.push( |
139 | | - `- Profiles with explicit userDataDir still need manual validation of the matching Chromium-based browser: ${explicitProfiles |
140 | | - .map((profile) => profile.name) |
141 | | - .join(", ")}.`, |
142 | | - ); |
143 | | - } |
144 | | - |
145 | | - noteFn(lines.join("\n"), "Browser"); |
146 | 31 | } |
0 commit comments