feat(browser): 完成 browser MCP 第一阶段能力与配置打通

- 增加 browser_wait_for、browser_eval 及 click/query 扩展能力
- 打通 browser eval_mode 的配置、状态、启动链路与设置界面
- 补充浏览器相关测试隔离、文档和 CI parity 收尾调整
This commit is contained in:
walker1211 committed 2026-04-18 00:29:43 +08:00
1 parent a0183073b3
commit b90eae3892
25 files changed
+2370 -97

No files matched your search

+21 -2
View File
@@ -604,7 +604,6 @@ async function main(): Promise<void> {
} catch (error) {
console.error(fail((error as Error).message));
process.exit(1);
return;
}
// Detect Claude CLI (needed for claude target and all CLIProxy-derived flows)
@@ -1089,7 +1088,13 @@ async function main(): Promise<void> {
}
const inheritedClaudeConfigDir = continuityInheritance.claudeConfigDir;
syncWebSearchMcpToConfigDir(inheritedClaudeConfigDir);
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
if (!syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir) && inheritedClaudeConfigDir) {
console.error(
warn(
'Image Analysis MCP config could not be synced into the inherited Claude instance. This session will continue with the current fallback behavior.'
)
);
}
if (
browserAttachConfig?.enabled &&
inheritedClaudeConfigDir &&
@@ -1269,7 +1274,13 @@ async function main(): Promise<void> {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
CCS_IMAGE_ANALYSIS_BACKEND_ID: '',
CCS_IMAGE_ANALYSIS_MODEL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '',
};
delete imageAnalysisEnv.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY;
delete imageAnalysisEnv.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED;
} else if (imageAnalysisStatus.proxyReadiness === 'stopped') {
const ensureServiceResult = await ensureCliproxyService(
CLIPROXY_DEFAULT_PORT,
@@ -1285,7 +1296,13 @@ async function main(): Promise<void> {
...imageAnalysisEnv,
CCS_CURRENT_PROVIDER: '',
CCS_IMAGE_ANALYSIS_SKIP: '1',
CCS_IMAGE_ANALYSIS_BACKEND_ID: '',
CCS_IMAGE_ANALYSIS_MODEL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL: '',
CCS_IMAGE_ANALYSIS_RUNTIME_PATH: '',
};
delete imageAnalysisEnv.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY;
delete imageAnalysisEnv.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED;
}
}
}
@@ -1308,6 +1325,7 @@ async function main(): Promise<void> {
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
? String(browserAttachConfig.devtoolsPort)
: undefined,
evalMode: browserAttachConfig.evalMode,
})),
};
}
@@ -1487,6 +1505,7 @@ async function main(): Promise<void> {
? String(browserAttachConfig.devtoolsPort)
: undefined,
})),
CCS_BROWSER_EVAL_MODE: browserAttachConfig.evalMode,
};
Object.assign(envVars, browserRuntimeEnv);
}
+1
View File
@@ -1164,6 +1164,7 @@ export async function execClaudeWithCLIProxy(
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
? String(browserAttachConfig.devtoolsPort)
: undefined,
evalMode: browserAttachConfig.evalMode,
})),
}
: undefined;
+2
View File
@@ -50,6 +50,7 @@ function writeClaudeStatus(
writeLine(` Source: ${status.source}${status.overrideActive ? ' (env override active)' : ''}`);
writeLine(` User data dir: ${status.effectiveUserDataDir}`);
writeLine(` DevTools port: ${status.devtoolsPort}`);
writeLine(` browser_eval access: ${status.evalMode}`);
writeLine(` Managed MCP: ${status.managedMcpServerName}`);
writeLine(` Managed path: ${status.managedMcpServerPath}`);
if (status.runtimeEnv?.CCS_BROWSER_DEVTOOLS_HTTP_URL) {
@@ -69,6 +70,7 @@ function writeCodexStatus(status: BrowserStatusPayload['codex'], writeLine: Help
writeLine(` State: ${status.state}`);
writeLine(` Enabled: ${status.enabled ? 'yes' : 'no'}`);
writeLine(` Managed server: ${status.serverName}`);
writeLine(` browser_eval access: ${status.evalMode}`);
writeLine(` Supports overrides: ${status.supportsConfigOverrides ? 'yes' : 'no'}`);
writeLine(` Codex binary: ${status.binaryPath || 'not detected'}`);
if (status.version) {
+12
View File
@@ -36,6 +36,7 @@ import type {
OfficialChannelId,
DashboardAuthConfig,
BrowserConfig,
BrowserEvalMode,
ImageAnalysisConfig,
LoggingConfig,
CursorConfig,
@@ -70,15 +71,25 @@ function normalizeBrowserDevtoolsPort(value: number | undefined): number {
return port;
}
function normalizeBrowserEvalMode(value: string | undefined): BrowserEvalMode {
if (value === 'disabled' || value === 'readonly' || value === 'readwrite') {
return value;
}
return DEFAULT_BROWSER_CONFIG.claude.eval_mode;
}
function canonicalizeBrowserConfig(config?: BrowserConfig): BrowserConfig {
return {
claude: {
enabled: config?.claude?.enabled ?? DEFAULT_BROWSER_CONFIG.claude.enabled,
user_data_dir: config?.claude?.user_data_dir?.trim() || getRecommendedBrowserUserDataDir(),
devtools_port: normalizeBrowserDevtoolsPort(config?.claude?.devtools_port),
eval_mode: normalizeBrowserEvalMode(config?.claude?.eval_mode),
},
codex: {
enabled: config?.codex?.enabled ?? DEFAULT_BROWSER_CONFIG.codex.enabled,
eval_mode: normalizeBrowserEvalMode(config?.codex?.eval_mode),
},
};
}
@@ -955,6 +966,7 @@ function generateYamlWithComments(config: UnifiedConfig): string {
lines.push('#');
lines.push('# claude.user_data_dir should point at the Chrome user-data directory for the');
lines.push('# dedicated attach session. claude.devtools_port is the expected debugging port.');
lines.push('# eval_mode controls browser_eval access: disabled | readonly | readwrite.');
lines.push('# Configure via: Settings > Browser or `ccs browser ...`.');
lines.push('# ----------------------------------------------------------------------------');
lines.push(
+8
View File
@@ -816,6 +816,8 @@ export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = {
* Browser automation configuration.
* Controls Claude browser attach and Codex browser tooling.
*/
export type BrowserEvalMode = 'disabled' | 'readonly' | 'readwrite';
export interface BrowserClaudeConfig {
/** Enable Claude browser attach (default: false) */
enabled: boolean;
@@ -823,11 +825,15 @@ export interface BrowserClaudeConfig {
user_data_dir: string;
/** DevTools port used for attach mode (default: 9222) */
devtools_port: number;
/** Eval access mode for browser_eval (default: readonly) */
eval_mode: BrowserEvalMode;
}
export interface BrowserCodexConfig {
/** Enable Codex browser tooling injection (default: true) */
enabled: boolean;
/** Stored for dashboard/config parity; runtime enforcement is Claude-only in Phase 1 */
eval_mode: BrowserEvalMode;
}
export interface BrowserConfig {
@@ -840,9 +846,11 @@ export const DEFAULT_BROWSER_CONFIG: BrowserConfig = {
enabled: false,
user_data_dir: '',
devtools_port: 9222,
eval_mode: 'readonly',
},
codex: {
enabled: true,
eval_mode: 'readonly',
},
};
+4 -1
View File
@@ -1,5 +1,5 @@
import * as path from 'path';
import type { BrowserConfig } from '../../config/unified-config-types';
import type { BrowserConfig, BrowserEvalMode } from '../../config/unified-config-types';
import { getCcsDir } from '../config-manager';
import { expandPath } from '../helpers';
@@ -11,6 +11,7 @@ export interface EffectiveClaudeBrowserAttachConfig {
overrideActive: boolean;
userDataDir: string;
devtoolsPort: number;
evalMode: BrowserEvalMode;
hasExplicitDevtoolsPort: boolean;
}
@@ -64,6 +65,7 @@ export function getEffectiveClaudeBrowserAttachConfig(
overrideActive: true,
userDataDir: override.userDataDir,
devtoolsPort: override.devtoolsPort ?? configPort,
evalMode: config.claude.eval_mode,
hasExplicitDevtoolsPort: override.devtoolsPort !== undefined,
};
}
@@ -74,6 +76,7 @@ export function getEffectiveClaudeBrowserAttachConfig(
overrideActive: false,
userDataDir: configUserDataDir,
devtoolsPort: configPort,
evalMode: config.claude.eval_mode,
// Config-backed browser attach always keeps an explicit port so launches
// stay aligned with Settings > Browser, even when the effective value is
// the default 9222.
+7
View File
@@ -25,6 +25,7 @@ export interface ClaudeBrowserStatus {
effectiveUserDataDir: string;
recommendedUserDataDir: string;
devtoolsPort: number;
evalMode: 'disabled' | 'readonly' | 'readwrite';
managedMcpServerName: string;
managedMcpServerPath: string;
launchCommands: BrowserLaunchCommands;
@@ -38,6 +39,7 @@ export interface CodexBrowserStatus {
detail: string;
nextStep: string;
serverName: string;
evalMode: 'disabled' | 'readonly' | 'readwrite';
supportsConfigOverrides: boolean;
binaryPath: string | null;
version?: string;
@@ -68,6 +70,7 @@ async function buildClaudeBrowserStatus(
effectiveUserDataDir: effective.userDataDir,
recommendedUserDataDir: getRecommendedBrowserUserDataDir(),
devtoolsPort: effective.devtoolsPort,
evalMode: effective.evalMode,
managedMcpServerName: getBrowserMcpServerName(),
managedMcpServerPath: getBrowserMcpServerPath(),
launchCommands,
@@ -89,6 +92,7 @@ async function buildClaudeBrowserStatus(
const runtimeEnv = await resolveBrowserRuntimeEnv({
profileDir: effective.userDataDir,
devtoolsPort: effective.hasExplicitDevtoolsPort ? String(effective.devtoolsPort) : undefined,
evalMode: effective.evalMode,
});
return {
@@ -142,6 +146,7 @@ function buildCodexBrowserStatus(browserConfig = getBrowserConfig()): CodexBrows
nextStep:
'Enable Codex Browser Tools in Settings > Browser to restore the managed Codex browser path.',
serverName: 'ccs_browser',
evalMode: browserConfig.codex.eval_mode,
supportsConfigOverrides: false,
binaryPath: null,
};
@@ -159,6 +164,7 @@ function buildCodexBrowserStatus(browserConfig = getBrowserConfig()): CodexBrows
: 'No Codex binary was detected, so CCS cannot confirm managed browser override support.',
nextStep: 'Install or upgrade Codex, then rerun browser status/doctor.',
serverName: 'ccs_browser',
evalMode: browserConfig.codex.eval_mode,
supportsConfigOverrides,
binaryPath: binaryInfo?.path ?? null,
version: binaryInfo?.version,
@@ -172,6 +178,7 @@ function buildCodexBrowserStatus(browserConfig = getBrowserConfig()): CodexBrows
detail: 'CCS can inject the managed Playwright MCP overrides into Codex-target launches.',
nextStep: 'Use a Codex-target CCS launch to access browser tools.',
serverName: 'ccs_browser',
evalMode: browserConfig.codex.eval_mode,
supportsConfigOverrides,
binaryPath: binaryInfo.path,
version: binaryInfo.version,
+4
View File
@@ -1,11 +1,13 @@
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import type { BrowserEvalMode } from '../../config/unified-config-types';
import { expandPath } from '../helpers';
export interface BrowserReuseOptions {
profileDir?: string;
devtoolsPort?: string;
evalMode?: BrowserEvalMode;
}
export interface BrowserRuntimeEnv {
@@ -14,6 +16,7 @@ export interface BrowserRuntimeEnv {
CCS_BROWSER_DEVTOOLS_PORT: string;
CCS_BROWSER_DEVTOOLS_HTTP_URL: string;
CCS_BROWSER_DEVTOOLS_WS_URL: string;
CCS_BROWSER_EVAL_MODE?: string;
}
const DEVTOOLS_HOST = '127.0.0.1';
@@ -100,6 +103,7 @@ export async function resolveBrowserRuntimeEnv(
CCS_BROWSER_DEVTOOLS_PORT: port,
CCS_BROWSER_DEVTOOLS_HTTP_URL: httpUrl,
CCS_BROWSER_DEVTOOLS_WS_URL: websocketUrl,
...(options.evalMode ? { CCS_BROWSER_EVAL_MODE: options.evalMode } : {}),
};
}
+22
View File
@@ -12,9 +12,11 @@ interface BrowserRouteBody {
enabled?: boolean;
userDataDir?: string;
devtoolsPort?: number;
evalMode?: 'disabled' | 'readonly' | 'readwrite';
};
codex?: {
enabled?: boolean;
evalMode?: 'disabled' | 'readonly' | 'readwrite';
};
}
@@ -22,6 +24,10 @@ function isValidDevtoolsPort(value: number): boolean {
return Number.isInteger(value) && value >= 1 && value <= 65535;
}
function isValidEvalMode(value: string): value is 'disabled' | 'readonly' | 'readwrite' {
return value === 'disabled' || value === 'readonly' || value === 'readwrite';
}
router.use((req: Request, res: Response, next) => {
if (requireLocalAccessWhenAuthDisabled(req, res, BROWSER_LOCAL_ACCESS_ERROR)) {
next();
@@ -86,10 +92,22 @@ router.put('/', async (req: Request, res: Response): Promise<void> => {
});
return;
}
if (claude?.evalMode !== undefined && !isValidEvalMode(claude.evalMode)) {
res.status(400).json({
error: 'Invalid value for claude.evalMode. Must be one of: disabled, readonly, readwrite.',
});
return;
}
if (codex?.enabled !== undefined && typeof codex.enabled !== 'boolean') {
res.status(400).json({ error: 'Invalid value for codex.enabled. Must be a boolean.' });
return;
}
if (codex?.evalMode !== undefined && !isValidEvalMode(codex.evalMode)) {
res.status(400).json({
error: 'Invalid value for codex.evalMode. Must be one of: disabled, readonly, readwrite.',
});
return;
}
try {
const current = getBrowserConfig();
@@ -101,9 +119,11 @@ router.put('/', async (req: Request, res: Response): Promise<void> => {
enabled: claude?.enabled ?? current.claude.enabled,
user_data_dir: nextClaudeUserDataDir,
devtools_port: claude?.devtoolsPort ?? current.claude.devtools_port,
eval_mode: claude?.evalMode ?? current.claude.eval_mode,
},
codex: {
enabled: codex?.enabled ?? current.codex.enabled,
eval_mode: codex?.evalMode ?? current.codex.eval_mode,
},
};
});
@@ -128,9 +148,11 @@ function toBrowserRouteConfig(config: ReturnType<typeof getBrowserConfig>) {
enabled: config.claude.enabled,
userDataDir: config.claude.user_data_dir,
devtoolsPort: config.claude.devtools_port,
evalMode: config.claude.eval_mode,
},
codex: {
enabled: config.codex.enabled,
evalMode: config.codex.eval_mode,
},
};
}