mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-15 06:16:03 +00:00
feat(browser): 完成 browser MCP 第一阶段能力与配置打通
- 增加 browser_wait_for、browser_eval 及 click/query 扩展能力 - 打通 browser eval_mode 的配置、状态、启动链路与设置界面 - 补充浏览器相关测试隔离、文档和 CI parity 收尾调整
This commit is contained in:
1 parent
a0183073b3
commit
b90eae3892
25 files changed
+2370
-97
No files matched your search
+21
-2
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1164,6 +1164,7 @@ export async function execClaudeWithCLIProxy(
|
||||
devtoolsPort: browserAttachConfig.hasExplicitDevtoolsPort
|
||||
? String(browserAttachConfig.devtoolsPort)
|
||||
: undefined,
|
||||
evalMode: browserAttachConfig.evalMode,
|
||||
})),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user