diff --git a/tests/unit/web-ui-behavior-parity.test.mjs b/tests/unit/web-ui-behavior-parity.test.mjs index 57c637fb..0df77ac6 100644 --- a/tests/unit/web-ui-behavior-parity.test.mjs +++ b/tests/unit/web-ui-behavior-parity.test.mjs @@ -411,6 +411,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus ]; allowedExtraCurrentKeys.push( 'lang', + 'configTemplateContext', 'configTemplateDiffVisible', 'configTemplateDiffLoading', 'configTemplateDiffError', @@ -424,6 +425,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'healthCheckBatchDone', 'healthCheckBatchFailed', 'showHealthCheckModal', + 'showCodexBridgePoolModal', + 'showClaudeBridgePoolModal', 'pluginsActiveId', 'pluginsLoading', 'pluginsError', @@ -571,6 +574,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus 'normalizeConfigTemplateDiffConfirmEnabled', 'setConfigTemplateDiffConfirmEnabled', 'extractClaudeResumeKeyFromFilePath', + 'openClaudeConfigTemplateEditor', + 'applyClaudeLocalBridge', 'loadPluginsOverview', 'selectPlugin', 'createPromptTemplate', diff --git a/web-ui/app.js b/web-ui/app.js index f89a21c6..a0520cff 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -71,6 +71,8 @@ document.addEventListener('DOMContentLoaded', () => { showAgentsModal: false, showSkillsModal: false, showHealthCheckModal: false, + showCodexBridgePoolModal: false, + showClaudeBridgePoolModal: false, // Plugins pluginsActiveId: 'prompt-templates', pluginsLoading: false, @@ -98,6 +100,7 @@ document.addEventListener('DOMContentLoaded', () => { confirmDialogResolver: null, configTemplateContent: '', configTemplateApplying: false, + configTemplateContext: 'codex', configTemplateDiffVisible: false, configTemplateDiffLoading: false, configTemplateDiffError: '', diff --git a/web-ui/logic.claude.mjs b/web-ui/logic.claude.mjs index 52d4d2ed..254157ac 100644 --- a/web-ui/logic.claude.mjs +++ b/web-ui/logic.claude.mjs @@ -111,6 +111,10 @@ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) { if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) { return ''; } + // 检测本地桥接 URL + if (typeof normalizedSettings.baseUrl === 'string' && normalizedSettings.baseUrl.includes('/bridge/claude-local/')) { + return 'claude-local'; + } const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl); const entries = Object.entries(claudeConfigs || {}); for (const [name, config] of entries) { diff --git a/web-ui/modules/app.methods.claude-config.mjs b/web-ui/modules/app.methods.claude-config.mjs index b216fb15..41e698ea 100644 --- a/web-ui/modules/app.methods.claude-config.mjs +++ b/web-ui/modules/app.methods.claude-config.mjs @@ -264,6 +264,43 @@ export function createClaudeConfigMethods(options = {}) { claudeLocalBridgeConfigured() { return this.claudeLocalBridgeCandidateProviders().some(p => p.hasKey); + }, + + async applyClaudeLocalBridge() { + this.currentClaudeConfig = 'claude-local'; + try { localStorage.setItem('currentClaudeConfig', 'claude-local'); } catch (_) {} + this.refreshClaudeModelContext(); + + const candidates = this.claudeLocalBridgeCandidateProviders(); + if (candidates.length === 0) { + return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error'); + } + + try { + const res = await api('claude-local-bridge-toggle', { enable: true }); + if (res.error) { + this.showMessage(res.error || '启用本地负载均衡失败', 'error'); + return; + } + this.showMessage('Claude 本地负载均衡已启用', 'success'); + } catch (e) { + this.showMessage('启用本地负载均衡失败', 'error'); + } + }, + + async openClaudeConfigTemplateEditor() { + try { + const res = await api('get-claude-settings-raw'); + if (res.error) { + this.showMessage(res.error, 'error'); + return; + } + this.configTemplateContent = res.content || '{}'; + this.configTemplateContext = 'claude'; + this.showConfigTemplateModal = true; + } catch (e) { + this.showMessage('加载 Claude settings 失败', 'error'); + } } }; } diff --git a/web-ui/modules/app.methods.codex-config.mjs b/web-ui/modules/app.methods.codex-config.mjs index bc6b4505..3f0faded 100644 --- a/web-ui/modules/app.methods.codex-config.mjs +++ b/web-ui/modules/app.methods.codex-config.mjs @@ -558,6 +558,7 @@ export function createCodexConfigMethods(options = {}) { template = `${template.trimEnd()}\n\n${appendBlock}\n`; } this.configTemplateContent = template; + this.configTemplateContext = 'codex'; this.showConfigTemplateModal = true; } catch (e) { this.showMessage('加载模板失败', 'error'); @@ -807,9 +808,16 @@ export function createCodexConfigMethods(options = {}) { const performApply = async () => { this.configTemplateApplying = true; try { - const res = await api('apply-config-template', { - template: this.configTemplateContent - }); + let res; + if (this.configTemplateContext === 'claude') { + res = await api('apply-claude-settings-raw', { + content: this.configTemplateContent + }); + } else { + res = await api('apply-config-template', { + template: this.configTemplateContent + }); + } if (res.error) { this.showMessage(res.error, 'error'); return; diff --git a/web-ui/partials/index/modals-basic.html b/web-ui/partials/index/modals-basic.html index 86c0e7da..70c50dc6 100644 --- a/web-ui/partials/index/modals-basic.html +++ b/web-ui/partials/index/modals-basic.html @@ -171,3 +171,29 @@ + + + + diff --git a/web-ui/partials/index/panel-config-claude.html b/web-ui/partials/index/panel-config-claude.html index 449dabf2..a93c7072 100644 --- a/web-ui/partials/index/panel-config-claude.html +++ b/web-ui/partials/index/panel-config-claude.html @@ -82,6 +82,7 @@ :placeholder="t('claude.model.placeholder')" >
{{ t('claude.model.hint') }}
+
@@ -97,20 +98,6 @@
-
-
-
L
-
-
- local - {{ t('config.badge.system') }} -
-
-
-
- {{ claudeLocalBridgeConfigured() ? t('claude.configured') : t('claude.notConfigured') }} -
-
{{ name.charAt(0).toUpperCase() }}
@@ -141,26 +128,5 @@
-
-
- - - - {{ t('claude.localBridge.poolTitle') }} - {{ t('claude.localBridge.poolHint') }} -
-
- - {{ t('claude.localBridge.noProviders') }} -
-
- -
-
-
diff --git a/web-ui/partials/index/panel-config-codex.html b/web-ui/partials/index/panel-config-codex.html index 99d96223..b86d6d89 100644 --- a/web-ui/partials/index/panel-config-codex.html +++ b/web-ui/partials/index/panel-config-codex.html @@ -124,6 +124,10 @@
{{ provider.name.charAt(0).toUpperCase() }}
+
+ + 已启用 {{ localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length }} / {{ localBridgeCandidateProviders().length }} +
{{ provider.name }} {{ t('config.badge.system') }} @@ -136,6 +140,9 @@ {{ formatLatency(speedResults[provider.name]) }} {{ providerPillText(provider) }}
+ @@ -166,26 +173,5 @@
- -
-
- - - - 轮询池 - 勾选参与负载均衡的提供商 -
-
- - 暂无可用上游 provider,请先添加直连 provider -
-
- -
-
\ No newline at end of file diff --git a/web-ui/styles/bridge-pool.css b/web-ui/styles/bridge-pool.css index d5005d19..a1e3607b 100644 --- a/web-ui/styles/bridge-pool.css +++ b/web-ui/styles/bridge-pool.css @@ -3,6 +3,43 @@ ============================================ */ +/* ---- 摘要状态(在卡片标题上方) ---- */ +.bridge-pool-summary { + display: flex; + align-items: center; + gap: 5px; + margin-bottom: 4px; +} + +.bridge-pool-summary-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 5px; + background: var(--color-brand-light); + color: var(--color-brand-dark); + flex-shrink: 0; +} + +.bridge-pool-summary-text { + font-size: 11px; + font-weight: 600; + color: var(--color-text-muted); + letter-spacing: -0.01em; +} + +/* ---- 轮询池触发按钮 ---- */ +.bridge-pool-trigger { + color: var(--color-brand); + background: rgba(200, 121, 99, 0.08); +} + +.bridge-pool-trigger:hover { + background: rgba(200, 121, 99, 0.16); +} + .bridge-pool-panel { margin-top: 18px; padding: 18px 20px 16px; @@ -195,3 +232,35 @@ padding: 10px 12px 10px 14px; } } + +/* ---- 模态框中的轮询池 ---- */ +.modal-bridge-pool { + max-width: 380px; +} + +.modal-title-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: var(--color-brand-light); + color: var(--color-brand-dark); + margin-right: 8px; + vertical-align: middle; +} + +.bridge-pool-modal-hint { + font-size: 12px; + color: var(--color-text-muted); + margin-bottom: 14px; + padding-bottom: 12px; + border-bottom: 1px solid var(--color-border-soft); +} + +.modal-bridge-pool .bridge-pool-list { + max-height: 320px; + overflow-y: auto; + margin-bottom: 16px; +}