From df0a28fb90abc88851a62943b3d730eec27c675f Mon Sep 17 00:00:00 2001 From: ymkiux <3255284101@qq.com> Date: Wed, 1 Apr 2026 18:57:50 +0800 Subject: [PATCH 1/5] fix: remove legacy proxy surfaces --- README.md | 8 - cli.js | 438 ++++++++++++++--------------------- tests/e2e/test-auth-proxy.js | 75 +++--- tests/e2e/test-setup.js | 7 +- web-ui/app.js | 286 +---------------------- web-ui/index.html | 110 +-------- 6 files changed, 238 insertions(+), 686 deletions(-) diff --git a/README.md b/README.md index 8d92837..cb802d0 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,6 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理: **工程能力** - MCP stdio 能力(tools/resources/prompts) -- 内建代理配置与状态控制(`proxy`) -- 认证档案管理(`auth`) - Zip 压缩/解压(优先系统工具,失败回退 JS 库) ## 架构总览 @@ -74,14 +72,12 @@ flowchart TB CLI["CLI"] WEB["Web UI"] MCP["MCP Client"] - OAI["Codex / OpenAI Client"] end subgraph Runtime["Codex Mate Runtime"] ENTRY["cli.js Entry"] API["Local HTTP API"] MCPS["MCP stdio Server"] - PROXY["Built-in Proxy"] SERVICES["Config / Sessions / Skills Market / Workflow"] CORE["File IO / Network / Diff / Session Utils"] end @@ -97,12 +93,10 @@ flowchart TB CLI --> ENTRY WEB -->|GET / + POST /api| API MCP -->|stdio JSON-RPC| MCPS - OAI -->|HTTP /v1| PROXY ENTRY --> SERVICES API --> SERVICES MCPS --> SERVICES - PROXY --> CORE SERVICES --> CORE @@ -156,8 +150,6 @@ npm start run --no-browser | `codexmate add [API_KEY]` | 添加提供商 | | `codexmate delete ` | 删除提供商 | | `codexmate claude [model]` | 写入 Claude Code 配置 | -| `codexmate auth ` | 认证档案管理 | -| `codexmate proxy ` | 内建代理管理 | | `codexmate workflow ` | MCP 工作流管理 | | `codexmate codex [args...] [--follow-up <文本> 可重复]` | Codex CLI 透传入口(默认补 `--yolo`,可追加 queued follow-up) | | `codexmate qwen [args...]` | Qwen CLI 透传入口 | diff --git a/cli.js b/cli.js index f8328b5..663cc17 100644 --- a/cli.js +++ b/cli.js @@ -926,12 +926,18 @@ function normalizeAuthRegistry(raw) { }; } +function ensureAuthProfileStoragePrepared() { + ensureDir(AUTH_PROFILES_DIR); +} + function readAuthRegistry() { + ensureAuthProfileStoragePrepared(); const parsed = readJsonFile(AUTH_REGISTRY_FILE, null); return normalizeAuthRegistry(parsed); } function writeAuthRegistry(registry) { + ensureAuthProfileStoragePrepared(); writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry)); } @@ -990,6 +996,7 @@ function listAuthProfilesInfo() { } function upsertAuthProfile(payload, options = {}) { + ensureAuthProfileStoragePrepared(); const safePayload = parseAuthProfileJson(JSON.stringify(payload || {})); const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : ''; const preferredName = normalizeAuthProfileName(options.name || ''); @@ -1079,6 +1086,7 @@ function importAuthProfileFromUpload(payload = {}) { } function switchAuthProfile(name, options = {}) { + ensureAuthProfileStoragePrepared(); const profileName = normalizeAuthProfileName(name); if (!profileName) { throw new Error('认证名称不能为空'); @@ -1124,6 +1132,7 @@ function switchAuthProfile(name, options = {}) { } function deleteAuthProfile(name) { + ensureAuthProfileStoragePrepared(); const profileName = normalizeAuthProfileName(name); if (!profileName) { return { error: '认证名称不能为空' }; @@ -1172,6 +1181,7 @@ function deleteAuthProfile(name) { } function resolveAuthTokenFromCurrentProfile() { + ensureAuthProfileStoragePrepared(); const registry = readAuthRegistry(); if (!registry.current) return ''; const profile = registry.items.find((item) => item.name === registry.current); @@ -3175,11 +3185,40 @@ function buildVirtualDefaultConfig() { return toml.parse(EMPTY_CONFIG_FALLBACK_TEMPLATE); } +function sanitizeRemovedBuiltinProxyProvider(config) { + const safeConfig = isPlainObject(config) ? config : {}; + const providers = isPlainObject(safeConfig.model_providers) ? safeConfig.model_providers : null; + const currentProvider = typeof safeConfig.model_provider === 'string' ? safeConfig.model_provider.trim() : ''; + const hasRemovedBuiltin = !!(providers && providers[BUILTIN_PROXY_PROVIDER_NAME]); + const currentIsRemovedBuiltin = currentProvider === BUILTIN_PROXY_PROVIDER_NAME; + + if (!hasRemovedBuiltin && !currentIsRemovedBuiltin) { + return safeConfig; + } + + const nextProviders = providers ? { ...providers } : {}; + delete nextProviders[BUILTIN_PROXY_PROVIDER_NAME]; + const providerNames = Object.keys(nextProviders); + const fallbackProvider = providerNames[0] || ''; + const currentModels = readCurrentModels(); + const fallbackModel = fallbackProvider + ? (currentModels[fallbackProvider] || (typeof safeConfig.model === 'string' ? safeConfig.model : '')) + : ''; + + return { + ...safeConfig, + model_providers: nextProviders, + model_provider: currentIsRemovedBuiltin ? fallbackProvider : safeConfig.model_provider, + model: currentIsRemovedBuiltin ? fallbackModel : safeConfig.model + }; +} + function readConfigOrVirtualDefault() { if (fs.existsSync(CONFIG_FILE)) { try { + removePersistedBuiltinProxyProviderFromConfig(); return { - config: readConfig(), + config: sanitizeRemovedBuiltinProxyProvider(readConfig()), isVirtual: false, reason: '', detail: '', @@ -3196,7 +3235,9 @@ function readConfigOrVirtualDefault() { ? e.configDetail.trim() : (e && e.message ? e.message : publicReason); return { - config: errorType === 'missing' ? buildVirtualDefaultConfig() : {}, + config: errorType === 'missing' + ? sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig()) + : {}, isVirtual: true, reason: publicReason, detail, @@ -3206,7 +3247,7 @@ function readConfigOrVirtualDefault() { } return { - config: buildVirtualDefaultConfig(), + config: sanitizeRemovedBuiltinProxyProvider(buildVirtualDefaultConfig()), isVirtual: true, reason: '未检测到 config.toml', detail: `配置文件不存在: ${CONFIG_FILE}`, @@ -3302,8 +3343,8 @@ function getConfigTemplate(params = {}) { } } catch (e) {} } - const selectedProvider = params.provider || ''; - const selectedModel = params.model || ''; + const selectedProvider = typeof params.provider === 'string' ? params.provider.trim() : ''; + const selectedModel = typeof params.model === 'string' ? params.model.trim() : ''; let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel); if (typeof params.serviceTier === 'string') { template = applyServiceTierToTemplate(template, params.serviceTier); @@ -3380,7 +3421,7 @@ function addProviderToConfig(params = {}) { return { error: 'local provider 为系统保留名称,不可新增' }; } if (isBuiltinProxyProvider(name) && !allowManaged) { - return { error: '本地代理配置为系统内建项,不可手动添加' }; + return { error: 'codexmate-proxy 为保留名称,不可手动添加' }; } ensureConfigDir(); @@ -3458,7 +3499,7 @@ function updateProviderInConfig(params = {}) { if (isDefaultLocalProvider(name)) { return { error: 'local provider 为系统保留项,不可编辑' }; } - return { error: '本地代理配置为系统内建项,不可编辑' }; + return { error: 'codexmate-proxy 为保留名称,不可编辑' }; } try { @@ -3476,7 +3517,7 @@ function deleteProviderFromConfig(params = {}) { if (isDefaultLocalProvider(name)) { return { error: 'local provider 为系统保留项,不可删除' }; } - return { error: '本地代理配置为系统内建项,不可删除' }; + return { error: 'codexmate-proxy 为保留名称,不可删除' }; } if (!fs.existsSync(CONFIG_FILE)) { return { error: 'config.toml 不存在' }; @@ -3506,7 +3547,7 @@ function performProviderDeletion(name, options = {}) { if (isNonDeletableProvider(name)) { const msg = isDefaultLocalProvider(name) ? 'local provider 为系统保留项,不可删除' - : '本地代理配置为系统内建项,不可删除'; + : 'codexmate-proxy 为保留名称,不可删除'; if (!silent) console.error('错误:', msg); return { error: msg }; } @@ -5511,6 +5552,124 @@ function buildProxyListenUrl(settings) { return `http://${host}:${settings.port}`; } +function buildBuiltinProxyProviderBaseUrl(settings) { + return `${buildProxyListenUrl(settings).replace(/\/+$/, '')}/v1`; +} + +function buildBuiltinProxyProviderConfig(settings) { + return { + name: BUILTIN_PROXY_PROVIDER_NAME, + base_url: buildBuiltinProxyProviderBaseUrl(settings), + wire_api: 'responses', + requires_openai_auth: false, + preferred_auth_method: '', + request_max_retries: 4, + stream_max_retries: 10, + stream_idle_timeout_ms: 300000 + }; +} + +function injectBuiltinProxyProvider(config) { + return isPlainObject(config) ? config : {}; +} + +function removePersistedBuiltinProxyProviderFromConfig() { + if (!fs.existsSync(CONFIG_FILE)) { + return { success: true, removed: false }; + } + + let config; + try { + config = readConfig(); + } catch (e) { + return { error: e.message || '读取 config.toml 失败' }; + } + + if (!config.model_providers || !config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]) { + return { success: true, removed: false }; + } + + const content = fs.readFileSync(CONFIG_FILE, 'utf-8'); + const lineEnding = content.includes('\r\n') ? '\r\n' : '\n'; + const hasBom = content.charCodeAt(0) === 0xFEFF; + const providerConfig = config.model_providers[BUILTIN_PROXY_PROVIDER_NAME]; + const providerSegments = providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments) + ? providerConfig.__codexmate_legacy_segments + : null; + const providerSegmentVariants = (() => { + const variants = []; + const seen = new Set(); + const pushVariant = (segments) => { + const normalized = normalizeLegacySegments(segments); + const key = buildLegacySegmentsKey(normalized); + if (!key || seen.has(key)) return; + seen.add(key); + variants.push(normalized); + }; + if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segments)) { + pushVariant(providerConfig.__codexmate_legacy_segments); + } + if (providerConfig && Array.isArray(providerConfig.__codexmate_legacy_segment_variants)) { + for (const segments of providerConfig.__codexmate_legacy_segment_variants) { + pushVariant(segments); + } + } + if (providerSegments) { + pushVariant(providerSegments); + } + if (variants.length === 0) { + pushVariant(String(BUILTIN_PROXY_PROVIDER_NAME || '').split('.').filter((item) => item)); + } + return variants; + })(); + + let updatedContent = null; + const combinedRanges = []; + for (const segments of providerSegmentVariants) { + combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, segments)); + combinedRanges.push(...findProviderDescendantSectionRanges(content, segments)); + } + if (combinedRanges.length === 0) { + combinedRanges.push(...findProviderSectionRanges(content, BUILTIN_PROXY_PROVIDER_NAME, providerSegments)); + } + + if (combinedRanges.length > 0) { + const sorted = combinedRanges.sort((a, b) => b.start - a.start || b.end - a.end); + const seen = new Set(); + let removedContent = content; + for (const range of sorted) { + const rangeKey = `${range.start}:${range.end}`; + if (seen.has(rangeKey)) continue; + seen.add(rangeKey); + removedContent = removedContent.slice(0, range.start) + removedContent.slice(range.end); + } + updatedContent = removedContent.replace(/\n{3,}/g, lineEnding + lineEnding); + } + + if (!updatedContent) { + const rebuilt = JSON.parse(JSON.stringify(config)); + delete rebuilt.model_providers[BUILTIN_PROXY_PROVIDER_NAME]; + const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER); + let rebuiltToml = toml.stringify(rebuilt).trimEnd(); + rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding); + if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) { + rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`; + } + updatedContent = rebuiltToml + lineEnding; + if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) { + updatedContent = '\uFEFF' + updatedContent; + } + } + + try { + writeConfig(updatedContent.trimEnd() + lineEnding); + } catch (e) { + return { error: e.message || '写入 config.toml 失败' }; + } + + return { success: true, removed: true }; +} + function hasCodexConfigReadyForProxy() { const result = readConfigOrVirtualDefault(); if (!result || result.isVirtual) { @@ -5787,131 +5946,11 @@ function getBuiltinProxyStatus() { } function applyBuiltinProxyProvider(params = {}) { - const settings = readBuiltinProxySettings(); - const hostForUrl = formatHostForUrl(settings.host); - const baseUrl = `http://${hostForUrl}:${settings.port}`; - - const { config } = readConfigOrVirtualDefault(); - const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {}; - const exists = !!providers[BUILTIN_PROXY_PROVIDER_NAME]; - const saveResult = exists - ? updateProviderInConfig({ - name: BUILTIN_PROXY_PROVIDER_NAME, - url: baseUrl, - key: '', - allowManaged: true - }) - : addProviderToConfig({ - name: BUILTIN_PROXY_PROVIDER_NAME, - url: baseUrl, - key: '', - allowManaged: true - }); - - if (saveResult && saveResult.error) { - return saveResult; - } - - const switchToProxy = params.switchToProxy !== false; - let targetModel = ''; - if (switchToProxy) { - try { - targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || ''; - } catch (e) { - return { error: `写入代理 provider 成功,但切换失败: ${e.message}` }; - } - } - - return { - success: true, - provider: BUILTIN_PROXY_PROVIDER_NAME, - baseUrl, - switched: switchToProxy, - model: targetModel - }; + return { error: '该功能已移除' }; } async function ensureBuiltinProxyForCodexDefault(params = {}) { - const payload = isPlainObject(params) ? { ...params } : {}; - const switchToProxy = payload.switchToProxy !== false; - delete payload.switchToProxy; - payload.enabled = true; - - const saveResult = saveBuiltinProxySettings(payload); - if (saveResult.error) { - return { error: saveResult.error }; - } - let nextSettings = saveResult.settings; - - let upstreamResult = resolveBuiltinProxyUpstream(nextSettings); - if (upstreamResult.error) { - return { error: upstreamResult.error }; - } - - const runtime = g_builtinProxyRuntime; - const shouldRestart = !!runtime && ( - runtime.settings.host !== nextSettings.host - || runtime.settings.port !== nextSettings.port - || runtime.settings.authSource !== nextSettings.authSource - || runtime.settings.timeoutMs !== nextSettings.timeoutMs - || runtime.upstream.providerName !== upstreamResult.providerName - || runtime.upstream.baseUrl !== upstreamResult.baseUrl - || runtime.upstream.authHeader !== upstreamResult.authHeader - ); - - if (shouldRestart) { - await stopBuiltinProxyRuntime(); - } - - if (!g_builtinProxyRuntime) { - let startRes = await startBuiltinProxyRuntime(nextSettings); - if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) { - const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30); - if (fallbackPort > 0) { - const retrySave = saveBuiltinProxySettings({ - ...nextSettings, - port: fallbackPort, - enabled: true - }); - if (retrySave.success) { - nextSettings = retrySave.settings; - upstreamResult = resolveBuiltinProxyUpstream(nextSettings); - if (upstreamResult.error) { - return { error: upstreamResult.error }; - } - startRes = await startBuiltinProxyRuntime(nextSettings); - } - } - } - if (!startRes.success) { - return { error: startRes.error || '启动内建代理失败' }; - } - } - - let applyRes = { - success: true, - provider: BUILTIN_PROXY_PROVIDER_NAME, - baseUrl: buildProxyListenUrl(nextSettings), - switched: false, - model: '' - }; - if (switchToProxy) { - applyRes = applyBuiltinProxyProvider({ switchToProxy: true }); - if (applyRes.error) { - return applyRes; - } - } - - const status = getBuiltinProxyStatus(); - return { - success: true, - provider: applyRes.provider, - baseUrl: applyRes.baseUrl, - switched: applyRes.switched, - model: applyRes.model || '', - settings: status.settings, - runtime: status.runtime - }; + return { error: '该功能已移除' }; } function removeClaudeSessionIndexEntry(indexPath, sessionFilePath, sessionId) { @@ -7553,7 +7592,8 @@ async function cmdSetup() { const { config } = readConfigOrVirtualDefault(); const providers = config.model_providers || {}; const providerNames = Object.keys(providers); - const defaultProvider = config.model_provider || providerNames[0] || ''; + const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : ''; + const defaultProvider = currentProvider || providerNames[0] || ''; let availableModels = []; let defaultModel = config.model || ''; let modelFetchUnlimited = false; @@ -7839,7 +7879,7 @@ async function cmdModels() { // 切换提供商 function cmdSwitch(providerName, silent = false) { - const config = readConfig(); + const config = sanitizeRemovedBuiltinProxyProvider(readConfig()); const providers = config.model_providers || {}; if (!providers[providerName]) { @@ -8006,7 +8046,7 @@ function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) { if (isNonEditableProvider(name) && !allowManaged) { const msg = isDefaultLocalProvider(name) ? 'local provider 为系统保留项,不可编辑' - : '本地代理配置为系统内建项,不可编辑'; + : 'codexmate-proxy 为保留名称,不可编辑'; if (!silent) console.error(`错误: ${msg}`); throw new Error(msg); } @@ -10556,25 +10596,6 @@ function cmdStart(options = {}) { openBrowser: !options.noBrowser }); - const proxySettings = readBuiltinProxySettings(); - const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy(); - if (shouldAutoStartProxy) { - ensureBuiltinProxyForCodexDefault({ - ...proxySettings, - switchToProxy: false - }).then((res) => { - if (res && res.success && res.runtime && res.runtime.listenUrl) { - const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME; - const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : ''; - console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`); - } else if (res && res.error) { - console.warn(`! 内建代理启动失败: ${res.error}`); - } - }).catch((err) => { - console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`); - }); - } - const requestWebUiRestart = createSerializedWebUiRestartHandler(async (info) => { const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown'); console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`); @@ -10766,111 +10787,8 @@ function parseProxyCliOptions(args = []) { } async function cmdProxy(args = []) { - const subcommand = (args[0] || 'status').toLowerCase(); - const optionResult = parseProxyCliOptions(args.slice(1)); - if (optionResult.error) { - throw new Error(optionResult.error); - } - const options = optionResult.payload || {}; - - if (subcommand === 'status') { - const status = getBuiltinProxyStatus(); - const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS; - console.log('\n内建代理状态:'); - console.log(' 运行中:', status.running ? '是' : '否'); - console.log(' 启用:', settings.enabled ? '是' : '否'); - console.log(' 监听:', buildProxyListenUrl(settings)); - console.log(' 上游 provider:', settings.provider || '(自动)'); - console.log(' 鉴权来源:', settings.authSource); - if (status.runtime) { - console.log(' 实际上游:', status.runtime.upstreamProvider); - console.log(' 启动时间:', status.runtime.startedAt); - } - console.log(); - return; - } - - if (subcommand === 'set' || subcommand === 'config') { - const result = saveBuiltinProxySettings(options); - if (result.error) { - throw new Error(result.error); - } - const settings = result.settings; - console.log('✓ 内建代理配置已保存'); - console.log(' 监听:', buildProxyListenUrl(settings)); - console.log(' 上游 provider:', settings.provider || '(自动)'); - console.log(' 鉴权来源:', settings.authSource); - console.log(); - return; - } - - if (subcommand === 'apply' || subcommand === 'apply-provider') { - const result = applyBuiltinProxyProvider({ - switchToProxy: options.switchToProxy !== false - }); - if (result.error) { - throw new Error(result.error); - } - console.log(`✓ 已写入本地代理 provider: ${result.provider}`); - console.log(` URL: ${result.baseUrl}`); - if (result.switched) { - console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`); - } - console.log(); - return; - } - - if (subcommand === 'enable' || subcommand === 'default-codex') { - const result = await ensureBuiltinProxyForCodexDefault(options); - if (result.error) { - throw new Error(result.error); - } - const listenUrl = result.runtime && result.runtime.listenUrl - ? result.runtime.listenUrl - : buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS); - console.log('✓ 已启用 Codex 内建代理默认模式'); - console.log(` 监听: ${listenUrl}`); - if (result.runtime && result.runtime.upstreamProvider) { - console.log(` 上游 provider: ${result.runtime.upstreamProvider}`); - } - console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`); - console.log(); - return; - } - - if (subcommand === 'start') { - const result = await startBuiltinProxyRuntime({ - ...options, - enabled: true - }); - if (result.error) { - throw new Error(result.error); - } - console.log(`✓ 内建代理已启动: ${result.listenUrl}`); - console.log(` 上游 provider: ${result.upstreamProvider}`); - console.log(' 按 Ctrl+C 停止代理\n'); - - await new Promise((resolve) => { - let stopping = false; - const gracefulStop = async () => { - if (stopping) return; - stopping = true; - await stopBuiltinProxyRuntime(); - resolve(); - }; - process.once('SIGINT', gracefulStop); - process.once('SIGTERM', gracefulStop); - }); - return; - } - - if (subcommand === 'stop') { - await stopBuiltinProxyRuntime(); - console.log('✓ 内建代理已停止\n'); - return; - } - - throw new Error(`未知 proxy 子命令: ${subcommand}`); + void args; + throw new Error('该功能已移除'); } function parseWorkflowInputArg(rawInput) { @@ -12944,11 +12862,9 @@ async function main() { console.log(' codexmate claude [模型] 写入 Claude Code 配置'); console.log(' codexmate add-model <模型> 添加模型'); console.log(' codexmate delete-model <模型> 删除模型'); - console.log(' codexmate auth 认证文件管理'); - console.log(' codexmate proxy 内建代理'); console.log(' codexmate workflow MCP 工作流中心'); console.log(' codexmate run [--host ] [--no-browser] 启动 Web 界面'); - console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo(不会自动启用内建代理)'); + console.log(' codexmate codex [参数...] [--follow-up <文本>|--queued-follow-up <文本> 可重复] 等同于 codex --yolo'); console.log(' 注: follow-up 自动排队仅支持 linux/android/netbsd/openbsd/darwin/freebsd 且 stdin 必须是 TTY,其他平台会报错'); console.log(' codexmate qwen [参数...] 等同于 qwen --yolo'); console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]'); diff --git a/tests/e2e/test-auth-proxy.js b/tests/e2e/test-auth-proxy.js index 5e22d6c..311b375 100644 --- a/tests/e2e/test-auth-proxy.js +++ b/tests/e2e/test-auth-proxy.js @@ -47,7 +47,7 @@ module.exports = async function testAuthProxy(ctx) { const { api, node, cliPath, env, tmpHome, mockProviderUrl } = ctx; const initialProxyStatus = await waitForProxyRunning(api); - assert(initialProxyStatus && initialProxyStatus.running === true, 'proxy should auto-start when codex config exists'); + assert(initialProxyStatus && typeof initialProxyStatus.running === 'boolean', 'proxy-status should return running flag'); const userProvidedFixture = 'C:\\Users\\Ymkiux\\Downloads\\fb5eefedbd149f08aeb3fe6c5a212efabb76d8df\\wbqm928lf@jienigui.me.json'; const fixturePath = fs.existsSync(userProvidedFixture) @@ -102,6 +102,16 @@ module.exports = async function testAuthProxy(ctx) { }); assert(importBackupRes.success === true, 'api import-auth-profile should succeed'); + const authProfilesDir = path.join(tmpHome, '.codex', 'auth-profiles'); + const backupAuthProfilePath = path.join(authProfilesDir, 'backup-auth.json'); + const authRegistryPath = path.join(authProfilesDir, 'registry.json'); + assert(fs.existsSync(backupAuthProfilePath), 'uploaded auth profile should be stored under ~/.codex/auth-profiles'); + assert(fs.existsSync(authRegistryPath), 'auth profile registry should be stored under ~/.codex/auth-profiles'); + assert( + !fs.existsSync(path.join(tmpHome, '.codexmate', 'codex', 'auth-profiles', 'backup-auth.json')), + 'uploaded auth profile should not be written into ~/.codexmate/codex/auth-profiles' + ); + const authProfiles = await api('list-auth-profiles'); assert(Array.isArray(authProfiles.profiles), 'list-auth-profiles should return array'); assert(authProfiles.profiles.some(item => item.name === 'backup-auth'), 'backup-auth profile missing'); @@ -141,7 +151,22 @@ module.exports = async function testAuthProxy(ctx) { }); assert(proxySaveRes.success === true, 'proxy-save-config should succeed'); - const proxyEnableRes = await api('proxy-enable-codex-default', { + const configTomlPath = path.join(tmpHome, '.codex', 'config.toml'); + fs.appendFileSync(configTomlPath, [ + '', + '[model_providers.codexmate-proxy]', + 'name = "codexmate-proxy"', + `base_url = "http://127.0.0.1:${proxyPort}/v1"`, + 'wire_api = "responses"', + 'requires_openai_auth = false', + 'preferred_auth_method = ""', + 'request_max_retries = 4', + 'stream_max_retries = 10', + 'stream_idle_timeout_ms = 300000', + '' + ].join('\n'), 'utf-8'); + + const proxyStartRes = await api('proxy-start', { enabled: true, host: '127.0.0.1', port: proxyPort, @@ -149,42 +174,30 @@ module.exports = async function testAuthProxy(ctx) { authSource: 'provider', timeoutMs: 5000 }); - assert(proxyEnableRes.success === true, `proxy-enable-codex-default failed: ${proxyEnableRes.error || ''}`); - assert(proxyEnableRes.runtime && proxyEnableRes.runtime.listenUrl && proxyEnableRes.runtime.listenUrl.includes(String(proxyPort)), 'proxy enable listen url mismatch'); + assert(proxyStartRes.success === true, `proxy-start failed: ${proxyStartRes.error || ''}`); + assert(proxyStartRes.listenUrl && proxyStartRes.listenUrl.includes(String(proxyPort)), 'proxy start listen url mismatch'); - const updateBuiltinRes = await api('update-provider', { - name: 'codexmate-proxy', - url: 'http://127.0.0.1:6553', - key: '' - }); + const providerList = await api('list'); assert( - typeof updateBuiltinRes.error === 'string' && updateBuiltinRes.error.includes('不可编辑'), - 'builtin proxy provider should be read-only for update' + !providerList.providers.some((item) => item && item.name === 'codexmate-proxy'), + 'provider list should not expose removed builtin proxy provider' ); - const deleteBuiltinRes = await api('delete-provider', { - name: 'codexmate-proxy' - }); + const configTomlAfterList = fs.readFileSync(configTomlPath, 'utf-8'); assert( - typeof deleteBuiltinRes.error === 'string' && deleteBuiltinRes.error.includes('不可删除'), - 'builtin proxy provider should be read-only for delete' + !/^\s*\[\s*model_providers\s*\.\s*(?:"codexmate-proxy"|'codexmate-proxy'|codexmate-proxy)\s*\]\s*$/m.test(configTomlAfterList), + 'provider list read should physically remove legacy codexmate-proxy block from config.toml' ); - const exportConfigRes = await api('export-config', { includeKeys: true }); - assert(exportConfigRes && exportConfigRes.data && exportConfigRes.data.providers, 'export-config should return provider map'); - assert(!exportConfigRes.data.providers['codexmate-proxy'], 'export-config should exclude builtin proxy provider'); - - const importConfigRes = await api('import-config', { - payload: exportConfigRes.data, - options: { - overwriteProviders: true, - applyCurrent: true, - applyCurrentModels: true - } + const enableRes = await api('proxy-enable-codex-default', { + enabled: true, + host: '127.0.0.1', + port: proxyPort, + provider: upstreamCandidate.name }); - assert(importConfigRes && importConfigRes.success === true, `import-config should succeed: ${importConfigRes && importConfigRes.error ? importConfigRes.error : ''}`); - - const statusAfterEnable = await api('status'); - assert(statusAfterEnable.provider === 'codexmate-proxy', 'status should switch to codexmate-proxy after proxy-enable-codex-default'); + assert( + typeof enableRes.error === 'string' && enableRes.error.includes('已移除'), + 'proxy-enable-codex-default should report removed builtin proxy provider' + ); const healthViaProxy = await requestJson(`http://127.0.0.1:${proxyPort}/health`); assert(healthViaProxy.statusCode === 200, `proxy /health should return 200, got ${healthViaProxy.statusCode}`); diff --git a/tests/e2e/test-setup.js b/tests/e2e/test-setup.js index 6531b90..0c46a9b 100644 --- a/tests/e2e/test-setup.js +++ b/tests/e2e/test-setup.js @@ -10,7 +10,6 @@ module.exports = async function testSetup(ctx) { const { env, node, cliPath, mockProviderUrl, noModelsUrl, htmlModelsUrl, authFailUrl, tmpHome } = ctx; const setupInput = [ - '2', 'e2e', mockProviderUrl, 'sk-test', @@ -53,6 +52,12 @@ module.exports = async function testSetup(ctx) { assert(listResult.status === 0, 'list failed'); assert(listResult.stdout.includes('e2e'), 'list missing provider'); + const helpResult = runSync(node, [cliPath], { env }); + assert(helpResult.status === 0, 'help output failed'); + assert(!helpResult.stdout.includes('codexmate proxy'), 'help should not expose removed proxy entry'); + assert(!helpResult.stdout.includes('codexmate auth'), 'help should not expose removed auth entry'); + assert(!helpResult.stdout.includes('内建代理'), 'help should not mention removed builtin proxy'); + const claudeModel = 'claude-e2e'; const claudeResult = runSync(node, [cliPath, 'claude', mockProviderUrl, 'sk-claude', claudeModel], { env }); assert(claudeResult.status === 0, `claude command failed: ${claudeResult.stderr || claudeResult.stdout}`); diff --git a/web-ui/app.js b/web-ui/app.js index 3a768bf..3e124b5 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -381,24 +381,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; claudeImportLoading: false, codexImportLoading: false, codexAuthProfiles: [], - codexAuthImportLoading: false, - codexAuthSwitching: {}, - codexAuthDeleting: {}, - proxySettings: { - enabled: false, - host: '127.0.0.1', - port: 8318, - provider: '', - authSource: 'provider', - timeoutMs: 30000 - }, - proxyRuntime: null, - proxyLoading: false, - proxySaving: false, - proxyStarting: false, - proxyStopping: false, - proxyApplying: false, - showProxyAdvanced: false, forceCompactLayout: false } }, @@ -583,47 +565,15 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; return list; }, hasLocalAndProxy() { - const list = Array.isArray(this.providersList) ? this.providersList : []; - if (!list.length) return false; - const hasLocal = list.some((item) => item && String(item.name || '').trim().toLowerCase() === 'local'); - const hasProxy = list.some((item) => item && String(item.name || '').trim().toLowerCase() === 'codexmate-proxy'); - return hasLocal && hasProxy; + return false; }, displayCurrentProvider() { const current = String(this.currentProvider || '').trim(); - if (!current) return ''; - if (this.hasLocalAndProxy && current.toLowerCase() === 'local') { - return 'codexmate-proxy'; - } return current; }, - localHiddenNotice() { - return this.hasLocalAndProxy && String(this.currentProvider || '').trim().toLowerCase() === 'local'; - }, displayProvidersList() { const list = Array.isArray(this.providersList) ? this.providersList : []; - if (!list.length) return []; - if (!this.hasLocalAndProxy) { - return list; - } - return list.filter((item) => String(item && item.name ? item.name : '').trim().toLowerCase() !== 'local'); - }, - proxyProviderOptions() { - const source = Array.isArray(this.providersList) ? this.providersList : []; - const list = source - .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : '')) - .filter((name) => name && name !== 'codexmate-proxy'); - return Array.from(new Set(list)); - }, - proxyRuntimeDisplayProvider() { - if (!this.proxyRuntime) return ''; - const hasProxy = this.hasLocalAndProxy; - const value = typeof this.proxyRuntime.provider === 'string' - ? this.proxyRuntime.provider.trim() - : ''; - if (!value) return hasProxy ? 'codexmate-proxy' : 'local'; - if (value.toLowerCase() === 'local') return hasProxy ? 'codexmate-proxy' : value; - return value; + return list.filter((item) => String(item && item.name ? item.name : '').trim().toLowerCase() !== 'codexmate-proxy'); }, installTargetCards() { const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : []; @@ -674,7 +624,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用'); if (this.agentsSaving) tasks.push('AGENTS 保存'); if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting || this.skillsZipImporting || this.skillsExporting) tasks.push('Skills 管理'); - if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) tasks.push('代理更新'); return tasks.length ? tasks.join(' / ') : '空闲'; }, inspectorMessageSummary() { @@ -714,15 +663,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; } return '正常'; }, - inspectorProxyStatus() { - if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) { - return '状态更新中'; - } - if (this.proxyRuntime && this.proxyRuntime.running === true) { - return `运行中(${this.proxyRuntimeDisplayProvider})`; - } - return '未运行'; - }, installTroubleshootingTips() { const platform = this.resolveInstallPlatform(); if (platform === 'win32') { @@ -786,12 +726,9 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; } try { - await Promise.all([ - this.loadCodexAuthProfiles(), - this.loadProxyStatus() - ]); + await this.loadCodexAuthProfiles(); } catch (e) { - // 认证/代理状态加载失败不阻塞主界面 + // 认证状态加载失败不阻塞主界面 } }, @@ -3877,7 +3814,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; ? String(providerOrName.name || '') : String(providerOrName); const normalized = rawName.trim().toLowerCase(); - return normalized === 'local' || normalized === 'codexmate-proxy'; + return normalized === 'local'; }, providerPillState(provider) { @@ -3917,7 +3854,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; if (!providerOrName) return false; if (typeof providerOrName === 'object') { const directName = String(providerOrName.name || '').trim().toLowerCase(); - if (directName === 'local' || directName === 'codexmate-proxy') { + if (directName === 'local') { return true; } return !!providerOrName.nonDeletable; @@ -3925,7 +3862,7 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; const name = String(providerOrName).trim(); if (!name) return false; const normalized = name.toLowerCase(); - if (normalized === 'local' || normalized === 'codexmate-proxy') { + if (normalized === 'local') { return true; } const target = (this.providersList || []).find((item) => item && item.name === name); @@ -5597,215 +5534,6 @@ import { createSkillsMethods } from './modules/skills.methods.mjs'; } }, - triggerCodexAuthUpload() { - const input = this.$refs.codexAuthImportInput; - if (input) { - input.value = ''; - input.click(); - } - }, - - handleCodexAuthImportChange(event) { - const file = event && event.target && event.target.files ? event.target.files[0] : null; - if (file) { - void this.importCodexAuthFile(file); - } - }, - - resetCodexAuthImportInput() { - const el = this.$refs.codexAuthImportInput; - if (el) { - el.value = ''; - } - }, - - async importCodexAuthFile(file) { - this.codexAuthImportLoading = true; - try { - const base64 = await this.readFileAsBase64(file); - const res = await api('import-auth-profile', { - fileName: file.name || 'codex-auth.json', - fileBase64: base64, - activate: true - }); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - await this.loadCodexAuthProfiles({ silent: true }); - this.showMessage('认证文件已导入并切换', 'success'); - } catch (e) { - this.showMessage('导入认证文件失败', 'error'); - } finally { - this.codexAuthImportLoading = false; - this.resetCodexAuthImportInput(); - } - }, - - async switchCodexAuthProfile(name) { - const key = String(name || '').trim(); - if (!key || this.codexAuthSwitching[key]) return; - this.codexAuthSwitching[key] = true; - try { - const res = await api('switch-auth-profile', { name: key }); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - await this.loadCodexAuthProfiles({ silent: true }); - this.showMessage(`已切换认证: ${key}`, 'success'); - } catch (e) { - this.showMessage('切换认证失败', 'error'); - } finally { - this.codexAuthSwitching[key] = false; - } - }, - - async deleteCodexAuthProfile(name) { - const key = String(name || '').trim(); - if (!key || this.codexAuthDeleting[key]) return; - this.codexAuthDeleting[key] = true; - try { - const res = await api('delete-auth-profile', { name: key }); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - await this.loadCodexAuthProfiles({ silent: true }); - const switchedTip = res && res.switchedTo ? `,已切换到 ${res.switchedTo}` : ''; - this.showMessage(`已删除认证${switchedTip}`, 'success'); - } catch (e) { - this.showMessage('删除认证失败', 'error'); - } finally { - this.codexAuthDeleting[key] = false; - } - }, - - mergeProxySettings(nextSettings) { - const safe = nextSettings && typeof nextSettings === 'object' ? nextSettings : {}; - const port = parseInt(String(safe.port), 10); - const timeoutMs = parseInt(String(safe.timeoutMs), 10); - this.proxySettings = { - enabled: safe.enabled !== false, - host: typeof safe.host === 'string' && safe.host.trim() ? safe.host.trim() : '127.0.0.1', - port: Number.isFinite(port) ? port : 8318, - provider: typeof safe.provider === 'string' ? safe.provider.trim() : '', - authSource: safe.authSource === 'profile' || safe.authSource === 'none' ? safe.authSource : 'provider', - timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 30000 - }; - }, - - async loadProxyStatus(options = {}) { - const silent = !!options.silent; - this.proxyLoading = true; - try { - const res = await api('proxy-status'); - if (res && res.error) { - if (!silent) { - this.showMessage(res.error, 'error'); - } - return; - } - this.mergeProxySettings(res && res.settings ? res.settings : {}); - this.proxyRuntime = res && res.runtime ? { running: true, ...res.runtime } : null; - } catch (e) { - if (!silent) { - this.showMessage('读取代理状态失败', 'error'); - } - } finally { - this.proxyLoading = false; - } - }, - - async saveProxySettings(options = {}) { - const silent = !!options.silent; - this.proxySaving = true; - try { - const res = await api('proxy-save-config', this.proxySettings); - if (res && res.error) { - if (!silent) { - this.showMessage(res.error, 'error'); - } - return; - } - if (res && res.settings) { - this.mergeProxySettings(res.settings); - } - if (!silent) { - this.showMessage('代理配置已保存', 'success'); - } - } catch (e) { - if (!silent) { - this.showMessage('保存代理配置失败', 'error'); - } - } finally { - this.proxySaving = false; - } - }, - - async startBuiltinProxy() { - this.proxyStarting = true; - try { - const res = await api('proxy-start', { - ...this.proxySettings, - enabled: true - }); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - if (res && res.settings) { - this.mergeProxySettings(res.settings); - } - await this.loadProxyStatus({ silent: true }); - const listenTip = res && res.listenUrl ? `:${res.listenUrl}` : ''; - this.showMessage(`代理已启动${listenTip}`, 'success'); - } catch (e) { - this.showMessage('启动代理失败', 'error'); - } finally { - this.proxyStarting = false; - } - }, - - async stopBuiltinProxy() { - this.proxyStopping = true; - try { - const res = await api('proxy-stop'); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - await this.loadProxyStatus({ silent: true }); - this.showMessage('代理已停止', 'success'); - } catch (e) { - this.showMessage('停止代理失败', 'error'); - } finally { - this.proxyStopping = false; - } - }, - - async applyBuiltinProxyProvider() { - this.proxyApplying = true; - try { - const saveRes = await api('proxy-save-config', this.proxySettings); - if (saveRes && saveRes.error) { - this.showMessage(saveRes.error, 'error'); - return; - } - const res = await api('proxy-apply-provider', { switchToProxy: true }); - if (res && res.error) { - this.showMessage(res.error, 'error'); - return; - } - await this.loadAll(); - this.showMessage('本地代理 provider 已写入并切换', 'success'); - } catch (e) { - this.showMessage('应用代理 provider 失败', 'error'); - } finally { - this.proxyApplying = false; - } - }, - showMessage(text, type) { this.message = text; this.messageType = type || 'info'; diff --git a/web-ui/index.html b/web-ui/index.html index 0c7cfe7..f40e62f 100644 --- a/web-ui/index.html +++ b/web-ui/index.html @@ -432,7 +432,7 @@

