
























@@ -8,7 +8,7 @@ import { formatErrorMessage } from "../infra/errors.js";
88import { resolveUserPath } from "../utils.js";
99import { assertCdpEndpointAllowed } from "./cdp.helpers.js";
1010import { resolveOpenClawUserDataDir } from "./chrome.js";
11-import { parseHttpUrl, resolveProfile } from "./config.js";
11+import { parseHttpUrl, resolveBrowserConfig, resolveProfile } from "./config.js";
1212import {
1313BrowserConflictError,
1414BrowserProfileNotFoundError,
@@ -107,11 +107,10 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
107107throw new BrowserConflictError(`profile "${name}" already exists`);
108108}
109109110-const usedColors = getUsedColors(resolvedProfiles);
111-const profileColor =
112-params.color && HEX_COLOR_RE.test(params.color) ? params.color : allocateColor(usedColors);
110+const explicitProfileColor =
111+params.color && HEX_COLOR_RE.test(params.color) ? params.color : undefined;
113112114-let profileConfig: BrowserProfileConfig;
113+let parsedCdpUrl: string | undefined;
115114if (normalizedUserDataDir && driver !== "existing-session") {
116115throw new BrowserValidationError(
117116"driver=existing-session is required when userDataDir is provided",
@@ -136,48 +135,82 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
136135} catch (err) {
137136throw new BrowserValidationError(formatErrorMessage(err));
138137}
139-profileConfig = {
140-cdpUrl: parsed.normalized,
141- ...(driver ? { driver } : {}),
142-color: profileColor,
143-};
144-} else {
145-if (driver === "existing-session") {
146-// existing-session uses Chrome MCP auto-connect; no CDP port needed
147-profileConfig = {
148- driver,
149-attachOnly: true,
150- ...(normalizedUserDataDir ? { userDataDir: normalizedUserDataDir } : {}),
151-color: profileColor,
152-};
153-} else {
154-const usedPorts = getUsedPorts(resolvedProfiles);
155-const range = cdpPortRange(state.resolved);
156-const cdpPort = allocateCdpPort(usedPorts, range);
157-if (cdpPort === null) {
158-throw new BrowserResourceExhaustedError("no available CDP ports in range");
159-}
160-profileConfig = {
161- cdpPort,
162- ...(driver ? { driver } : {}),
163-color: profileColor,
164-};
165-}
138+parsedCdpUrl = parsed.normalized;
166139}
167140168-await mutateConfigFile({
141+const mutation = await mutateConfigFile<BrowserProfileConfig>({
169142afterWrite: { mode: "auto" },
170-mutate: (draft) => {
143+mutate: async (draft) => {
144+const latestResolved = resolveBrowserConfig({
145+ ...state.resolved,
146+ ...draft.browser,
147+profiles: draft.browser?.profiles ?? state.resolved.profiles,
148+});
149+const latestProfiles = draft.browser?.profiles ?? {};
150+if (name in latestProfiles || name in latestResolved.profiles) {
151+throw new BrowserConflictError(`profile "${name}" already exists`);
152+}
153+154+const profileColor =
155+explicitProfileColor ?? allocateColor(getUsedColors(latestResolved.profiles));
156+157+let nextProfileConfig: BrowserProfileConfig;
158+if (parsedCdpUrl) {
159+try {
160+await assertCdpEndpointAllowed(parsedCdpUrl, latestResolved.ssrfPolicy);
161+} catch (err) {
162+throw new BrowserValidationError(formatErrorMessage(err));
163+}
164+nextProfileConfig = {
165+cdpUrl: parsedCdpUrl,
166+ ...(driver ? { driver } : {}),
167+color: profileColor,
168+};
169+} else if (driver === "existing-session") {
170+// existing-session uses Chrome MCP auto-connect; no CDP port needed.
171+nextProfileConfig = {
172+ driver,
173+attachOnly: true,
174+ ...(normalizedUserDataDir ? { userDataDir: normalizedUserDataDir } : {}),
175+color: profileColor,
176+};
177+} else {
178+const usedPorts = getUsedPorts(latestResolved.profiles);
179+const rangeStart = draft.browser?.cdpPortRangeStart ?? state.resolved.cdpPortRangeStart;
180+const range = cdpPortRange({
181+controlPort: state.resolved.controlPort,
182+cdpPortRangeStart: rangeStart,
183+cdpPortRangeEnd:
184+draft.browser?.cdpPortRangeStart === undefined
185+ ? state.resolved.cdpPortRangeEnd
186+ : latestResolved.cdpPortRangeEnd,
187+});
188+const cdpPort = allocateCdpPort(usedPorts, range);
189+if (cdpPort === null) {
190+throw new BrowserResourceExhaustedError("no available CDP ports in range");
191+}
192+nextProfileConfig = {
193+ cdpPort,
194+ ...(driver ? { driver } : {}),
195+color: profileColor,
196+};
197+}
198+171199draft.browser = {
172200 ...draft.browser,
173201profiles: {
174202 ...draft.browser?.profiles,
175-[name]: profileConfig,
203+[name]: nextProfileConfig,
176204},
177205};
206+return nextProfileConfig;
178207},
179208});
180209210+const profileConfig = mutation.result;
211+if (!profileConfig) {
212+throw new BrowserProfileNotFoundError(`profile "${name}" not found after creation`);
213+}
181214state.resolved.profiles[name] = profileConfig;
182215const resolved = resolveProfile(state.resolved, name);
183216if (!resolved) {
@@ -241,10 +274,16 @@ export function createBrowserProfilesService(ctx: BrowserRouteContext) {
241274afterWrite: { mode: "auto" },
242275mutate: (draft) => {
243276const { [name]: _removed, ...remainingProfiles } = draft.browser?.profiles ?? {};
244-draft.browser = {
277+const nextBrowser = {
245278 ...draft.browser,
246279profiles: remainingProfiles,
247280};
281+if (nextBrowser.defaultProfile === name) {
282+delete nextBrowser.defaultProfile;
283+}
284+draft.browser = {
285+ ...nextBrowser,
286+};
248287},
249288});
250289此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。