From 2541bd78fab5e3ace6b702be4d25f556a165e99e Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 08:17:45 +0000 Subject: [PATCH 01/21] feat(health): add claude speed probe to health checks --- cli.js | 87 ++++++++++++++++++++++++++ lib/cli-network-utils.js | 30 ++++++++- tests/unit/cli-network-utils.test.mjs | 42 +++++++++++++ web-ui/modules/app.methods.runtime.mjs | 19 +++++- 4 files changed, 175 insertions(+), 3 deletions(-) diff --git a/cli.js b/cli.js index a819f648..1cea1edf 100644 --- a/cli.js +++ b/cli.js @@ -6003,6 +6003,62 @@ function importConfigData(payload, options = {}) { function resolveSpeedTestTarget(params) { if (!params) return { error: 'Missing params' }; + if (typeof params.kind === 'string' && params.kind.trim() === 'claude') { + const baseUrl = typeof params.url === 'string' ? params.url.trim() : ''; + const apiKey = typeof params.apiKey === 'string' ? params.apiKey.trim() : ''; + const model = typeof params.model === 'string' ? params.model.trim() : ''; + if (!baseUrl) { + return { error: 'Missing url' }; + } + if (!apiKey) { + return { error: 'Missing apiKey' }; + } + if (!model) { + return { error: 'Missing model' }; + } + const normalizedBase = baseUrl.replace(/\/+$/, ''); + const candidates = []; + if (normalizedBase.endsWith('/v1')) { + candidates.push({ + method: 'POST', + url: `${normalizedBase}/messages`, + body: { + model, + max_tokens: 16, + messages: [{ role: 'user', content: 'ping' }] + } + }); + } else { + candidates.push({ + method: 'POST', + url: `${normalizedBase}/v1/messages`, + body: { + model, + max_tokens: 16, + messages: [{ role: 'user', content: 'ping' }] + } + }); + candidates.push({ + method: 'POST', + url: `${normalizedBase}/messages`, + body: { + model, + max_tokens: 16, + messages: [{ role: 'user', content: 'ping' }] + } + }); + } + return { + kind: 'claude', + candidates, + apiKey, + apiKeyHeader: 'x-api-key', + headers: { + 'anthropic-version': '2023-06-01' + } + }; + } + if (params.name) { const { config } = readConfigOrVirtualDefault(); const providers = config.model_providers || {}; @@ -6198,6 +6254,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) { if (method === 'POST') { return probeJsonPost(targetUrl, options.body || {}, { apiKey, + apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '', + headers: options.headers && typeof options.headers === 'object' ? options.headers : null, timeoutMs, maxBytes: 256 * 1024 }).then((result) => ({ @@ -6209,6 +6267,8 @@ function runSpeedTest(targetUrl, apiKey, options = {}) { } return probeUrl(targetUrl, { apiKey, + apiKeyHeader: typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader : '', + headers: options.headers && typeof options.headers === 'object' ? options.headers : null, timeoutMs, maxBytes: 256 * 1024 }).then((result) => ({ @@ -8446,6 +8506,33 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser result = { error: target.error }; break; } + if (target.kind === 'claude' && Array.isArray(target.candidates) && target.candidates.length > 0) { + let finalCandidate = target.candidates[0]; + let finalResult = null; + for (let index = 0; index < target.candidates.length; index += 1) { + const candidate = target.candidates[index]; + const probeResult = await runSpeedTest(candidate.url, target.apiKey, { + ...candidate, + apiKeyHeader: target.apiKeyHeader, + headers: target.headers + }); + finalCandidate = candidate; + finalResult = probeResult; + const shouldTryNext = index < target.candidates.length - 1 + && (!probeResult.ok || probeResult.status === 404); + if (!shouldTryNext) { + break; + } + } + result = { + ok: !!(finalResult && finalResult.ok), + status: Number.isFinite(finalResult && finalResult.status) ? finalResult.status : 0, + durationMs: Number.isFinite(finalResult && finalResult.durationMs) ? finalResult.durationMs : 0, + error: finalResult && finalResult.ok ? '' : (finalResult && finalResult.error ? finalResult.error : ''), + url: finalCandidate && finalCandidate.url ? finalCandidate.url : '' + }; + break; + } result = await runSpeedTest(target.url, target.apiKey, target); break; } diff --git a/lib/cli-network-utils.js b/lib/cli-network-utils.js index 9ed96dc5..3e570203 100644 --- a/lib/cli-network-utils.js +++ b/lib/cli-network-utils.js @@ -73,8 +73,21 @@ async function probeUrl(targetUrl, options = {}) { 'User-Agent': 'codexmate-health-check', 'Accept': 'application/json' }; + if (options.headers && typeof options.headers === 'object') { + for (const [key, value] of Object.entries(options.headers)) { + if (value === undefined || value === null) continue; + headers[key] = String(value); + } + } if (options.apiKey) { - headers['Authorization'] = `Bearer ${options.apiKey}`; + const apiKeyHeader = typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader.trim() : ''; + if (apiKeyHeader) { + if (headers[apiKeyHeader] === undefined) { + headers[apiKeyHeader] = String(options.apiKey); + } + } else if (headers.Authorization === undefined) { + headers['Authorization'] = `Bearer ${options.apiKey}`; + } } const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0; @@ -124,8 +137,21 @@ async function probeJsonPost(targetUrl, body, options = {}) { 'Accept': 'application/json', 'Content-Type': 'application/json' }; + if (options.headers && typeof options.headers === 'object') { + for (const [key, value] of Object.entries(options.headers)) { + if (value === undefined || value === null) continue; + headers[key] = String(value); + } + } if (options.apiKey) { - headers['Authorization'] = `Bearer ${options.apiKey}`; + const apiKeyHeader = typeof options.apiKeyHeader === 'string' ? options.apiKeyHeader.trim() : ''; + if (apiKeyHeader) { + if (headers[apiKeyHeader] === undefined) { + headers[apiKeyHeader] = String(options.apiKey); + } + } else if (headers.Authorization === undefined) { + headers['Authorization'] = `Bearer ${options.apiKey}`; + } } const payload = JSON.stringify(body || {}); diff --git a/tests/unit/cli-network-utils.test.mjs b/tests/unit/cli-network-utils.test.mjs index e2d0fb92..7f25a9c1 100644 --- a/tests/unit/cli-network-utils.test.mjs +++ b/tests/unit/cli-network-utils.test.mjs @@ -61,3 +61,45 @@ test('probeJsonPost retries with family=4 after a retryable network timeout', as https.request = originalRequest; } }); + +test('probeJsonPost supports custom api key header and merges extra headers', async () => { + const originalRequest = https.request; + const seenHeaders = []; + + try { + https.request = (_parsed, options, callback) => { + seenHeaders.push(options.headers || {}); + const req = new EventEmitter(); + req.setTimeout = () => {}; + req.write = () => {}; + req.end = () => { + const res = new EventEmitter(); + res.statusCode = 200; + callback(res); + process.nextTick(() => { + res.emit('data', Buffer.from('{"ok":true}')); + res.emit('end'); + }); + }; + return req; + }; + + const result = await probeJsonPost('https://example.com/v1/messages', { ping: true }, { + apiKey: 'sk-demo', + apiKeyHeader: 'x-api-key', + headers: { + 'anthropic-version': '2023-06-01' + }, + timeoutMs: 1000, + maxBytes: 1024 + }); + + assert.strictEqual(result.ok, true); + assert.strictEqual(seenHeaders.length, 1); + assert.strictEqual(seenHeaders[0]['x-api-key'], 'sk-demo'); + assert.strictEqual(seenHeaders[0]['anthropic-version'], '2023-06-01'); + assert.strictEqual(seenHeaders[0].Authorization, undefined); + } finally { + https.request = originalRequest; + } +}); diff --git a/web-ui/modules/app.methods.runtime.mjs b/web-ui/modules/app.methods.runtime.mjs index 8a0e4198..b767153d 100644 --- a/web-ui/modules/app.methods.runtime.mjs +++ b/web-ui/modules/app.methods.runtime.mjs @@ -66,6 +66,8 @@ export function createRuntimeMethods(options = {}) { async runClaudeSpeedTest(name, config) { if (!name || this.claudeSpeedLoading[name]) return null; const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : ''; + const apiKey = config && typeof config.apiKey === 'string' ? config.apiKey.trim() : ''; + const model = config && typeof config.model === 'string' ? config.model.trim() : ''; this.claudeSpeedLoading[name] = true; try { if (!baseUrl) { @@ -73,7 +75,22 @@ export function createRuntimeMethods(options = {}) { this.claudeSpeedResults[name] = res; return res; } - const res = await api('speed-test', { url: baseUrl }); + if (!apiKey) { + const res = { ok: false, error: 'Missing API key' }; + this.claudeSpeedResults[name] = res; + return res; + } + if (!model) { + const res = { ok: false, error: 'Missing model' }; + this.claudeSpeedResults[name] = res; + return res; + } + const res = await api('speed-test', { + kind: 'claude', + url: baseUrl, + apiKey, + model + }); if (res.error) { this.claudeSpeedResults[name] = { ok: false, error: res.error }; return { ok: false, error: res.error }; From 337f556b1e949fbb8f0ac1c40c172b55112a0f90 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 08:48:07 +0000 Subject: [PATCH 02/21] fix(web-ui): improve health check error UX --- web-ui/modules/app.methods.codex-config.mjs | 51 +++++++++++++++++-- web-ui/modules/i18n.mjs | 14 +++++ web-ui/partials/index/modal-health-check.html | 5 +- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/web-ui/modules/app.methods.codex-config.mjs b/web-ui/modules/app.methods.codex-config.mjs index 3516e6aa..de4b18da 100644 --- a/web-ui/modules/app.methods.codex-config.mjs +++ b/web-ui/modules/app.methods.codex-config.mjs @@ -24,6 +24,45 @@ function getResponseMessage(response, fallback) { return fallback; } +function sanitizeHealthCheckDetail(detail) { + const text = String(detail || '').trim(); + if (!text) return ''; + const clipped = text.length > 1600 ? `${text.slice(0, 1600)}...` : text; + const scrubbed = clipped + .replace(/(?:[A-Za-z]:\\|\/)[^\s:]+(?:[\\/][^\s:]+)+(?=:\d+)/g, '') + .replace(/\s+/g, ' ') + .trim(); + return scrubbed; +} + +function formatHealthCheckErrorMessage(raw, t, lang) { + const detail = sanitizeHealthCheckDetail(raw); + const lower = detail.toLowerCase(); + const isEn = typeof lang === 'string' && lang.trim().toLowerCase() === 'en'; + const pick = (key, fallbackZh, fallbackEn) => (typeof t === 'function' + ? t(key) + : (isEn ? fallbackEn : fallbackZh)); + if (!detail) { + return { message: pick('modal.healthCheck.error.unknown', '请求失败:请检查 endpoint、网络与鉴权配置。', 'Request failed. Check endpoint, network, and auth settings.'), detail: '' }; + } + if (/handshake|sslv3|ssl routines|eproto/.test(lower)) { + return { message: pick('modal.healthCheck.error.handshake', 'TLS 握手失败:请检查 endpoint 是否支持 HTTPS、证书/协议是否兼容。', 'TLS handshake failed. Check endpoint HTTPS support and certificate/protocol compatibility.'), detail }; + } + if (/self signed|unable to verify|certificate|cert_|tls/.test(lower)) { + return { message: pick('modal.healthCheck.error.cert', 'TLS 证书校验失败:可能为自签名证书或证书链不完整。', 'TLS certificate validation failed. The certificate may be self-signed or incomplete.'), detail }; + } + if (/enotfound|eai_again|dns/.test(lower)) { + return { message: pick('modal.healthCheck.error.dns', 'DNS 解析失败:请检查域名与网络环境。', 'DNS lookup failed. Check the hostname and network.'), detail }; + } + if (/econnrefused|refused/.test(lower)) { + return { message: pick('modal.healthCheck.error.refused', '连接被拒绝:请检查端口是否开放或服务是否在运行。', 'Connection refused. Check whether the service/port is reachable.'), detail }; + } + if (/timeout|timed out|etimedout/.test(lower)) { + return { message: pick('modal.healthCheck.error.timeout', '连接超时:请检查网络或 endpoint 是否可达。', 'Request timed out. Check network connectivity or endpoint availability.'), detail }; + } + return { message: pick('modal.healthCheck.error.unknown', '请求失败:请检查 endpoint、网络与鉴权配置。', 'Request failed. Check endpoint, network, and auth settings.'), detail }; +} + export function createCodexConfigMethods(options = {}) { const { api, @@ -422,7 +461,9 @@ export function createCodexConfigMethods(options = {}) { this.healthCheckDialogLastResult = res; if (hasResponseError(res) || res.ok === false) { - const message = getResponseMessage(res, '健康聊天测试失败'); + const rawMessage = getResponseMessage(res, '健康聊天测试失败'); + const formatted = formatHealthCheckErrorMessage(rawMessage, this.t, this.lang); + const message = formatted.message || rawMessage; this.healthCheckDialogMessages.push({ id: `assistant-${Date.now()}`, role: 'assistant', @@ -431,7 +472,7 @@ export function createCodexConfigMethods(options = {}) { status: Number.isFinite(res && res.status) ? res.status : 0, durationMs: Number.isFinite(res && res.durationMs) ? res.durationMs : 0, model: typeof (res && res.model) === 'string' ? res.model : '', - rawPreview: typeof (res && res.rawPreview) === 'string' ? res.rawPreview : '' + rawPreview: formatted.detail || (typeof (res && res.rawPreview) === 'string' ? res.rawPreview : '') }); this.showMessage(message, 'error'); return; @@ -452,7 +493,9 @@ export function createCodexConfigMethods(options = {}) { }); this.healthCheckDialogPrompt = ''; } catch (e) { - const message = e && e.message ? e.message : '健康聊天测试失败'; + const rawMessage = e && e.message ? e.message : '健康聊天测试失败'; + const formatted = formatHealthCheckErrorMessage(rawMessage, this.t, this.lang); + const message = formatted.message || rawMessage; this.healthCheckDialogMessages.push({ id: `assistant-${Date.now()}`, role: 'assistant', @@ -461,7 +504,7 @@ export function createCodexConfigMethods(options = {}) { status: 0, durationMs: 0, model: '', - rawPreview: '' + rawPreview: formatted.detail }); this.healthCheckDialogLastResult = { ok: false, error: message }; this.showMessage(message, 'error'); diff --git a/web-ui/modules/i18n.mjs b/web-ui/modules/i18n.mjs index 3258e842..1a5a3129 100644 --- a/web-ui/modules/i18n.mjs +++ b/web-ui/modules/i18n.mjs @@ -281,7 +281,14 @@ const DICT = Object.freeze({ 'modal.healthCheck.result.fail': '本轮失败', 'modal.healthCheck.emptyHint': '发一句话,看是否正常回复。', 'modal.healthCheck.realApiHint': '真实接口,单轮发送,不带历史上下文。', + 'modal.healthCheck.details': '详情', 'modal.healthCheck.send': '发送测试', + 'modal.healthCheck.error.handshake': 'TLS 握手失败:请检查 endpoint 是否支持 HTTPS、证书/协议是否兼容。', + 'modal.healthCheck.error.cert': 'TLS 证书校验失败:可能为自签名证书或证书链不完整。', + 'modal.healthCheck.error.timeout': '连接超时:请检查网络或 endpoint 是否可达。', + 'modal.healthCheck.error.dns': 'DNS 解析失败:请检查域名与网络环境。', + 'modal.healthCheck.error.refused': '连接被拒绝:请检查端口是否开放或服务是否在运行。', + 'modal.healthCheck.error.unknown': '请求失败:请检查 endpoint、网络与鉴权配置。', // Basic modals 'modal.providerAdd.title': '添加提供商', @@ -1164,7 +1171,14 @@ const DICT = Object.freeze({ 'modal.healthCheck.result.fail': 'Failed', 'modal.healthCheck.emptyHint': 'Send a message and see if it replies.', 'modal.healthCheck.realApiHint': 'Real API call. Single turn, no history.', + 'modal.healthCheck.details': 'Details', 'modal.healthCheck.send': 'Send test', + 'modal.healthCheck.error.handshake': 'TLS handshake failed. Check endpoint HTTPS support and certificate/protocol compatibility.', + 'modal.healthCheck.error.cert': 'TLS certificate validation failed. The certificate may be self-signed or incomplete.', + 'modal.healthCheck.error.timeout': 'Request timed out. Check network connectivity or endpoint availability.', + 'modal.healthCheck.error.dns': 'DNS lookup failed. Check the hostname and network.', + 'modal.healthCheck.error.refused': 'Connection refused. Check whether the service/port is reachable.', + 'modal.healthCheck.error.unknown': 'Request failed. Check endpoint, network, and auth settings.', // Basic modals 'modal.providerAdd.title': 'Add provider', diff --git a/web-ui/partials/index/modal-health-check.html b/web-ui/partials/index/modal-health-check.html index 23d9bae3..d6239bb2 100644 --- a/web-ui/partials/index/modal-health-check.html +++ b/web-ui/partials/index/modal-health-check.html @@ -46,7 +46,10 @@ {{ item.durationMs }} ms
{{ item.text }}
-
{{ item.rawPreview }}
+
+ {{ t('modal.healthCheck.details') }} +
{{ item.rawPreview }}
+
From b53a3892d4219a8a085fd1afe9051361c3470118 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 15:05:02 +0000 Subject: [PATCH 03/21] perf(web-ui): cap health chat request timeout --- tests/unit/agents-modal-guards.test.mjs | 3 ++- web-ui/modules/app.methods.codex-config.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/agents-modal-guards.test.mjs b/tests/unit/agents-modal-guards.test.mjs index 736c7041..47d43b20 100644 --- a/tests/unit/agents-modal-guards.test.mjs +++ b/tests/unit/agents-modal-guards.test.mjs @@ -388,7 +388,8 @@ test('sendHealthCheckDialogMessage appends transcript and clears prompt after su action: 'provider-chat-check', params: { name: 'alpha', - prompt: 'say ok' + prompt: 'say ok', + timeoutMs: 10000 } }]); assert.strictEqual(context.healthCheckDialogPrompt, ''); diff --git a/web-ui/modules/app.methods.codex-config.mjs b/web-ui/modules/app.methods.codex-config.mjs index de4b18da..a11bb3f8 100644 --- a/web-ui/modules/app.methods.codex-config.mjs +++ b/web-ui/modules/app.methods.codex-config.mjs @@ -456,7 +456,8 @@ export function createCodexConfigMethods(options = {}) { try { const res = await api('provider-chat-check', { name: provider, - prompt + prompt, + timeoutMs: 10000 }); this.healthCheckDialogLastResult = res; From 32a4019bb89594a264896693304e61042e9d3e4f Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 15:48:31 +0000 Subject: [PATCH 04/21] perf(health): reduce provider chat fallback waits --- cli.js | 62 ++++++++++++++++++++++++++++------------------------------ 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/cli.js b/cli.js index 1cea1edf..81f520e8 100644 --- a/cli.js +++ b/cli.js @@ -6017,36 +6017,34 @@ function resolveSpeedTestTarget(params) { return { error: 'Missing model' }; } const normalizedBase = baseUrl.replace(/\/+$/, ''); + let parsed = null; + try { + parsed = new URL(normalizedBase); + } catch (_) { + return { error: 'Invalid URL' }; + } + const pathname = typeof parsed.pathname === 'string' ? parsed.pathname : ''; + const trimmedPath = pathname.replace(/\/+$/, ''); + const isRootPath = !trimmedPath || trimmedPath === '/'; + const endsWithV1 = trimmedPath.endsWith('/v1'); + const makeCandidate = (url) => ({ + method: 'POST', + url, + body: { + model, + max_tokens: 16, + messages: [{ role: 'user', content: 'ping' }] + } + }); const candidates = []; - if (normalizedBase.endsWith('/v1')) { - candidates.push({ - method: 'POST', - url: `${normalizedBase}/messages`, - body: { - model, - max_tokens: 16, - messages: [{ role: 'user', content: 'ping' }] - } - }); + if (endsWithV1) { + candidates.push(makeCandidate(`${normalizedBase}/messages`)); + } else if (isRootPath) { + candidates.push(makeCandidate(`${normalizedBase}/v1/messages`)); + candidates.push(makeCandidate(`${normalizedBase}/messages`)); } else { - candidates.push({ - method: 'POST', - url: `${normalizedBase}/v1/messages`, - body: { - model, - max_tokens: 16, - messages: [{ role: 'user', content: 'ping' }] - } - }); - candidates.push({ - method: 'POST', - url: `${normalizedBase}/messages`, - body: { - model, - max_tokens: 16, - messages: [{ role: 'user', content: 'ping' }] - } - }); + candidates.push(makeCandidate(`${normalizedBase}/messages`)); + candidates.push(makeCandidate(`${normalizedBase}/v1/messages`)); } return { kind: 'claude', @@ -6189,8 +6187,8 @@ async function runProviderChatCheck(params = {}) { }); finalSpec = candidate; result = probeResult; - const shouldTryNextCandidate = index < target.specs.length - 1 - && (!probeResult.ok || probeResult.status === 404); + const status = Number.isFinite(probeResult && probeResult.status) ? probeResult.status : 0; + const shouldTryNextCandidate = index < target.specs.length - 1 && status === 404; if (!shouldTryNextCandidate) { break; } @@ -8518,8 +8516,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }); finalCandidate = candidate; finalResult = probeResult; - const shouldTryNext = index < target.candidates.length - 1 - && (!probeResult.ok || probeResult.status === 404); + const status = Number.isFinite(probeResult && probeResult.status) ? probeResult.status : 0; + const shouldTryNext = index < target.candidates.length - 1 && status === 404; if (!shouldTryNext) { break; } From 1ea0428907e7c22ce067ea6c39c37b99cbb3c0b4 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 16:10:13 +0000 Subject: [PATCH 05/21] feat(codex): per-provider availability test --- cli.js | 40 +++++++++++++------ tests/unit/config-tabs-ui.test.mjs | 4 ++ web-ui/modules/i18n.mjs | 4 ++ web-ui/partials/index/panel-config-codex.html | 12 ++++++ 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/cli.js b/cli.js index 81f520e8..cca81af8 100644 --- a/cli.js +++ b/cli.js @@ -42,7 +42,9 @@ const { extractModelNames, hasModelsListPayload, buildModelsCacheKey, + buildApiProbeUrlCandidates, buildModelProbeSpec, + buildModelProbeSpecs, buildModelConversationSpecs, extractModelResponseText, normalizeWireApi, @@ -6067,20 +6069,32 @@ function resolveSpeedTestTarget(params) { if (!provider.base_url) { return { error: 'Provider missing URL' }; } - const currentModel = typeof config.model === 'string' ? config.model.trim() : ''; - const probeSpec = buildModelProbeSpec(provider, currentModel, provider.base_url); - if (probeSpec && probeSpec.url) { - return { - method: 'POST', - url: probeSpec.url, - body: probeSpec.body, - apiKey: provider.preferred_auth_method || '' - }; + const providerName = String(params.name).trim(); + const currentModels = readCurrentModels(); + const selectedModel = typeof currentModels[providerName] === 'string' && currentModels[providerName].trim() + ? currentModels[providerName].trim() + : (typeof config.model === 'string' ? config.model.trim() : ''); + + const apiKey = typeof provider.preferred_auth_method === 'string' + ? provider.preferred_auth_method.trim() + : ''; + + const candidates = []; + for (const url of buildApiProbeUrlCandidates(provider.base_url, 'models')) { + candidates.push({ method: 'GET', url }); + } + for (const spec of buildModelProbeSpecs(provider, selectedModel, provider.base_url)) { + if (!spec || !spec.url) continue; + candidates.push({ method: 'POST', url: spec.url, body: spec.body }); + } + if (candidates.length === 0) { + candidates.push({ method: 'GET', url: provider.base_url }); } + return { - method: 'GET', - url: provider.base_url, - apiKey: provider.preferred_auth_method || '' + kind: 'provider', + candidates, + apiKey }; } @@ -8504,7 +8518,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser result = { error: target.error }; break; } - if (target.kind === 'claude' && Array.isArray(target.candidates) && target.candidates.length > 0) { + if (Array.isArray(target.candidates) && target.candidates.length > 0) { let finalCandidate = target.candidates[0]; let finalResult = null; for (let index = 0; index < target.candidates.length; index += 1) { diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 61c18491..4b4c7884 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -322,6 +322,10 @@ test('config template keeps expected config tabs in top and side navigation', () html, //); assert.match(html, //); assert.match(html, / +
{{ t('config.health.hint') }}
+
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }} +
+
+ {{ (healthCheckResult.issues || [])[0].message }} +
+ +
From d9492baf92ef7c2cc8b5653879cfb54440475af9 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 16:30:49 +0000 Subject: [PATCH 07/21] perf(codex): show batch health progress and cap probe time --- cli.js | 11 ++- tests/unit/web-ui-behavior-parity.test.mjs | 3 + web-ui/app.js | 3 + web-ui/modules/app.methods.codex-config.mjs | 77 ++++++++++++------- web-ui/modules/app.methods.runtime.mjs | 7 +- web-ui/modules/i18n.mjs | 2 + web-ui/partials/index/panel-config-codex.html | 3 + 7 files changed, 74 insertions(+), 32 deletions(-) diff --git a/cli.js b/cli.js index cca81af8..d33b3160 100644 --- a/cli.js +++ b/cli.js @@ -8518,6 +8518,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser result = { error: target.error }; break; } + const timeoutMs = Number.isFinite(params && params.timeoutMs) + ? Math.max(1000, Number(params.timeoutMs)) + : 0; if (Array.isArray(target.candidates) && target.candidates.length > 0) { let finalCandidate = target.candidates[0]; let finalResult = null; @@ -8526,7 +8529,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser const probeResult = await runSpeedTest(candidate.url, target.apiKey, { ...candidate, apiKeyHeader: target.apiKeyHeader, - headers: target.headers + headers: target.headers, + timeoutMs: timeoutMs || undefined }); finalCandidate = candidate; finalResult = probeResult; @@ -8545,7 +8549,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }; break; } - result = await runSpeedTest(target.url, target.apiKey, target); + result = await runSpeedTest(target.url, target.apiKey, { + ...target, + timeoutMs: timeoutMs || undefined + }); break; } case 'provider-chat-check': { diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 4ddbcb7a..bfbd91b1 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -374,6 +374,9 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'configTemplateDiffFingerprint', '_configTemplateDiffPreviewRequestToken', 'configTemplateDiffConfirmEnabled', + 'healthCheckBatchTotal', + 'healthCheckBatchDone', + 'healthCheckBatchFailed', 'pluginsActiveId', 'pluginsLoading', 'pluginsError', diff --git a/web-ui/app.js b/web-ui/app.js index bf7957e4..26e921dd 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -245,6 +245,9 @@ document.addEventListener('DOMContentLoaded', () => { providerSwitchInProgress: false, pendingProviderSwitch: '', providerSwitchDisplayTarget: '', + healthCheckBatchTotal: 0, + healthCheckBatchDone: 0, + healthCheckBatchFailed: 0, healthCheckDialogLockedProvider: '', healthCheckDialogSelectedProvider: '', healthCheckDialogPrompt: '请简短回复:连接正常。', diff --git a/web-ui/modules/app.methods.codex-config.mjs b/web-ui/modules/app.methods.codex-config.mjs index a11bb3f8..0c76689d 100644 --- a/web-ui/modules/app.methods.codex-config.mjs +++ b/web-ui/modules/app.methods.codex-config.mjs @@ -313,11 +313,50 @@ export function createCodexConfigMethods(options = {}) { async runHealthCheck() { this.healthCheckLoading = true; this.healthCheckResult = null; + this.healthCheckBatchTotal = 0; + this.healthCheckBatchDone = 0; + this.healthCheckBatchFailed = 0; let shouldRunClaudeSpeedTests = false; try { - const res = await api('config-health-check', { - remote: this.configMode === 'codex' - }); + const shouldRunSpeedTests = this.configMode === 'codex'; + const speedTimeoutMs = shouldRunSpeedTests ? 3500 : 0; + const providers = shouldRunSpeedTests + ? (this.providersList || []) + .map((provider) => typeof provider === 'string' + ? provider.trim() + : String((provider && provider.name) || '').trim()) + .filter(Boolean) + : []; + const currentProvider = String(this.currentProvider || '').trim(); + const orderedProviders = currentProvider && providers.includes(currentProvider) + ? [currentProvider, ...providers.filter((name) => name !== currentProvider)] + : providers; + this.healthCheckBatchTotal = orderedProviders.length; + + const speedTasks = orderedProviders.map((provider) => this.runSpeedTest(provider, { silent: true, timeoutMs: speedTimeoutMs }) + .then((result) => { + if (!result || result.ok !== true) { + this.healthCheckBatchFailed += 1; + } + return { name: provider, result }; + }) + .catch((err) => { + this.healthCheckBatchFailed += 1; + return { + name: provider, + result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' } + }; + }) + .finally(() => { + this.healthCheckBatchDone += 1; + }) + ); + + const configTask = api('config-health-check', { remote: this.configMode === 'codex' }); + const [res, pairs] = await Promise.all([ + configTask, + Promise.all(speedTasks) + ]); if (hasResponseError(res)) { this.healthCheckResult = null; this.showMessage(getResponseMessage(res, '检查失败'), 'error'); @@ -325,38 +364,16 @@ export function createCodexConfigMethods(options = {}) { shouldRunClaudeSpeedTests = true; const issues = Array.isArray(res.issues) ? [...res.issues] : []; let remote = res.remote || null; - { - const providers = (this.providersList || []) - .map((provider) => typeof provider === 'string' - ? provider.trim() - : String((provider && provider.name) || '').trim()) - .filter(Boolean); - const tasks = providers.map(provider => - this.runSpeedTest(provider, { silent: true }) - .then(result => ({ name: provider, result })) - .catch(err => ({ - name: provider, - result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' } - })) - ); - const pairs = await Promise.all(tasks); + if (shouldRunSpeedTests) { const results = {}; for (const pair of pairs) { results[pair.name] = pair.result || null; const issue = this.buildSpeedTestIssue(pair.name, pair.result); if (issue) issues.push(issue); } - if (remote && typeof remote === 'object') { - remote = { - ...remote, - speedTests: results - }; - } else { - remote = { - type: 'speed-test', - speedTests: results - }; - } + remote = remote && typeof remote === 'object' + ? { ...remote, speedTests: results } + : { type: 'speed-test', speedTests: results }; } const ok = issues.length === 0; @@ -377,6 +394,8 @@ export function createCodexConfigMethods(options = {}) { this.healthCheckResult = null; this.showMessage('检查失败', 'error'); } finally { + this.healthCheckBatchTotal = this.healthCheckBatchTotal || 0; + this.healthCheckBatchDone = Math.min(this.healthCheckBatchDone || 0, this.healthCheckBatchTotal || 0); if (shouldRunClaudeSpeedTests && this.configMode === 'claude') { try { const entries = Object.entries(this.claudeConfigs || {}); diff --git a/web-ui/modules/app.methods.runtime.mjs b/web-ui/modules/app.methods.runtime.mjs index b767153d..8ebdee5e 100644 --- a/web-ui/modules/app.methods.runtime.mjs +++ b/web-ui/modules/app.methods.runtime.mjs @@ -37,7 +37,12 @@ export function createRuntimeMethods(options = {}) { const silent = !!options.silent; this.speedLoading[name] = true; try { - const res = await api('speed-test', { name }); + const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 0; + const payload = { name }; + if (timeoutMs) { + payload.timeoutMs = timeoutMs; + } + const res = await api('speed-test', payload); if (res.error) { this.speedResults[name] = { ok: false, error: res.error }; if (!silent) { diff --git a/web-ui/modules/i18n.mjs b/web-ui/modules/i18n.mjs index e72d31cc..b0930800 100644 --- a/web-ui/modules/i18n.mjs +++ b/web-ui/modules/i18n.mjs @@ -606,6 +606,7 @@ const DICT = Object.freeze({ 'config.health.run': '运行检查', 'config.health.running': '检查中...', 'config.health.hint': '会批量探测所有提供商可用性,并刷新延迟显示。', + 'config.health.progress': '已完成 {done}/{total} · 失败 {failed}', 'config.health.ok': '检查通过', 'config.health.fail': '检查未通过', 'config.health.issues': '{count} 项问题', @@ -1505,6 +1506,7 @@ const DICT = Object.freeze({ 'config.health.run': 'Run check', 'config.health.running': 'Checking...', 'config.health.hint': 'Runs availability probes across all providers and refreshes latency badges.', + 'config.health.progress': '{done}/{total} done · {failed} failed', 'config.health.ok': 'Passed', 'config.health.fail': 'Failed', 'config.health.issues': '{count} issues', diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index b2cfac96..039eec37 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -171,6 +171,9 @@ {{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}
{{ t('config.health.hint') }}
+
+ {{ t('config.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }} +
{{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }}
From d52bdf88b239c4a5c4fb0dd26124f0db61633c9a Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 16:33:44 +0000 Subject: [PATCH 08/21] chore(cli): rename dev hint label --- cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.js b/cli.js index d33b3160..5f953227 100644 --- a/cli.js +++ b/cli.js @@ -9019,7 +9019,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser const openUrl = `http://${formatHostForUrl(openHost)}:${port}`; server.listen(port, host, () => { console.log('\n✓ Web UI 已启动'); - console.log(` 待访问: ${openUrl}`); + console.log(` 已打开: ${openUrl}`); if (host && host !== openHost) { console.log(' 监听地址:', host); } From 10f38c72a9a16a8f0cf623af5fb78916df17c897 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 16:46:28 +0000 Subject: [PATCH 09/21] feat(cli): auto-open web ui in run --- cli.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cli.js b/cli.js index 5f953227..f930d565 100644 --- a/cli.js +++ b/cli.js @@ -9019,7 +9019,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser const openUrl = `http://${formatHostForUrl(openHost)}:${port}`; server.listen(port, host, () => { console.log('\n✓ Web UI 已启动'); - console.log(` 已打开: ${openUrl}`); + const willOpenBrowser = !!openBrowser && !process.env.CODEXMATE_NO_BROWSER; + console.log(` ${willOpenBrowser ? '已打开' : '待访问'}: ${openUrl}`); if (host && host !== openHost) { console.log(' 监听地址:', host); } @@ -9029,9 +9030,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。'); } - if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) { - const url = openUrl; - openBrowserAfterReady(url); + if (willOpenBrowser) { + openBrowserAfterReady(openUrl); } }); @@ -9142,14 +9142,15 @@ function cmdStart(options = {}) { || process.env.CODEXMATE_DEV === '1' || process.env.CODEXMATE_DEV === 'true'; - // 禁止自动打开浏览器:仅输出 URL,交由用户自行点击/打开。 + const shouldOpenBrowser = !options.noBrowser && !process.env.CODEXMATE_NO_BROWSER; + let serverHandle = createWebServer({ htmlPath, assetsDir, webDir, host, port, - openBrowser: false + openBrowser: shouldOpenBrowser }); // 禁止前端变更侦测与自动重启:避免终端输出噪音与访问时短暂 Connection Refused。 From c15d284aea7fb4f8a4217df2783b1692476dc13c Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 16:54:14 +0000 Subject: [PATCH 10/21] ui(config): move test latency before configured pill --- tests/unit/config-tabs-ui.test.mjs | 8 ++++++++ web-ui/partials/index/panel-config-claude.html | 6 +++--- web-ui/partials/index/panel-config-codex.html | 6 +++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index a13134f8..3f58775c 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -268,10 +268,18 @@ test('config template keeps expected config tabs in top and side navigation', () html, /:class="\['card', \{ active: displayCurrentProvider === provider\.name \}\]"[\s\S]*@click="switchProvider\(provider\.name\)"[\s\S]*@keydown\.enter\.self\.prevent="switchProvider\(provider\.name\)"[\s\S]*@keydown\.space\.self\.prevent="switchProvider\(provider\.name\)"[\s\S]*tabindex="0"[\s\S]*role="button"[\s\S]*:aria-current="displayCurrentProvider === provider\.name \? 'true' : null"/ ); + assert.match( + html, + /\s*\{\{\s*formatLatency\(speedResults\[provider\.name\]\)\s*\}\}\s*<\/span>[\s\S]*/ + ); assert.match( html, /:class="\['card', \{ active: currentClaudeConfig === name \}\]"[\s\S]*@click="applyClaudeConfig\(name\)"[\s\S]*@keydown\.enter\.self\.prevent="applyClaudeConfig\(name\)"[\s\S]*@keydown\.space\.self\.prevent="applyClaudeConfig\(name\)"[\s\S]*tabindex="0"[\s\S]*role="button"[\s\S]*:aria-current="currentClaudeConfig === name \? 'true' : null"/ ); + assert.match( + html, + /\s*\{\{\s*formatLatency\(claudeSpeedResults\[name\]\)\s*\}\}\s*<\/span>[\s\S]*/ + ); assert.match( html, /:class="\['card', \{ active: currentOpenclawConfig === name \}\]"[\s\S]*@click="applyOpenclawConfig\(name\)"[\s\S]*@keydown\.enter\.self\.prevent="applyOpenclawConfig\(name\)"[\s\S]*@keydown\.space\.self\.prevent="applyOpenclawConfig\(name\)"[\s\S]*tabindex="0"[\s\S]*role="button"[\s\S]*:aria-current="currentOpenclawConfig === name \? 'true' : null"/ diff --git a/web-ui/partials/index/panel-config-claude.html b/web-ui/partials/index/panel-config-claude.html index ea592454..6dfdf6d6 100644 --- a/web-ui/partials/index/panel-config-claude.html +++ b/web-ui/partials/index/panel-config-claude.html @@ -125,12 +125,12 @@
- - {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }} - {{ formatLatency(claudeSpeedResults[name]) }} + + {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }} +
- - {{ providerPillText(provider) }} - {{ formatLatency(speedResults[provider.name]) }} + + {{ providerPillText(provider) }} +
From 0102d8583a801332bc06ad4050224839f29d37f2 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Thu, 23 Apr 2026 17:42:20 +0000 Subject: [PATCH 12/21] refactor(health): remove chat check ui and api --- cli.js | 154 ---------------- lib/cli-models-utils.js | 40 ----- tests/e2e/helpers.js | 2 - tests/e2e/test-health-speed.js | 19 -- tests/unit/agents-modal-guards.test.mjs | 87 --------- tests/unit/config-tabs-ui.test.mjs | 9 - tests/unit/provider-chat-utils.test.mjs | 43 ----- tests/unit/web-ui-behavior-parity.test.mjs | 15 +- web-ui/app.js | 7 - web-ui/index.html | 1 - web-ui/modules/app.methods.codex-config.mjs | 166 ------------------ web-ui/modules/i18n.mjs | 40 ----- web-ui/partials/index/modal-health-check.html | 75 -------- web-ui/partials/index/panel-config-codex.html | 13 -- 14 files changed, 13 insertions(+), 658 deletions(-) delete mode 100644 tests/unit/provider-chat-utils.test.mjs delete mode 100644 web-ui/partials/index/modal-health-check.html diff --git a/cli.js b/cli.js index f930d565..4f8bd0b0 100644 --- a/cli.js +++ b/cli.js @@ -45,7 +45,6 @@ const { buildApiProbeUrlCandidates, buildModelProbeSpec, buildModelProbeSpecs, - buildModelConversationSpecs, extractModelResponseText, normalizeWireApi, getSupplementalModelsForBaseUrl, @@ -6109,155 +6108,6 @@ function resolveSpeedTestTarget(params) { return { error: 'Missing name or url' }; } -function extractApiPayloadErrorMessage(payload) { - if (!payload || typeof payload !== 'object') { - return ''; - } - if (typeof payload.error === 'string' && payload.error.trim()) { - return payload.error.trim(); - } - if (!payload.error || typeof payload.error !== 'object') { - return ''; - } - if (typeof payload.error.message === 'string' && payload.error.message.trim()) { - return payload.error.message.trim(); - } - if (typeof payload.error.code === 'string' && payload.error.code.trim()) { - return payload.error.code.trim(); - } - return ''; -} - -function resolveProviderChatTarget(params) { - const providerName = typeof (params && params.name) === 'string' ? params.name.trim() : ''; - const prompt = typeof (params && params.prompt) === 'string' ? params.prompt.trim() : ''; - if (!providerName) { - return { error: 'Provider name is required' }; - } - if (!prompt) { - return { error: 'Prompt is required' }; - } - - const { config } = readConfigOrVirtualDefault(); - const providers = config.model_providers || {}; - const provider = providers[providerName]; - if (!provider || typeof provider !== 'object') { - return { error: `Provider not found: ${providerName}` }; - } - - const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : ''; - if (!baseUrl) { - return { error: `Provider ${providerName} missing URL` }; - } - - const currentModels = readCurrentModels(); - const savedModel = currentModels && typeof currentModels[providerName] === 'string' - ? currentModels[providerName].trim() - : ''; - const activeProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : ''; - const activeModel = typeof config.model === 'string' ? config.model.trim() : ''; - const model = savedModel || (activeProvider === providerName ? activeModel : ''); - if (!model) { - return { error: `Provider ${providerName} missing current model` }; - } - - const specs = buildModelConversationSpecs(provider, model, baseUrl, prompt, { - maxOutputTokens: 256 - }); - if (!specs.length) { - return { error: `Provider ${providerName} missing available conversation endpoint` }; - } - - return { - providerName, - provider, - model, - prompt, - specs, - apiKey: typeof provider.preferred_auth_method === 'string' - ? provider.preferred_auth_method.trim() - : '' - }; -} - -async function runProviderChatCheck(params = {}) { - const target = resolveProviderChatTarget(params); - if (target.error) { - return { ok: false, error: target.error }; - } - - const timeoutMs = Number.isFinite(params.timeoutMs) - ? Math.max(1000, Number(params.timeoutMs)) - : 30000; - let finalSpec = target.specs[0]; - let result = null; - - for (let index = 0; index < target.specs.length; index += 1) { - const candidate = target.specs[index]; - const probeResult = await probeJsonPost(candidate.url, candidate.body, { - apiKey: target.apiKey, - timeoutMs, - maxBytes: 512 * 1024 - }); - finalSpec = candidate; - result = probeResult; - const status = Number.isFinite(probeResult && probeResult.status) ? probeResult.status : 0; - const shouldTryNextCandidate = index < target.specs.length - 1 && status === 404; - if (!shouldTryNextCandidate) { - break; - } - } - - if (!result || !result.ok) { - return { - ok: false, - provider: target.providerName, - model: target.model, - url: finalSpec.url, - status: Number.isFinite(result && result.status) ? result.status : 0, - durationMs: Number.isFinite(result && result.durationMs) ? result.durationMs : 0, - reply: '', - rawPreview: '', - error: result && result.error ? result.error : 'request failed' - }; - } - - let payload = null; - try { - payload = result.body ? JSON.parse(result.body) : null; - } catch (e) { - payload = null; - } - - const payloadError = extractApiPayloadErrorMessage(payload); - if (result.status >= 400 || payloadError) { - return { - ok: false, - provider: target.providerName, - model: target.model, - url: finalSpec.url, - status: Number.isFinite(result.status) ? result.status : 0, - durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0, - reply: '', - rawPreview: result.body ? truncateText(result.body, 600) : '', - error: payloadError || `HTTP ${result.status}` - }; - } - - const reply = extractModelResponseText(payload); - return { - ok: true, - provider: target.providerName, - model: target.model, - url: finalSpec.url, - status: Number.isFinite(result.status) ? result.status : 0, - durationMs: Number.isFinite(result.durationMs) ? result.durationMs : 0, - reply, - rawPreview: reply ? '' : (result.body ? truncateText(result.body, 600) : ''), - error: '' - }; -} - function runSpeedTest(targetUrl, apiKey, options = {}) { const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) @@ -8555,10 +8405,6 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }); break; } - case 'provider-chat-check': { - result = await runProviderChatCheck(params || {}); - break; - } case 'openai-bridge-get-provider': { const name = params && typeof params.name === 'string' ? params.name.trim() : ''; if (!name) { diff --git a/lib/cli-models-utils.js b/lib/cli-models-utils.js index fb782d6a..deaa327c 100644 --- a/lib/cli-models-utils.js +++ b/lib/cli-models-utils.js @@ -271,45 +271,6 @@ function buildModelProbeSpec(provider, modelName, baseUrl) { return buildModelProbeSpecs(provider, modelName, baseUrl)[0] || null; } -function buildModelConversationSpecs(provider, modelName, baseUrl, prompt, options = {}) { - const model = typeof modelName === 'string' ? modelName.trim() : ''; - const userPrompt = typeof prompt === 'string' ? prompt.trim() : ''; - if (!model || !userPrompt) return []; - - const wireApi = normalizeWireApi(provider && provider.wire_api); - const maxOutputTokens = Number.isFinite(options.maxOutputTokens) - ? Math.max(1, Number(options.maxOutputTokens)) - : 256; - let pathSuffix = 'responses'; - let body = { - model, - input: userPrompt, - max_output_tokens: maxOutputTokens - }; - - if (wireApi === 'chat_completions' || wireApi === 'chat') { - pathSuffix = 'chat/completions'; - body = { - model, - messages: [{ role: 'user', content: userPrompt }], - max_tokens: maxOutputTokens - }; - } else if (wireApi === 'completions') { - pathSuffix = 'completions'; - body = { - model, - prompt: userPrompt, - max_tokens: maxOutputTokens - }; - } - - return buildApiProbeUrlCandidates(baseUrl, pathSuffix).map((url) => ({ - url, - body, - wireApi - })); -} - function collectStructuredText(content, pieces) { if (typeof content === 'string') { const text = content.trim(); @@ -410,7 +371,6 @@ module.exports = { buildModelsProbeUrl, buildModelProbeSpecs, buildModelProbeSpec, - buildModelConversationSpecs, extractModelResponseText, hashModelsCacheValue, buildModelsCacheKey, diff --git a/tests/e2e/helpers.js b/tests/e2e/helpers.js index 1602b7ee..fccbcaa3 100644 --- a/tests/e2e/helpers.js +++ b/tests/e2e/helpers.js @@ -7,7 +7,6 @@ const { writeJsonAtomic } = require('../../lib/cli-file-utils'); const { normalizeWireApi, buildModelProbeSpec, - buildModelConversationSpecs, extractModelResponseText } = require('../../lib/cli-models-utils'); @@ -248,6 +247,5 @@ module.exports = { writeJsonAtomic, normalizeWireApi, buildModelProbeSpec, - buildModelConversationSpecs, extractModelResponseText }; diff --git a/tests/e2e/test-health-speed.js b/tests/e2e/test-health-speed.js index aa699173..0c429664 100644 --- a/tests/e2e/test-health-speed.js +++ b/tests/e2e/test-health-speed.js @@ -3,7 +3,6 @@ const { assert, normalizeWireApi, buildModelProbeSpec, - buildModelConversationSpecs, extractModelResponseText, fileMode, writeJsonAtomic @@ -156,24 +155,6 @@ module.exports = async function testHealthAndSpeed(ctx) { 'speed-test(provider) should not inject /v1 before direct routed responses' ); - const conversationSpecs = buildModelConversationSpecs({ wire_api: 'responses' }, 'e2e-routed', routedProviderUrl, 'hello'); - assert(Array.isArray(conversationSpecs) && conversationSpecs.length > 0, 'buildModelConversationSpecs should build candidate endpoints'); - assert( - conversationSpecs[0].url === `${routedProviderUrl}/responses`, - 'buildModelConversationSpecs should keep direct provider routes ahead of /v1 fallback' - ); - - const conversationResult = await api('provider-chat-check', { - name: 'routed', - prompt: '请回复连接正常' - }, 5000); - assert(conversationResult.ok === true, 'provider-chat-check should succeed'); - assert(conversationResult.reply === 'routed provider is healthy', 'provider-chat-check should parse assistant text'); - assert( - routedProviderRequests.includes('/project/ym/responses'), - 'provider-chat-check should hit the direct routed responses endpoint' - ); - const parsedText = extractModelResponseText({ choices: [{ message: { diff --git a/tests/unit/agents-modal-guards.test.mjs b/tests/unit/agents-modal-guards.test.mjs index 47d43b20..776c784a 100644 --- a/tests/unit/agents-modal-guards.test.mjs +++ b/tests/unit/agents-modal-guards.test.mjs @@ -312,93 +312,6 @@ test('runHealthCheck preserves backend remote health result while appending spee }); }); -test('openHealthCheckDialog opens unlocked selector by default and locks when provider is specified', () => { - const methods = createCodexConfigMethods({ - api: async () => ({}), - getProviderConfigModeMeta() { - return null; - } - }); - const context = { - ...methods, - currentProvider: 'alpha', - displayProvidersList: [{ name: 'alpha' }, { name: 'beta' }], - showHealthCheckDialog: false, - healthCheckDialogLockedProvider: '', - healthCheckDialogSelectedProvider: '', - healthCheckDialogPrompt: '', - healthCheckDialogMessages: [{ id: 'stale' }], - healthCheckDialogLastResult: { ok: false }, - shownMessages: [], - showMessage(message, type) { - this.shownMessages.push({ message, type }); - } - }; - - methods.openHealthCheckDialog.call(context); - assert.strictEqual(context.showHealthCheckDialog, true); - assert.strictEqual(context.healthCheckDialogLockedProvider, ''); - assert.strictEqual(context.healthCheckDialogSelectedProvider, 'alpha'); - assert.deepStrictEqual(context.healthCheckDialogMessages, []); - - methods.openHealthCheckDialog.call(context, { providerName: 'beta', locked: true }); - assert.strictEqual(context.healthCheckDialogLockedProvider, ''); - assert.strictEqual(context.healthCheckDialogSelectedProvider, 'alpha'); - assert.deepStrictEqual(context.shownMessages, [{ - message: '请先切换到该提供商再进行健康聊天测试', - type: 'info' - }]); -}); - -test('sendHealthCheckDialogMessage appends transcript and clears prompt after success', async () => { - const apiCalls = []; - const methods = createCodexConfigMethods({ - api: async (action, params) => { - apiCalls.push({ action, params }); - return { - ok: true, - provider: params.name, - model: 'alpha-model', - status: 200, - durationMs: 12, - reply: 'provider is healthy' - }; - }, - getProviderConfigModeMeta() { - return null; - } - }); - const context = { - ...methods, - healthCheckDialogLockedProvider: '', - healthCheckDialogSelectedProvider: 'alpha', - healthCheckDialogPrompt: 'say ok', - healthCheckDialogMessages: [], - healthCheckDialogSending: false, - healthCheckDialogLastResult: null, - shownMessages: [], - showMessage(message, type) { - this.shownMessages.push({ message, type }); - } - }; - - await methods.sendHealthCheckDialogMessage.call(context); - - assert.deepStrictEqual(apiCalls, [{ - action: 'provider-chat-check', - params: { - name: 'alpha', - prompt: 'say ok', - timeoutMs: 10000 - } - }]); - assert.strictEqual(context.healthCheckDialogPrompt, ''); - assert.strictEqual(context.healthCheckDialogSending, false); - assert.strictEqual(context.healthCheckDialogMessages.length, 2); - assert.strictEqual(context.healthCheckDialogMessages[0].role, 'user'); - assert.strictEqual(context.healthCheckDialogMessages[1].text, 'provider is healthy'); -}); - test('applyCodexConfigDirect keeps the successful apply result when only the refresh fails', async () => { const apiCalls = []; const methods = createCodexConfigMethods({ diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index 3f58775c..91f5946e 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -9,7 +9,6 @@ import { test('config template keeps expected config tabs in top and side navigation', () => { const html = readBundledWebUiHtml(); const modalsBasic = readProjectFile('web-ui/partials/index/modals-basic.html'); - const healthCheckModal = readProjectFile('web-ui/partials/index/modal-health-check.html'); const templateAgentModals = readProjectFile('web-ui/partials/index/modal-config-template-agents.html'); const openclawModal = readProjectFile('web-ui/partials/index/modal-openclaw-config.html'); const sessionsPanel = readProjectFile('web-ui/partials/index/panel-sessions.html'); @@ -326,10 +325,6 @@ test('config template keeps expected config tabs in top and side navigation', () assert.doesNotMatch(html, /local 本地端口<\/span>/); assert.match(html, / - -
- - diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index e154d54f..de4207ad 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -219,19 +219,6 @@ - - -
CLAUDE.md @@ -108,6 +99,15 @@
+
+
+ {{ t('claude.health.title') }} +
+ +
+
Date: Fri, 24 Apr 2026 02:42:54 +0000 Subject: [PATCH 16/21] ui(health): align Claude health check with Codex --- tests/unit/agents-modal-guards.test.mjs | 12 +--- web-ui/modules/app.methods.codex-config.mjs | 56 ++++++++++++++++--- web-ui/modules/i18n.mjs | 4 ++ .../partials/index/panel-config-claude.html | 4 ++ 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/tests/unit/agents-modal-guards.test.mjs b/tests/unit/agents-modal-guards.test.mjs index 776c784a..f9ee3ed5 100644 --- a/tests/unit/agents-modal-guards.test.mjs +++ b/tests/unit/agents-modal-guards.test.mjs @@ -244,23 +244,17 @@ test('runHealthCheck skips Claude speed tests when the primary health check alre showMessage(message, type) { this.shownMessages.push({ message, type }); }, - async runSpeedTest() { - throw new Error('speed tests should be skipped when health check already failed'); - }, async runClaudeSpeedTest() { claudeSpeedTestCalls += 1; + return { ok: false, error: 'timeout' }; } }; await methods.runHealthCheck.call(context); assert.strictEqual(context.healthCheckLoading, false); - assert.strictEqual(context.healthCheckResult, null); - assert.strictEqual(claudeSpeedTestCalls, 0); - assert.deepStrictEqual(context.shownMessages, [{ - message: 'health failed', - type: 'error' - }]); + assert.strictEqual(claudeSpeedTestCalls, 1); + assert.strictEqual(context.healthCheckResult.ok, false); }); test('runHealthCheck preserves backend remote health result while appending speed test summaries', async () => { diff --git a/web-ui/modules/app.methods.codex-config.mjs b/web-ui/modules/app.methods.codex-config.mjs index 72ad0b93..8d7c5033 100644 --- a/web-ui/modules/app.methods.codex-config.mjs +++ b/web-ui/modules/app.methods.codex-config.mjs @@ -277,8 +277,55 @@ export function createCodexConfigMethods(options = {}) { this.healthCheckBatchTotal = 0; this.healthCheckBatchDone = 0; this.healthCheckBatchFailed = 0; - let shouldRunClaudeSpeedTests = false; try { + if (this.configMode === 'claude') { + const entries = Object.entries(this.claudeConfigs || {}); + this.healthCheckBatchTotal = entries.length; + + const speedTasks = entries.map(([name, config]) => this.runClaudeSpeedTest(name, config) + .then((result) => { + if (!result || result.ok !== true) { + this.healthCheckBatchFailed += 1; + } + return { name, result }; + }) + .catch((err) => { + this.healthCheckBatchFailed += 1; + return { + name, + result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' } + }; + }) + .finally(() => { + this.healthCheckBatchDone += 1; + }) + ); + + const pairs = await Promise.all(speedTasks); + const results = {}; + const issues = []; + for (const pair of pairs) { + results[pair.name] = pair.result || null; + if (typeof this.buildSpeedTestIssue === 'function') { + const issue = this.buildSpeedTestIssue(pair.name, pair.result); + if (issue) issues.push(issue); + } + } + const ok = issues.length === 0 && this.healthCheckBatchFailed === 0; + this.healthCheckResult = { + ok, + issues, + remote: { + type: 'speed-test', + speedTests: results + } + }; + if (ok) { + this.showMessage('检查通过', 'success'); + } + return; + } + const shouldRunSpeedTests = this.configMode === 'codex'; const speedTimeoutMs = shouldRunSpeedTests ? 3500 : 0; const providers = shouldRunSpeedTests @@ -322,7 +369,6 @@ export function createCodexConfigMethods(options = {}) { this.healthCheckResult = null; this.showMessage(getResponseMessage(res, '检查失败'), 'error'); } else if (res && typeof res === 'object') { - shouldRunClaudeSpeedTests = true; const issues = Array.isArray(res.issues) ? [...res.issues] : []; let remote = res.remote || null; if (shouldRunSpeedTests) { @@ -357,12 +403,6 @@ export function createCodexConfigMethods(options = {}) { } finally { this.healthCheckBatchTotal = this.healthCheckBatchTotal || 0; this.healthCheckBatchDone = Math.min(this.healthCheckBatchDone || 0, this.healthCheckBatchTotal || 0); - if (shouldRunClaudeSpeedTests && this.configMode === 'claude') { - try { - const entries = Object.entries(this.claudeConfigs || {}); - await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config))); - } catch (e) {} - } this.healthCheckLoading = false; } }, diff --git a/web-ui/modules/i18n.mjs b/web-ui/modules/i18n.mjs index 58d608bd..3b620ce5 100644 --- a/web-ui/modules/i18n.mjs +++ b/web-ui/modules/i18n.mjs @@ -878,6 +878,8 @@ const DICT = Object.freeze({ 'claude.health.title': '配置健康检查', 'claude.health.run': '运行检查', 'claude.health.running': '检查中...', + 'claude.health.hint': '会批量探测所有 Claude 配置可用性,并刷新延迟显示。', + 'claude.health.progress': '已完成 {done}/{total} · 失败 {failed}', 'claude.md.title': 'CLAUDE.md', 'claude.md.open': '打开 CLAUDE.md', 'claude.md.hint': '读写 ~/.claude/CLAUDE.md。', @@ -1787,6 +1789,8 @@ const DICT = Object.freeze({ 'claude.health.title': 'Config health check', 'claude.health.run': 'Run check', 'claude.health.running': 'Checking...', + 'claude.health.hint': 'Runs availability probes for all Claude configs and refreshes the latency badges.', + 'claude.health.progress': '{done}/{total} done · {failed} failed', 'claude.md.title': 'CLAUDE.md', 'claude.md.open': 'Open CLAUDE.md', 'claude.md.hint': 'Read/write ~/.claude/CLAUDE.md.', diff --git a/web-ui/partials/index/panel-config-claude.html b/web-ui/partials/index/panel-config-claude.html index 520d253e..2aaf2906 100644 --- a/web-ui/partials/index/panel-config-claude.html +++ b/web-ui/partials/index/panel-config-claude.html @@ -106,6 +106,10 @@ +
{{ t('claude.health.hint') }}
+
+ {{ t('claude.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }} +
From bf94c1fd08ce26e57c6dfbaa336b508011fe5899 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Fri, 24 Apr 2026 02:49:25 +0000 Subject: [PATCH 17/21] ui(install): avoid CLI missing flicker before status loads --- web-ui/app.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/web-ui/app.js b/web-ui/app.js index 392b3bf2..051898fe 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -251,28 +251,7 @@ document.addEventListener('DOMContentLoaded', () => { installCommandAction: 'install', installRegistryPreset: 'default', installRegistryCustom: '', - installStatusTargets: [ - { - id: 'claude', - name: 'Claude Code CLI', - packageName: '@anthropic-ai/claude-code', - installed: false, - bin: 'claude', - version: '', - commandPath: '', - error: '' - }, - { - id: 'codex', - name: 'Codex CLI', - packageName: '@openai/codex', - installed: false, - bin: 'codex', - version: '', - commandPath: '', - error: '' - } - ], + installStatusTargets: null, newProvider: { name: '', url: '', key: '', useTransform: false }, resetConfigLoading: false, editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false }, From 39d9aad760c804138efa4c692f59faaf41a25473 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Fri, 24 Apr 2026 03:09:18 +0000 Subject: [PATCH 18/21] chore(reset): default to origin/main without prompt --- README.md | 2 +- README.zh.md | 2 +- tools/dev/reset-main.js | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 128135f8..1ac6396f 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ npm run reset npm run reset 79 ``` -- `npm run reset`: prompt for a PR number; leave it blank to return to default `origin/main` +- `npm run reset`: reset to default `origin/main` - `npm run reset 79`: sync directly to the latest head snapshot of PR `#79` - The script also handles local branch switching, workspace cleanup, untracked file cleanup, and final state validation diff --git a/README.zh.md b/README.zh.md index 28fefbe9..cf36d6d5 100644 --- a/README.zh.md +++ b/README.zh.md @@ -193,7 +193,7 @@ npm run reset npm run reset 79 ``` -- `npm run reset`:交互输入 PR 编号;留空则回到默认 `origin/main` +- `npm run reset`:直接重置到默认 `origin/main` - `npm run reset 79`:直接同步到 PR `#79` 的最新 head 快照 - 脚本会自动完成本地分支切换、工作区清理、未跟踪文件清理与最终状态校验 diff --git a/tools/dev/reset-main.js b/tools/dev/reset-main.js index b41c4acc..2e19148e 100644 --- a/tools/dev/reset-main.js +++ b/tools/dev/reset-main.js @@ -167,8 +167,7 @@ async function main({ argv = process.argv.slice(2), stdin = process.stdin, stdou } const argPrNumber = resolveArgPrNumber(argv); - const prNumber = argPrNumber || await promptForPrNumber({ stdin, stdout }); - const plan = buildResetPlan({ prNumber }); + const plan = buildResetPlan({ prNumber: argPrNumber }); executeResetPlan(plan); } From 7cfdd7419f5136d81ffd4b9a5b1ff826f6517749 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Fri, 24 Apr 2026 03:57:28 +0000 Subject: [PATCH 19/21] fix(ui): address sessions copy, prompt template import, service tier i18n --- cli/auth-profiles.js | 30 ++++++++++++++----- plugins/prompt-templates/overview.mjs | 8 ++++- web-ui/modules/app.computed.dashboard.mjs | 2 +- .../modules/app.methods.session-browser.mjs | 9 +++++- web-ui/partials/index/panel-config-codex.html | 2 +- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/cli/auth-profiles.js b/cli/auth-profiles.js index ba8aa207..4e0797a2 100644 --- a/cli/auth-profiles.js +++ b/cli/auth-profiles.js @@ -28,12 +28,17 @@ function createAuthProfileController(deps = {}) { function normalizeAuthProfileName(value) { const raw = typeof value === 'string' ? value.trim() : ''; if (!raw) return ''; - const sanitized = raw + return raw.slice(0, 120); + } + + function sanitizeAuthProfileFileStem(value) { + const raw = typeof value === 'string' ? value.trim() : ''; + if (!raw) return ''; + return raw .replace(/[\\/:*?"<>|]/g, '-') .replace(/\s+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 120); - return sanitized; } function normalizeAuthRegistry(raw) { @@ -46,7 +51,7 @@ function createAuthProfileController(deps = {}) { version: 1, current: typeof raw.current === 'string' ? raw.current.trim() : '', items: items.map((item) => ({ - name: normalizeAuthProfileName(item.name) || item.name.trim(), + name: item.name.trim(), fileName: typeof item.fileName === 'string' ? path.basename(item.fileName) : '', type: typeof item.type === 'string' ? item.type : '', email: typeof item.email === 'string' ? item.email : '', @@ -135,13 +140,24 @@ function createAuthProfileController(deps = {}) { const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : ''; const preferredName = normalizeAuthProfileName(options.name || ''); const profileName = preferredName || getAuthProfileNameFallback(safePayload, sourceFile); - const fileName = `${profileName}.json`; - const profilePath = path.join(AUTH_PROFILES_DIR, fileName); + let fileStem = sanitizeAuthProfileFileStem(profileName) || sanitizeAuthProfileFileStem(sourceFile) || `auth-${Date.now()}`; + + const registry = readAuthRegistry(); + const existed = registry.items.find((item) => item && item.name === profileName); + if (existed && existed.fileName) { + fileStem = path.basename(existed.fileName, path.extname(existed.fileName)); + } + + let fileName = `${fileStem}.json`; + let profilePath = path.join(AUTH_PROFILES_DIR, fileName); + if (!existed && fs.existsSync(profilePath)) { + fileStem = `${fileStem}-${Date.now().toString(16).slice(-6)}`; + fileName = `${fileStem}.json`; + profilePath = path.join(AUTH_PROFILES_DIR, fileName); + } ensureDir(AUTH_PROFILES_DIR); writeJsonAtomic(profilePath, safePayload); - - const registry = readAuthRegistry(); const meta = buildAuthProfileSummary(profileName, safePayload, fileName); meta.importedAt = toIsoTime(Date.now()); meta.sourceFile = sourceFile || ''; diff --git a/plugins/prompt-templates/overview.mjs b/plugins/prompt-templates/overview.mjs index 792e2226..6f1cbbd4 100644 --- a/plugins/prompt-templates/overview.mjs +++ b/plugins/prompt-templates/overview.mjs @@ -29,7 +29,13 @@ function ensureBuiltinTemplates(rawList, builtins) { const list = Array.isArray(rawList) ? rawList.filter(Boolean) : []; const builtinList = Array.isArray(builtins) ? builtins.filter(Boolean) : []; const rest = list.filter((item) => !(item && item.isBuiltin === true)); - return [...builtinList, ...rest]; + const overridden = new Set( + rest + .map((item) => (item && typeof item.id === 'string' ? item.id.trim() : '')) + .filter(Boolean) + ); + const resolvedBuiltins = builtinList.filter((item) => !(item && overridden.has(item.id))); + return [...resolvedBuiltins, ...rest]; } export async function loadPromptTemplatesOverview(ctx, options = {}) { diff --git a/web-ui/modules/app.computed.dashboard.mjs b/web-ui/modules/app.computed.dashboard.mjs index c3c910a8..2eaf108a 100644 --- a/web-ui/modules/app.computed.dashboard.mjs +++ b/web-ui/modules/app.computed.dashboard.mjs @@ -37,7 +37,7 @@ export function createDashboardComputed() { }, displayProvidersList() { const list = Array.isArray(this.providersList) ? this.providersList : []; - return list.filter((item) => String(item && item.name ? item.name : '').trim().toLowerCase() !== 'codexmate-proxy'); + return list; }, installTargetCards() { const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : []; diff --git a/web-ui/modules/app.methods.session-browser.mjs b/web-ui/modules/app.methods.session-browser.mjs index 73a6ab1f..7a3ceb92 100644 --- a/web-ui/modules/app.methods.session-browser.mjs +++ b/web-ui/modules/app.methods.session-browser.mjs @@ -442,12 +442,19 @@ export function createSessionBrowserMethods(options = {}) { await this.onSessionSourceChange(); }, - copySessionsFilterShareUrl() { + async copySessionsFilterShareUrl() { const url = buildSessionsFilterShareUrl(this); if (!url) { this.showMessage(typeof this.t === 'function' ? this.t('sessions.filters.urlBuildFail') : 'Failed to build link', 'error'); return; } + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(url); + this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success'); + return; + } + } catch (_) {} const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(url) : false; if (ok) { this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success'); diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index e814ba35..93aea872 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -86,7 +86,7 @@
{{ t('config.serviceTier.hint', { field: 'service_tier' }) }} From ea006c9969ab64fd211cf95b27b0f3c7ecb23c1f Mon Sep 17 00:00:00 2001 From: ymkiux Date: Fri, 24 Apr 2026 04:38:06 +0000 Subject: [PATCH 20/21] ui(plugins): remove builtin prompt templates subtitle --- web-ui/partials/index/panel-plugins.html | 1 - 1 file changed, 1 deletion(-) diff --git a/web-ui/partials/index/panel-plugins.html b/web-ui/partials/index/panel-plugins.html index 889ebf73..dfc37feb 100644 --- a/web-ui/partials/index/panel-plugins.html +++ b/web-ui/partials/index/panel-plugins.html @@ -45,7 +45,6 @@
{{ t('plugins.promptTemplates.title') }}
-
{{ t('plugins.promptTemplates.subtitle') }}
From d543aa392f93c0b450e7ebf2ce68b8ccef1a9a45 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Fri, 24 Apr 2026 04:54:38 +0000 Subject: [PATCH 21/21] fix(health/docs): show health results, add modal, align help --- README.md | 1 - cli.js | 1 + site/guide/getting-started.md | 1 - site/index.md | 1 - tests/e2e/test-setup.js | 2 +- tests/unit/agents-diff-ui.test.mjs | 2 +- tests/unit/web-ui-behavior-parity.test.mjs | 1 + web-ui/app.js | 1 + web-ui/index.html | 1 + web-ui/modules/i18n.mjs | 2 + .../index/modal-config-template-agents.html | 3 +- web-ui/partials/index/modal-health-check.html | 45 +++++++++++++++++++ .../partials/index/panel-config-claude.html | 11 +++++ web-ui/partials/index/panel-config-codex.html | 11 +++++ 14 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 web-ui/partials/index/modal-health-check.html diff --git a/README.md b/README.md index 1ac6396f..9bea9700 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,6 @@ npm run reset 79 | `codexmate delete ` | Delete provider | | `codexmate claude [model]` | Write Claude Code config | | `codexmate auth ` | Auth profile management | -| `codexmate proxy ` | Built-in proxy management | | `codexmate workflow ` | MCP workflow management | | `codexmate codex [args...] [--follow-up repeatable]` | Codex CLI passthrough entrypoint (auto-adds `--yolo`, supports queued follow-up appends) | | `codexmate qwen [args...]` | Qwen CLI passthrough entrypoint | diff --git a/cli.js b/cli.js index e0da5ea1..2c1d04a6 100644 --- a/cli.js +++ b/cli.js @@ -13056,6 +13056,7 @@ async function main() { console.log(' codexmate add <名称> [密钥] [--bridge ]'); console.log(' codexmate delete <名称> 删除提供商'); console.log(' codexmate claude [模型] 写入 Claude Code 配置'); + console.log(' codexmate auth 认证管理'); console.log(' codexmate add-model <模型> 添加模型'); console.log(' codexmate delete-model <模型> 删除模型'); console.log(' codexmate workflow MCP 工作流中心'); diff --git a/site/guide/getting-started.md b/site/guide/getting-started.md index f220d188..ed7af7a6 100644 --- a/site/guide/getting-started.md +++ b/site/guide/getting-started.md @@ -46,7 +46,6 @@ codexmate switch codexmate use codexmate claude [model] codexmate auth -codexmate proxy codexmate workflow codexmate qwen [args...] codexmate export-session --source --session-id diff --git a/site/index.md b/site/index.md index 489a74d0..7cee55bd 100644 --- a/site/index.md +++ b/site/index.md @@ -59,7 +59,6 @@ codexmate run --no-browser - `codexmate switch ` / `codexmate use ` - `codexmate claude [model]` - `codexmate auth ` -- `codexmate proxy ` - `codexmate workflow ` - `codexmate qwen [args...]` - `codexmate run [--host ] [--no-browser]` diff --git a/tests/e2e/test-setup.js b/tests/e2e/test-setup.js index 33cd9e33..61731ad2 100644 --- a/tests/e2e/test-setup.js +++ b/tests/e2e/test-setup.js @@ -55,8 +55,8 @@ module.exports = async function testSetup(ctx) { 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'); + assert(helpResult.stdout.includes('codexmate auth'), 'help should expose auth entry'); const claudeModel = 'claude-e2e'; const claudeResult = runSync(node, [cliPath, 'claude', mockProviderUrl, 'sk-claude', claudeModel], { env }); diff --git a/tests/unit/agents-diff-ui.test.mjs b/tests/unit/agents-diff-ui.test.mjs index f879ed41..367b9289 100644 --- a/tests/unit/agents-diff-ui.test.mjs +++ b/tests/unit/agents-diff-ui.test.mjs @@ -20,7 +20,7 @@ test('agents modal exposes diff preview hooks in template and script', () => { assert.match(template, /@click\.self="!configTemplateApplying && closeConfigTemplateModal\(\)"/); assert.match(template, /:readonly="configTemplateApplying \|\| configTemplateDiffLoading"/); assert.match(template, /
diff --git a/web-ui/partials/index/modal-health-check.html b/web-ui/partials/index/modal-health-check.html new file mode 100644 index 00000000..b3071ad8 --- /dev/null +++ b/web-ui/partials/index/modal-health-check.html @@ -0,0 +1,45 @@ + diff --git a/web-ui/partials/index/panel-config-claude.html b/web-ui/partials/index/panel-config-claude.html index 2aaf2906..46ab6417 100644 --- a/web-ui/partials/index/panel-config-claude.html +++ b/web-ui/partials/index/panel-config-claude.html @@ -110,6 +110,17 @@
{{ t('claude.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
+
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }} +
+ +
+
+ {{ issue.message || issue.code || '' }} · {{ issue.suggestion }} +
+
diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index 93aea872..57108491 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -174,6 +174,17 @@
{{ t('config.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
+
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }} +
+ +
+
+ {{ issue.message || issue.code || '' }} · {{ issue.suggestion }} +
+