Codex 配置需先改模板,再手动应用。
- {{ activeProviderBridgeHint }} 模板、认证和代理仅在 Codex 模式下可编辑。 + {{ activeProviderBridgeHint }} 模板仅在 Codex 模式下可编辑。
-
-
- Codex 认证文件 -
-
- 上传 JSON 切换账号(写入 ~/.codex/auth.json)。 -
- - -
暂无认证文件。
-
-
-
-
-
{{ profile.name }}
-
- {{ profile.type || 'unknown' }} - - {{ profile.current ? '当前' : '备用' }} - -
-
-
- - -
-
-
-
- email - {{ profile.email }} -
-
- account_id - {{ profile.accountId }} -
-
- expired - {{ profile.expired }} -
-
-
-
-
- -
-
- 内建代理 -
- -
-
- - - - -
-
- - - - -
-
高级参数修改后自动保存。
-
-
- 运行中:入口 {{ proxyRuntimeDisplayProvider }} - (上游 {{ proxyRuntime.upstreamProvider }} -
-
@@ -604,10 +508,6 @@

-
- 当前 provider 为 local,已合并显示为 codexmate-proxy。 -
-
{{ provider.name }} - 内建 + 系统
- {{ provider.readOnly ? '系统内建本地代理(自动维护)' : (provider.url || '未设置 URL') }} + {{ provider.url || '未设置 URL' }}
@@ -1526,8 +1426,6 @@

{{ inspectorHealthStatus }} 模型加载 {{ inspectorModelLoadStatus }} - 代理状态 - {{ inspectorProxyStatus }}

@@ -1932,7 +1830,7 @@

Providers(只读)
- 未发现 providers 配置(可能使用内置 provider 或 auth profiles)。 + 未发现 providers 配置(可能使用环境变量或外部配置)。
From 3d1a2c8a74e71522c6c5f90af6b2b4e09a5b87a2 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:13:56 +0000 Subject: [PATCH 2/5] fix: apply CodeRabbit auto-fixes Fixed 2 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit --- cli.js | 4 ++-- tests/e2e/test-setup.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cli.js b/cli.js index 663cc17..cdf652d 100644 --- a/cli.js +++ b/cli.js @@ -253,7 +253,7 @@ let g_builtinProxyRuntime = null; const DEFAULT_LOCAL_PROVIDER_NAME = 'local'; function isBuiltinProxyProvider(providerName) { - return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME; + return typeof providerName === 'string' && providerName.trim().toLowerCase() === BUILTIN_PROXY_PROVIDER_NAME.toLowerCase(); } function isReservedProviderNameForCreation(providerName) { @@ -12966,4 +12966,4 @@ async function main() { main().catch((err) => { console.error('错误:', err && err.message ? err.message : err); process.exit(1); -}); +}); \ No newline at end of file diff --git a/tests/e2e/test-setup.js b/tests/e2e/test-setup.js index 0c46a9b..920344d 100644 --- a/tests/e2e/test-setup.js +++ b/tests/e2e/test-setup.js @@ -52,7 +52,7 @@ module.exports = async function testSetup(ctx) { assert(listResult.status === 0, 'list failed'); assert(listResult.stdout.includes('e2e'), 'list missing provider'); - const helpResult = runSync(node, [cliPath], { env }); + const helpResult = runSync(node, [cliPath, '--help'], { env }); assert(helpResult.status === 0, 'help output failed'); assert(!helpResult.stdout.includes('codexmate proxy'), 'help should not expose removed proxy entry'); assert(!helpResult.stdout.includes('codexmate auth'), 'help should not expose removed auth entry'); @@ -161,4 +161,4 @@ module.exports = async function testSetup(ctx) { htmlModelsUrl, authFailUrl }); -}; +}; \ No newline at end of file From a599163479e685c2a0701d2a9b329e3905c5cd35 Mon Sep 17 00:00:00 2001 From: SurviveM <254925152+SurviveM@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:08:55 +0800 Subject: [PATCH 3/5] fix: resolve proxy removal review follow-ups --- cli.js | 6 +++++- tests/e2e/test-auth-proxy.js | 4 ++++ tests/e2e/test-setup.js | 4 ++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/cli.js b/cli.js index cdf652d..3c85082 100644 --- a/cli.js +++ b/cli.js @@ -7982,6 +7982,10 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) { if (!silent) console.error('错误: local provider 为系统保留名称,不可新增'); throw new Error('local provider 为系统保留名称,不可新增'); } + if (isBuiltinProxyProvider(providerName)) { + if (!silent) console.error('错误: codexmate-proxy 为保留名称,不可手动添加'); + throw new Error('codexmate-proxy 为保留名称,不可手动添加'); + } const config = readConfig(); if (config.model_providers && config.model_providers[providerName]) { @@ -12966,4 +12970,4 @@ async function main() { main().catch((err) => { console.error('错误:', err && err.message ? err.message : err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/tests/e2e/test-auth-proxy.js b/tests/e2e/test-auth-proxy.js index 311b375..d82f983 100644 --- a/tests/e2e/test-auth-proxy.js +++ b/tests/e2e/test-auth-proxy.js @@ -178,6 +178,10 @@ module.exports = async function testAuthProxy(ctx) { assert(proxyStartRes.listenUrl && proxyStartRes.listenUrl.includes(String(proxyPort)), 'proxy start listen url mismatch'); const providerList = await api('list'); + assert( + providerList && Array.isArray(providerList.providers), + `list should return providers: ${providerList && providerList.error ? providerList.error : JSON.stringify(providerList)}` + ); assert( !providerList.providers.some((item) => item && item.name === 'codexmate-proxy'), 'provider list should not expose removed builtin proxy provider' diff --git a/tests/e2e/test-setup.js b/tests/e2e/test-setup.js index 920344d..0c46a9b 100644 --- a/tests/e2e/test-setup.js +++ b/tests/e2e/test-setup.js @@ -52,7 +52,7 @@ module.exports = async function testSetup(ctx) { assert(listResult.status === 0, 'list failed'); assert(listResult.stdout.includes('e2e'), 'list missing provider'); - const helpResult = runSync(node, [cliPath, '--help'], { env }); + const helpResult = runSync(node, [cliPath], { env }); assert(helpResult.status === 0, 'help output failed'); assert(!helpResult.stdout.includes('codexmate proxy'), 'help should not expose removed proxy entry'); assert(!helpResult.stdout.includes('codexmate auth'), 'help should not expose removed auth entry'); @@ -161,4 +161,4 @@ module.exports = async function testSetup(ctx) { htmlModelsUrl, authFailUrl }); -}; \ No newline at end of file +}; From e677c44544e2c889e10baa0fd0cc70798e017f77 Mon Sep 17 00:00:00 2001 From: SurviveM <254925152+SurviveM@users.noreply.github.com> Date: Thu, 2 Apr 2026 00:53:18 +0800 Subject: [PATCH 4/5] test: remove proxy auth e2e script --- tests/e2e/run.js | 2 - tests/e2e/test-auth-proxy.js | 225 ----------------------------------- 2 files changed, 227 deletions(-) delete mode 100644 tests/e2e/test-auth-proxy.js diff --git a/tests/e2e/run.js b/tests/e2e/run.js index da94065..aa5442f 100644 --- a/tests/e2e/run.js +++ b/tests/e2e/run.js @@ -20,7 +20,6 @@ const testSessions = require('./test-sessions'); const testOpenclaw = require('./test-openclaw'); const testHealthSpeed = require('./test-health-speed'); const testMessages = require('./test-messages'); -const testAuthProxy = require('./test-auth-proxy'); const testMcp = require('./test-mcp'); const testWorkflow = require('./test-workflow'); const testInvalidConfig = require('./test-invalid-config'); @@ -117,7 +116,6 @@ async function main() { await testOpenclaw(ctx); await testHealthSpeed(ctx); await testMessages(ctx); - await testAuthProxy(ctx); await testMcp(ctx); await testWorkflow(ctx); diff --git a/tests/e2e/test-auth-proxy.js b/tests/e2e/test-auth-proxy.js deleted file mode 100644 index d82f983..0000000 --- a/tests/e2e/test-auth-proxy.js +++ /dev/null @@ -1,225 +0,0 @@ -const http = require('http'); -const { assert, fs, path, runSync } = require('./helpers'); - -function requestJson(url, timeoutMs = 4000) { - return new Promise((resolve, reject) => { - const req = http.get(url, (res) => { - let body = ''; - res.setEncoding('utf-8'); - res.on('data', (chunk) => body += chunk); - res.on('end', () => { - let parsed = {}; - try { - parsed = body ? JSON.parse(body) : {}; - } catch (e) { - return reject(new Error(`invalid json response: ${body.slice(0, 160)}`)); - } - resolve({ - statusCode: res.statusCode || 0, - headers: res.headers || {}, - body: parsed - }); - }); - }); - req.on('error', reject); - req.setTimeout(timeoutMs, () => { - req.destroy(new Error('request timeout')); - }); - }); -} - -async function waitForProxyRunning(api, retries = 20, delayMs = 150) { - let lastStatus = null; - for (let i = 0; i < retries; i += 1) { - // eslint-disable-next-line no-await-in-loop - const status = await api('proxy-status'); - lastStatus = status; - if (status && status.running) { - return status; - } - // eslint-disable-next-line no-await-in-loop - await new Promise((resolve) => setTimeout(resolve, delayMs)); - } - return lastStatus; -} - -module.exports = async function testAuthProxy(ctx) { - const { api, node, cliPath, env, tmpHome, mockProviderUrl } = ctx; - - const initialProxyStatus = await waitForProxyRunning(api); - assert(initialProxyStatus && typeof initialProxyStatus.running === 'boolean', 'proxy-status should return running flag'); - - const userProvidedFixture = 'C:\\Users\\Ymkiux\\Downloads\\fb5eefedbd149f08aeb3fe6c5a212efabb76d8df\\wbqm928lf@jienigui.me.json'; - const fixturePath = fs.existsSync(userProvidedFixture) - ? userProvidedFixture - : path.join(tmpHome, 'codex-auth.fixture.json'); - - if (!fs.existsSync(fixturePath)) { - const fallbackPayload = { - type: 'codex', - email: 'e2e-primary@example.com', - account_id: 'acc-e2e-primary', - access_token: 'token-primary-access', - refresh_token: 'token-primary-refresh', - id_token: 'token-primary-id', - expired: '2099-01-01T00:00:00Z', - last_refresh: '2026-03-18T00:00:00Z' - }; - fs.writeFileSync(fixturePath, JSON.stringify(fallbackPayload, null, 2), 'utf-8'); - } - - const importResult = runSync(node, [cliPath, 'auth', 'import', fixturePath], { env }); - assert(importResult.status === 0, `auth import failed: ${importResult.stderr || importResult.stdout}`); - - const listResult = runSync(node, [cliPath, 'auth', 'list'], { env }); - assert(listResult.status === 0, 'auth list should succeed'); - - const authPath = path.join(tmpHome, '.codex', 'auth.json'); - assert(fs.existsSync(authPath), 'auth.json should exist after auth import'); - const currentAuth = JSON.parse(fs.readFileSync(authPath, 'utf-8')); - assert( - typeof currentAuth.access_token === 'string' || typeof currentAuth.OPENAI_API_KEY === 'string', - 'auth.json should contain credential field' - ); - - const backupPayload = { - type: 'codex', - email: 'e2e-backup@example.com', - account_id: 'acc-e2e-backup', - access_token: 'token-backup-access', - refresh_token: 'token-backup-refresh', - id_token: 'token-backup-id', - expired: '2099-02-01T00:00:00Z', - last_refresh: '2026-03-18T00:00:00Z' - }; - const backupBase64 = Buffer.from(JSON.stringify(backupPayload), 'utf-8').toString('base64'); - - const importBackupRes = await api('import-auth-profile', { - fileName: 'backup-auth.json', - name: 'backup-auth', - fileBase64: backupBase64, - activate: false - }); - assert(importBackupRes.success === true, 'api import-auth-profile should succeed'); - - const authProfilesDir = path.join(tmpHome, '.codex', 'auth-profiles'); - const backupAuthProfilePath = path.join(authProfilesDir, 'backup-auth.json'); - const authRegistryPath = path.join(authProfilesDir, 'registry.json'); - assert(fs.existsSync(backupAuthProfilePath), 'uploaded auth profile should be stored under ~/.codex/auth-profiles'); - assert(fs.existsSync(authRegistryPath), 'auth profile registry should be stored under ~/.codex/auth-profiles'); - assert( - !fs.existsSync(path.join(tmpHome, '.codexmate', 'codex', 'auth-profiles', 'backup-auth.json')), - 'uploaded auth profile should not be written into ~/.codexmate/codex/auth-profiles' - ); - - const authProfiles = await api('list-auth-profiles'); - assert(Array.isArray(authProfiles.profiles), 'list-auth-profiles should return array'); - assert(authProfiles.profiles.some(item => item.name === 'backup-auth'), 'backup-auth profile missing'); - - const switchBackupRes = await api('switch-auth-profile', { name: 'backup-auth' }); - assert(switchBackupRes.success === true, 'switch-auth-profile should succeed'); - - const switchedAuth = JSON.parse(fs.readFileSync(authPath, 'utf-8')); - assert(switchedAuth.email === 'e2e-backup@example.com', 'switch-auth-profile should update auth.json'); - - const upstreamCandidate = { - name: 'proxy-e2e-upstream', - url: mockProviderUrl - }; - const addProxyProviderRes = await api('add-provider', { - name: upstreamCandidate.name, - url: upstreamCandidate.url, - key: 'sk-proxy-e2e' - }); - if (addProxyProviderRes.error) { - const updateProxyProviderRes = await api('update-provider', { - name: upstreamCandidate.name, - url: upstreamCandidate.url, - key: 'sk-proxy-e2e' - }); - assert(updateProxyProviderRes.success === true, 'failed to prepare proxy upstream provider'); - } - - const proxyPort = 19000 + Math.floor(Math.random() * 1000); - const proxySaveRes = await api('proxy-save-config', { - enabled: true, - host: '127.0.0.1', - port: proxyPort, - provider: upstreamCandidate.name, - authSource: 'provider', - timeoutMs: 5000 - }); - assert(proxySaveRes.success === true, 'proxy-save-config should succeed'); - - const configTomlPath = path.join(tmpHome, '.codex', 'config.toml'); - fs.appendFileSync(configTomlPath, [ - '', - '[model_providers.codexmate-proxy]', - 'name = "codexmate-proxy"', - `base_url = "http://127.0.0.1:${proxyPort}/v1"`, - 'wire_api = "responses"', - 'requires_openai_auth = false', - 'preferred_auth_method = ""', - 'request_max_retries = 4', - 'stream_max_retries = 10', - 'stream_idle_timeout_ms = 300000', - '' - ].join('\n'), 'utf-8'); - - const proxyStartRes = await api('proxy-start', { - enabled: true, - host: '127.0.0.1', - port: proxyPort, - provider: upstreamCandidate.name, - authSource: 'provider', - timeoutMs: 5000 - }); - assert(proxyStartRes.success === true, `proxy-start failed: ${proxyStartRes.error || ''}`); - assert(proxyStartRes.listenUrl && proxyStartRes.listenUrl.includes(String(proxyPort)), 'proxy start listen url mismatch'); - - const providerList = await api('list'); - assert( - providerList && Array.isArray(providerList.providers), - `list should return providers: ${providerList && providerList.error ? providerList.error : JSON.stringify(providerList)}` - ); - assert( - !providerList.providers.some((item) => item && item.name === 'codexmate-proxy'), - 'provider list should not expose removed builtin proxy provider' - ); - const configTomlAfterList = fs.readFileSync(configTomlPath, 'utf-8'); - assert( - !/^\s*\[\s*model_providers\s*\.\s*(?:"codexmate-proxy"|'codexmate-proxy'|codexmate-proxy)\s*\]\s*$/m.test(configTomlAfterList), - 'provider list read should physically remove legacy codexmate-proxy block from config.toml' - ); - - const enableRes = await api('proxy-enable-codex-default', { - enabled: true, - host: '127.0.0.1', - port: proxyPort, - provider: upstreamCandidate.name - }); - assert( - typeof enableRes.error === 'string' && enableRes.error.includes('已移除'), - 'proxy-enable-codex-default should report removed builtin proxy provider' - ); - - const healthViaProxy = await requestJson(`http://127.0.0.1:${proxyPort}/health`); - assert(healthViaProxy.statusCode === 200, `proxy /health should return 200, got ${healthViaProxy.statusCode}`); - - const modelsViaProxy = await requestJson(`http://127.0.0.1:${proxyPort}/v1/models`); - assert(modelsViaProxy.statusCode === 200, `proxy /v1/models should return 200, got ${modelsViaProxy.statusCode}`); - assert(Array.isArray(modelsViaProxy.body.data), 'proxy /v1/models should return model list'); - assert(modelsViaProxy.body.data.some(item => item.id === 'e2e2-model'), 'proxy /v1/models missing expected model id'); - - const proxyStatus = await api('proxy-status'); - assert(proxyStatus.running === true, 'proxy should be running before stop'); - assert(proxyStatus.runtime && proxyStatus.runtime.upstreamProvider === upstreamCandidate.name, 'proxy upstream provider mismatch'); - - const proxyStopRes = await api('proxy-stop'); - assert(proxyStopRes.success === true, 'proxy-stop should succeed'); - - const proxyStatusAfterStop = await api('proxy-status'); - assert(proxyStatusAfterStop.running === false, 'proxy should stop after proxy-stop'); - - await api('delete-provider', { name: upstreamCandidate.name }); -}; From 01af830a7e6b59c313955f5b8684253b45edcde5 Mon Sep 17 00:00:00 2001 From: SurviveM <254925152+SurviveM@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:00:51 +0800 Subject: [PATCH 5/5] ci: gate coderabbit comments on commit count --- .github/workflows/coderabbit-review.yml | 35 ++++++++++++++++++++++-- tests/unit/coderabbit-workflows.test.mjs | 10 +++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/coderabbit-review.yml b/.github/workflows/coderabbit-review.yml index e242fde..4365755 100644 --- a/.github/workflows/coderabbit-review.yml +++ b/.github/workflows/coderabbit-review.yml @@ -25,14 +25,43 @@ jobs: uses: actions/github-script@v8 with: script: | + const { owner, repo } = context.repo; const pr = context.payload.pull_request; + const markerRegex = //; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pr.number, + per_page: 100, + }); + + let previousCommitCount = null; + for (const comment of comments.slice().reverse()) { + const match = comment.body?.match(markerRegex); + if (!match) { + continue; + } + + previousCommitCount = Number(match[1]); + break; + } + + if (previousCommitCount === pr.commits) { + core.info( + `Skipping CodeRabbit re-review comment because commit count is unchanged at ${pr.commits}.` + ); + return; + } + const body = [ - "@coderabbitai re-review !Stop making breaking changes, do a proper review!", + "@coderabbitai re-review", + "Stop making breaking changes, do a proper review!", + ``, ].join("\n"); await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, + owner, + repo, issue_number: pr.number, body, }); diff --git a/tests/unit/coderabbit-workflows.test.mjs b/tests/unit/coderabbit-workflows.test.mjs index d18d275..ace7e3a 100644 --- a/tests/unit/coderabbit-workflows.test.mjs +++ b/tests/unit/coderabbit-workflows.test.mjs @@ -18,8 +18,14 @@ test('coderabbit autofix workflow is removed', () => { ); }); -test('coderabbit review workflow uses github-script v8 and sends the re-review command', () => { +test('coderabbit review workflow only comments when commit count changes and uses an ASCII-safe re-review command', () => { const workflow = readProjectFile('.github/workflows/coderabbit-review.yml'); assert.match(workflow, /uses:\s+actions\/github-script@v8/); - assert.match(workflow, /@coderabbitai re-review !Stop making breaking changes, do a proper review!/); + assert.match(workflow, /github\.paginate\(github\.rest\.issues\.listComments/); + assert.match(workflow, /codexmate-coderabbit-review-commit-count/); + assert.match(workflow, /previousCommitCount === pr\.commits/); + assert.match(workflow, /github\.rest\.issues\.createComment/); + assert.match(workflow, /@coderabbitai re-review/); + assert.match(workflow, /Stop making breaking changes, do a proper review!/); + assert.doesNotMatch(workflow, /!/); });