codexmate 0.0.38 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/openai-bridge.js +541 -210
  3. package/cli.js +19 -1
  4. package/package.json +1 -1
  5. package/web-ui/app.js +12 -3
  6. package/web-ui/modules/app.computed.main-tabs.mjs +37 -30
  7. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  8. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  9. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  10. package/web-ui/modules/app.methods.providers.mjs +36 -10
  11. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  12. package/web-ui/modules/app.methods.startup-claude.mjs +1 -0
  13. package/web-ui/modules/config-mode.computed.mjs +3 -3
  14. package/web-ui/modules/i18n.dict.mjs +13 -0
  15. package/web-ui/modules/i18n.mjs +65 -16
  16. package/web-ui/partials/index/layout-header.html +16 -46
  17. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  18. package/web-ui/partials/index/modal-webhook.html +8 -8
  19. package/web-ui/partials/index/modals-basic.html +56 -16
  20. package/web-ui/partials/index/panel-config-claude.html +20 -20
  21. package/web-ui/partials/index/panel-config-codex.html +5 -5
  22. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  23. package/web-ui/partials/index/panel-dashboard.html +62 -77
  24. package/web-ui/partials/index/panel-settings.html +28 -7
  25. package/web-ui/partials/index/panel-trash.html +14 -14
  26. package/web-ui/res/web-ui-render.precompiled.js +846 -539
  27. package/web-ui/styles/controls-forms.css +6 -0
  28. package/web-ui/styles/dashboard.css +46 -14
  29. package/web-ui/styles/layout-shell.css +45 -0
  30. package/web-ui/styles/navigation-panels.css +3 -3
  31. package/web-ui/styles/openclaw-structured.css +383 -33
  32. package/web-ui/styles/responsive.css +68 -0
  33. package/web-ui/styles/sessions-usage.css +105 -9
  34. package/web-ui/styles/settings-panel.css +4 -0
@@ -33,86 +33,150 @@
33
33
  <button class="btn-mini" @click="resetOpenclawQuick">{{ t('common.clear') }}</button>
34
34
  </div>
35
35
  </div>
36
- <div class="quick-steps">
37
- <div class="quick-step"><span class="step-badge">1</span><span>{{ t('modal.openclaw.quick.step1') }}</span></div>
38
- <div class="quick-step"><span class="step-badge">2</span><span>{{ t('modal.openclaw.quick.step2') }}</span></div>
39
- <div class="quick-step"><span class="step-badge">3</span><span>{{ t('modal.openclaw.quick.step3') }}</span></div>
40
- </div>
41
- <div class="quick-grid">
42
- <div class="quick-card">
43
- <div class="structured-card-title">Provider</div>
44
- <div class="form-group">
45
- <label class="form-label">{{ t('field.providerName') }}</label>
46
- <input v-model="openclawQuick.providerName" class="form-input" placeholder="例如: custom-myapi">
47
- <div class="form-hint">{{ t('modal.openclaw.quick.providerHint') }}</div>
48
- </div>
49
- <div class="form-group">
50
- <label class="form-label">{{ t('field.baseUrl') }}</label>
51
- <input v-model="openclawQuick.baseUrl" class="form-input" placeholder="https://api.example.com/v1" :readonly="openclawQuick.baseUrlReadOnly">
52
- <div v-if="openclawQuick.baseUrlDisplayKind === 'builtin-default'" class="form-hint">{{ t('modal.openclaw.quick.baseUrlHintDefault') }}</div>
53
- <div v-else-if="openclawQuick.baseUrlReadOnly" class="form-hint">{{ t('modal.openclaw.quick.baseUrlHintReadonly') }}</div>
54
- </div>
55
- <div class="form-group">
56
- <label class="form-label">API Key</label>
57
- <div class="list-row">
58
- <input v-model="openclawQuick.apiKey" class="form-input" :readonly="openclawQuick.apiKeyReadOnly" :type="(openclawQuick.apiKeyReadOnly || openclawQuick.showKey) ? 'text' : 'password'" placeholder="sk-...">
59
- <button v-if="!openclawQuick.apiKeyReadOnly" class="btn-mini" @click="toggleOpenclawQuickKey">
60
- {{ openclawQuick.showKey ? t('common.hide') : t('common.show') }}
36
+
37
+ <!-- Accordion Stepper -->
38
+ <div class="quick-accordion">
39
+ <!-- Step 1: Provider -->
40
+ <div :class="['accordion-panel', { active: openclawAccordionStep === 1, completed: openclawAccordionStep > 1 }]">
41
+ <button class="accordion-trigger" @click="toggleAccordionStep(1)" type="button">
42
+ <span class="accordion-step-badge">
43
+ <template v-if="openclawAccordionStep > 1">✓</template>
44
+ <template v-else>1</template>
45
+ </span>
46
+ <span class="accordion-title">Provider</span>
47
+ <span class="accordion-status" v-if="openclawQuick.providerName">{{ openclawQuick.providerName }}</span>
48
+ <svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
49
+ <path d="M6 9l6 6 6-6"/>
50
+ </svg>
51
+ </button>
52
+ <div class="accordion-content" v-show="openclawAccordionStep === 1">
53
+ <div class="form-group">
54
+ <label class="form-label">{{ t('field.providerName') }}</label>
55
+ <input v-model="openclawQuick.providerName" class="form-input" placeholder="例如: custom-myapi" @input="validateProviderName">
56
+ <div class="form-hint" :class="{ 'hint-error': !openclawValidation.providerName.valid }">
57
+ {{ openclawValidation.providerName.message || t('modal.openclaw.quick.providerHint') }}
58
+ </div>
59
+ </div>
60
+ <div class="form-group">
61
+ <label class="form-label">{{ t('field.baseUrl') }}</label>
62
+ <input v-model="openclawQuick.baseUrl" class="form-input" placeholder="https://api.example.com/v1" :readonly="openclawQuick.baseUrlReadOnly">
63
+ <div v-if="openclawQuick.baseUrlDisplayKind === 'builtin-default'" class="form-hint">{{ t('modal.openclaw.quick.baseUrlHintDefault') }}</div>
64
+ <div v-else-if="openclawQuick.baseUrlReadOnly" class="form-hint">{{ t('modal.openclaw.quick.baseUrlHintReadonly') }}</div>
65
+ </div>
66
+ <div class="form-group">
67
+ <label class="form-label">API Key</label>
68
+ <div class="list-row">
69
+ <input v-model="openclawQuick.apiKey" class="form-input" :readonly="openclawQuick.apiKeyReadOnly" :type="(openclawQuick.apiKeyReadOnly || openclawQuick.showKey) ? 'text' : 'password'" placeholder="sk-...">
70
+ <button v-if="!openclawQuick.apiKeyReadOnly" class="btn-mini" @click="toggleOpenclawQuickKey">
71
+ {{ openclawQuick.showKey ? t('common.hide') : t('common.show') }}
72
+ </button>
73
+ </div>
74
+ <div v-if="openclawQuick.apiKeyDisplayKind === 'auth-profile-value'" class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintFromAuth') }}</div>
75
+ <div v-else-if="openclawQuick.apiKeyReadOnly" class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintReadonly') }}</div>
76
+ <div v-else class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintKeep') }}</div>
77
+ </div>
78
+ <div class="form-group">
79
+ <label class="form-label">{{ t('field.apiType') }}</label>
80
+ <input v-model="openclawQuick.apiType" class="form-input" list="openclawApiTypeList" :placeholder="t('placeholder.apiTypeExample')">
81
+ <datalist id="openclawApiTypeList">
82
+ <option value="openai-responses"></option>
83
+ <option value="openai-chat"></option>
84
+ <option value="anthropic"></option>
85
+ <option value="custom"></option>
86
+ </datalist>
87
+ </div>
88
+ <div class="accordion-actions">
89
+ <button class="btn btn-confirm btn-sm" @click="nextAccordionStep" :disabled="!openclawQuick.providerName">
90
+ 下一步 →
61
91
  </button>
62
92
  </div>
63
- <div v-if="openclawQuick.apiKeyDisplayKind === 'auth-profile-value'" class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintFromAuth') }}</div>
64
- <div v-else-if="openclawQuick.apiKeyReadOnly" class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintReadonly') }}</div>
65
- <div v-else class="form-hint">{{ t('modal.openclaw.quick.apiKeyHintKeep') }}</div>
66
- </div>
67
- <div class="form-group">
68
- <label class="form-label">{{ t('field.apiType') }}</label>
69
- <input v-model="openclawQuick.apiType" class="form-input" list="openclawApiTypeList" :placeholder="t('placeholder.apiTypeExample')">
70
- <datalist id="openclawApiTypeList">
71
- <option value="openai-responses"></option>
72
- <option value="openai-chat"></option>
73
- <option value="anthropic"></option>
74
- <option value="custom"></option>
75
- </datalist>
76
93
  </div>
77
94
  </div>
78
95
 
79
- <div class="quick-card">
80
- <div class="structured-card-title">{{ t('modal.openclaw.quick.modelTitle') }}</div>
81
- <div class="form-group">
82
- <label class="form-label">{{ t('field.modelId') }}</label>
83
- <input v-model="openclawQuick.modelId" class="form-input" :placeholder="t('placeholder.modelIdExample')">
84
- </div>
85
- <div class="form-group">
86
- <label class="form-label">{{ t('field.displayName') }}</label>
87
- <input v-model="openclawQuick.modelName" class="form-input" :placeholder="t('placeholder.modelNameOptional')">
88
- </div>
89
- <div class="form-group">
90
- <label class="form-label">{{ t('field.contextAndMaxOutput') }}</label>
91
- <div class="list-row">
92
- <input v-model="openclawQuick.contextWindow" class="form-input" :placeholder="t('field.contextWindow')">
93
- <input v-model="openclawQuick.maxTokens" class="form-input" :placeholder="t('field.maxOutput')">
96
+ <!-- Step 2: Model -->
97
+ <div :class="['accordion-panel', { active: openclawAccordionStep === 2, completed: openclawAccordionStep > 2 }]">
98
+ <button class="accordion-trigger" @click="toggleAccordionStep(2)" type="button">
99
+ <span class="accordion-step-badge">
100
+ <template v-if="openclawAccordionStep > 2">✓</template>
101
+ <template v-else>2</template>
102
+ </span>
103
+ <span class="accordion-title">{{ t('modal.openclaw.quick.modelTitle') }}</span>
104
+ <span class="accordion-status" v-if="openclawQuick.modelId">{{ openclawQuick.modelName || openclawQuick.modelId }}</span>
105
+ <svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
106
+ <path d="M6 9l6 6 6-6"/>
107
+ </svg>
108
+ </button>
109
+ <div class="accordion-content" v-show="openclawAccordionStep === 2">
110
+ <div class="form-group">
111
+ <label class="form-label">{{ t('field.modelId') }}</label>
112
+ <input v-model="openclawQuick.modelId" class="form-input" :placeholder="t('placeholder.modelIdExample')" @input="validateModelId">
113
+ <div class="form-hint" :class="{ 'hint-error': !openclawValidation.modelId.valid }">
114
+ {{ openclawValidation.modelId.message || '必填,例如: gpt-4' }}
115
+ </div>
116
+ </div>
117
+ <div class="form-group">
118
+ <label class="form-label">{{ t('field.displayName') }}</label>
119
+ <input v-model="openclawQuick.modelName" class="form-input" :placeholder="t('placeholder.modelNameOptional')">
120
+ <div class="form-hint">可选,用于显示</div>
121
+ </div>
122
+ <div class="form-group">
123
+ <label class="form-label">{{ t('field.contextAndMaxOutput') }}</label>
124
+ <div class="list-row">
125
+ <input v-model="openclawQuick.contextWindow" class="form-input" :placeholder="t('field.contextWindow')">
126
+ <input v-model="openclawQuick.maxTokens" class="form-input" :placeholder="t('field.maxOutput')">
127
+ </div>
128
+ <div class="form-hint">{{ t('hint.emptyNoChange') }}</div>
129
+ </div>
130
+ <div class="accordion-actions">
131
+ <button class="btn btn-cancel btn-sm" @click="prevAccordionStep">
132
+ ← 上一步
133
+ </button>
134
+ <button class="btn btn-confirm btn-sm" @click="nextAccordionStep" :disabled="!openclawQuick.modelId">
135
+ 下一步 →
136
+ </button>
94
137
  </div>
95
- <div class="form-hint">{{ t('hint.emptyNoChange') }}</div>
96
138
  </div>
97
139
  </div>
98
140
 
99
- <div class="quick-card">
100
- <div class="structured-card-title">{{ t('modal.openclaw.quick.optionsTitle') }}</div>
101
- <label class="quick-option">
102
- <input type="checkbox" v-model="openclawQuick.setPrimary">
103
- {{ t('modal.openclaw.quick.setPrimary') }}
104
- </label>
105
- <label class="quick-option">
106
- <input type="checkbox" v-model="openclawQuick.overrideProvider">
107
- {{ t('modal.openclaw.quick.overrideProvider') }}
108
- </label>
109
- <label class="quick-option">
110
- <input type="checkbox" v-model="openclawQuick.overrideModels">
111
- {{ t('modal.openclaw.quick.overrideModels') }}
112
- </label>
113
- <div class="form-hint">{{ t('modal.openclaw.quick.optionsHint') }}</div>
141
+ <!-- Step 3: Options -->
142
+ <div :class="['accordion-panel', { active: openclawAccordionStep === 3, completed: openclawAccordionStep > 3 }]">
143
+ <button class="accordion-trigger" @click="toggleAccordionStep(3)" type="button">
144
+ <span class="accordion-step-badge">
145
+ <template v-if="openclawAccordionStep > 3">✓</template>
146
+ <template v-else>3</template>
147
+ </span>
148
+ <span class="accordion-title">{{ t('modal.openclaw.quick.optionsTitle') }}</span>
149
+ <span class="accordion-status">高级选项</span>
150
+ <svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
+ <path d="M6 9l6 6 6-6"/>
152
+ </svg>
153
+ </button>
154
+ <div class="accordion-content" v-show="openclawAccordionStep === 3">
155
+ <label class="quick-option">
156
+ <input type="checkbox" v-model="openclawQuick.setPrimary">
157
+ {{ t('modal.openclaw.quick.setPrimary') }}
158
+ </label>
159
+ <label class="quick-option">
160
+ <input type="checkbox" v-model="openclawQuick.overrideProvider">
161
+ {{ t('modal.openclaw.quick.overrideProvider') }}
162
+ </label>
163
+ <label class="quick-option">
164
+ <input type="checkbox" v-model="openclawQuick.overrideModels">
165
+ {{ t('modal.openclaw.quick.overrideModels') }}
166
+ </label>
167
+ <div class="form-hint">{{ t('modal.openclaw.quick.optionsHint') }}</div>
168
+ <div class="accordion-actions">
169
+ <button class="btn btn-cancel btn-sm" @click="prevAccordionStep">
170
+ ← 上一步
171
+ </button>
172
+ <button class="btn btn-confirm btn-sm" @click="finishAccordionStep">
173
+ 完成 ✓
174
+ </button>
175
+ </div>
176
+ </div>
114
177
  </div>
115
178
  </div>
179
+
116
180
  <div class="btn-group">
117
181
  <button class="btn btn-confirm" @click="applyOpenclawQuickToText">{{ t('modal.openclaw.quick.writeToEditor') }}</button>
118
182
  </div>
@@ -262,14 +326,14 @@
262
326
  :readonly="openclawSaving || openclawApplying"
263
327
  placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
264
328
  <div class="template-editor-warning">
265
- <span v-if="openclawEditing.lockName && openclawEditing.name === '默认配置'">默认配置始终映射当前 openclaw.json,请直接使用“保存并应用”。</span>
329
+ <span v-if="openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name)">默认配置始终映射当前 openclaw.json,请直接使用“保存并应用”。</span>
266
330
  <span v-else>保存仅写入本地配置库。点击“保存并应用”后会写入 openclaw.json。</span>
267
331
  </div>
268
332
  </div>
269
333
 
270
334
  <div class="btn-group">
271
335
  <button class="btn btn-cancel" @click="closeOpenclawConfigModal" :disabled="openclawSaving || openclawApplying">取消</button>
272
- <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving || openclawApplying || (openclawEditing.lockName && openclawEditing.name === '默认配置')">
336
+ <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving || openclawApplying || (openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name))">
273
337
  {{ openclawSaving ? '保存中...' : '保存' }}
274
338
  </button>
275
339
  <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawSaving || openclawApplying">
@@ -1,18 +1,18 @@
1
- <!-- Webhook 配置模态框 -->
1
+ <!-- Webhook settings modal -->
2
2
  <div v-if="showWebhookModal" class="modal-overlay" @click.self="closeWebhookModal">
3
3
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="webhook-modal-title">
4
- <div class="modal-title" id="webhook-modal-title">Webhook 配置</div>
4
+ <div class="modal-title" id="webhook-modal-title">{{ t('settings.webhook.modalTitle') }}</div>
5
5
 
6
6
  <div class="form-group">
7
- <label class="form-label">启用状态</label>
7
+ <label class="form-label">{{ t('settings.webhook.enabledLabel') }}</label>
8
8
  <label class="settings-toggle">
9
9
  <input type="checkbox" v-model="webhookConfig.enabled">
10
- <span>启用 Webhook</span>
10
+ <span>{{ t('settings.webhook.enableToggle') }}</span>
11
11
  </label>
12
12
  </div>
13
13
 
14
14
  <div class="form-group">
15
- <label class="form-label">URL</label>
15
+ <label class="form-label">{{ t('settings.webhook.urlLabel') }}</label>
16
16
  <input
17
17
  v-model="webhookConfig.url"
18
18
  class="form-input"
@@ -23,7 +23,7 @@
23
23
  </div>
24
24
 
25
25
  <div class="form-group">
26
- <label class="form-label">事件</label>
26
+ <label class="form-label">{{ t('settings.webhook.eventsLabel') }}</label>
27
27
  <div class="webhook-events-checkbox-list">
28
28
  <label v-for="ev in webhookEventOptions" :key="ev" class="webhook-event-checkbox-item">
29
29
  <input type="checkbox" :checked="webhookConfig.events.includes(ev)" @change="toggleWebhookEvent(ev)">
@@ -33,9 +33,9 @@
33
33
  </div>
34
34
 
35
35
  <div class="btn-group">
36
- <button class="btn btn-cancel" @click="closeWebhookModal">取消</button>
36
+ <button class="btn btn-cancel" @click="closeWebhookModal">{{ t('common.cancel') }}</button>
37
37
  <button class="btn btn-confirm" @click="saveWebhookSettings" :disabled="webhookSaving">
38
- {{ webhookSaving ? '保存中...' : '保存' }}
38
+ {{ webhookSaving ? t('common.saving') : t('common.save') }}
39
39
  </button>
40
40
  </div>
41
41
  </div>
@@ -25,9 +25,27 @@
25
25
  @blur="normalizeProviderDraft('add')">
26
26
  <div v-if="providerFieldError('add', 'url')" class="form-hint form-error">{{ providerFieldError('add', 'url') }}</div>
27
27
  </div>
28
+ <div class="form-group">
29
+ <label class="form-label">{{ t('field.modelName') }}</label>
30
+ <input
31
+ v-model="newProvider.model"
32
+ :class="['form-input', { invalid: !!providerFieldError('add', 'model') }]"
33
+ :placeholder="t('placeholder.modelExample')"
34
+ autocomplete="off"
35
+ spellcheck="false"
36
+ @blur="normalizeProviderDraft('add')">
37
+ <div v-if="providerFieldError('add', 'model')" class="form-hint form-error">{{ providerFieldError('add', 'model') }}</div>
38
+ </div>
28
39
  <div class="form-group">
29
40
  <label class="form-label">{{ t('field.apiKey') }}</label>
30
- <input v-model="newProvider.key" class="form-input" type="password" placeholder="sk-...">
41
+ <div class="input-with-toggle">
42
+ <input v-model="newProvider.key" :class="['form-input', { invalid: !!providerFieldError('add', 'key') }]" :type="showAddProviderKey ? 'text' : 'password'" placeholder="sk-..." autocomplete="off" spellcheck="false" @blur="normalizeProviderDraft('add')">
43
+ <button type="button" class="input-toggle-btn" @click="toggleAddProviderKey" :title="showAddProviderKey ? t('common.hide') : t('common.show')" :aria-label="showAddProviderKey ? t('common.hide') : t('common.show')">
44
+ <svg v-if="!showAddProviderKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
45
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
46
+ </button>
47
+ </div>
48
+ <div v-if="providerFieldError('add', 'key')" class="form-hint form-error">{{ providerFieldError('add', 'key') }}</div>
31
49
  </div>
32
50
  <div class="form-group">
33
51
  <label class="form-label">
@@ -67,7 +85,7 @@
67
85
  <label class="form-label">{{ t('field.apiKey') }}</label>
68
86
  <div class="input-with-toggle">
69
87
  <input v-model="editingProvider.key" class="form-input" :type="showEditProviderKey ? 'text' : 'password'" placeholder="sk-..." autocomplete="off" spellcheck="false">
70
- <button type="button" class="input-toggle-btn" @click="toggleEditProviderKey" :title="showEditProviderKey ? t('common.hide') : t('common.show')">
88
+ <button type="button" class="input-toggle-btn" @click="toggleEditProviderKey" :title="showEditProviderKey ? t('common.hide') : t('common.show')" :aria-label="showEditProviderKey ? t('common.hide') : t('common.show')">
71
89
  <svg v-if="!showEditProviderKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
72
90
  <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
73
91
  </button>
@@ -123,20 +141,34 @@
123
141
 
124
142
  <div class="form-group">
125
143
  <label class="form-label">{{ t('field.configName') }}</label>
126
- <input v-model="newClaudeConfig.name" class="form-input" :placeholder="t('placeholder.configNameExample')">
144
+ <input v-model="newClaudeConfig.name" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'name') }]" :placeholder="t('placeholder.configNameExample')">
145
+ <div v-if="claudeConfigFieldError('add', 'name')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'name') }}</div>
127
146
  </div>
128
147
  <div class="form-group">
129
148
  <label class="form-label">API Key</label>
130
- <input v-model="newClaudeConfig.apiKey" class="form-input" type="password" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
149
+ <div class="input-with-toggle">
150
+ <input v-model="newClaudeConfig.apiKey" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'apiKey') }]" :type="showAddClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
151
+ <button type="button" class="input-toggle-btn" @click="toggleAddClaudeConfigKey" :title="showAddClaudeConfigKey ? t('common.hide') : t('common.show')" :aria-label="showAddClaudeConfigKey ? t('common.hide') : t('common.show')">
152
+ <svg v-if="!showAddClaudeConfigKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
153
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
154
+ </button>
155
+ </div>
156
+ <div v-if="claudeConfigFieldError('add', 'apiKey')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'apiKey') }}</div>
131
157
  </div>
132
158
  <div class="form-group">
133
159
  <label class="form-label">{{ t('field.baseUrl') }}</label>
134
- <input v-model="newClaudeConfig.baseUrl" class="form-input" :placeholder="t('placeholder.baseUrlExampleClaude')">
160
+ <input v-model="newClaudeConfig.baseUrl" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'baseUrl') }]" :placeholder="t('placeholder.baseUrlExampleClaude')">
161
+ <div v-if="claudeConfigFieldError('add', 'baseUrl')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'baseUrl') }}</div>
162
+ </div>
163
+ <div class="form-group">
164
+ <label class="form-label">{{ t('field.modelName') }}</label>
165
+ <input v-model="newClaudeConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
166
+ <div v-if="claudeConfigFieldError('add', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'model') }}</div>
135
167
  </div>
136
168
 
137
169
  <div class="btn-group">
138
170
  <button class="btn btn-cancel" @click="closeClaudeConfigModal">{{ t('common.cancel') }}</button>
139
- <button class="btn btn-confirm" @click="addClaudeConfig">{{ t('common.add') }}</button>
171
+ <button class="btn btn-confirm" @click="addClaudeConfig" :disabled="!canSubmitClaudeConfig('add')">{{ t('common.add') }}</button>
140
172
  </div>
141
173
  </div>
142
174
  </div>
@@ -148,46 +180,54 @@
148
180
 
149
181
  <div class="form-group">
150
182
  <label class="form-label">{{ t('field.configName') }}</label>
151
- <input v-model="editingConfig.name" class="form-input" :placeholder="t('field.configName')" readonly>
183
+ <input v-model="editingConfig.name" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'name') }]" :placeholder="t('field.configName')" readonly>
184
+ <div v-if="claudeConfigFieldError('edit', 'name')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'name') }}</div>
152
185
  </div>
153
186
  <div class="form-group">
154
187
  <label class="form-label">API Key</label>
155
188
  <div class="input-with-toggle">
156
- <input v-model="editingConfig.apiKey" class="form-input" :type="showEditClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
157
- <button type="button" class="input-toggle-btn" @click="toggleEditClaudeConfigKey" :title="showEditClaudeConfigKey ? t('common.hide') : t('common.show')">
189
+ <input v-model="editingConfig.apiKey" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'apiKey') }]" :type="showEditClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
190
+ <button type="button" class="input-toggle-btn" @click="toggleEditClaudeConfigKey" :title="showEditClaudeConfigKey ? t('common.hide') : t('common.show')" :aria-label="showEditClaudeConfigKey ? t('common.hide') : t('common.show')">
158
191
  <svg v-if="!showEditClaudeConfigKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
159
192
  <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
160
193
  </button>
161
194
  </div>
195
+ <div v-if="claudeConfigFieldError('edit', 'apiKey')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'apiKey') }}</div>
162
196
  </div>
163
197
  <div class="form-group">
164
198
  <label class="form-label">{{ t('field.baseUrl') }}</label>
165
- <input v-model="editingConfig.baseUrl" class="form-input" :placeholder="t('placeholder.baseUrlExampleClaude')">
199
+ <input v-model="editingConfig.baseUrl" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'baseUrl') }]" :placeholder="t('placeholder.baseUrlExampleClaude')">
200
+ <div v-if="claudeConfigFieldError('edit', 'baseUrl')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'baseUrl') }}</div>
201
+ </div>
202
+ <div class="form-group">
203
+ <label class="form-label">{{ t('field.modelName') }}</label>
204
+ <input v-model="editingConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
205
+ <div v-if="claudeConfigFieldError('edit', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'model') }}</div>
166
206
  </div>
167
207
 
168
208
  <div class="btn-group">
169
209
  <button class="btn btn-cancel" @click="closeEditConfigModal">{{ t('common.cancel') }}</button>
170
- <button class="btn btn-confirm" @click="saveAndApplyConfig">{{ t('common.saveApply') }}</button>
210
+ <button class="btn btn-confirm" @click="saveAndApplyConfig" :disabled="!canSubmitClaudeConfig('edit')">{{ t('common.saveApply') }}</button>
171
211
  </div>
172
212
  </div>
173
213
  </div>
174
214
 
175
- <!-- Codex 轮询池控制模态框 -->
215
+ <!-- Codex bridge pool modal -->
176
216
  <div v-if="showCodexBridgePoolModal" class="modal-overlay" @click.self="showCodexBridgePoolModal = false">
177
217
  <div class="modal modal-bridge-pool" role="dialog" aria-modal="true" aria-labelledby="codex-bridge-pool-modal-title">
178
218
  <div class="modal-title" id="codex-bridge-pool-modal-title">
179
219
  <svg class="modal-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
180
- 轮询池设置
220
+ {{ t('config.localBridge.poolSettings') }}
181
221
  </div>
182
- <div class="bridge-pool-modal-hint">勾选参与负载均衡的提供商</div>
222
+ <div class="bridge-pool-modal-hint">{{ t('config.localBridge.poolHint') }}</div>
183
223
  <div v-if="localBridgeCandidateProviders().length === 0" class="bridge-pool-empty">
184
224
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
185
- <span>暂无可用上游 provider,请先添加直连 provider</span>
225
+ <span>{{ t('config.localBridge.noProviders') }}</span>
186
226
  </div>
187
227
  <div v-else class="bridge-pool-list">
188
228
  <label v-for="cp in localBridgeCandidateProviders()" :key="cp.name" class="bridge-pool-item">
189
229
  <span class="bridge-pool-item-name">{{ cp.name }}</span>
190
- <span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? '未启用' : '已启用' }}</span>
230
+ <span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? t('common.disabled') : t('common.enabled') }}</span>
191
231
  <input type="checkbox" :checked="!isLocalBridgeExcluded(cp.name)" @change="toggleLocalBridgeExcluded(cp.name)" />
192
232
  </label>
193
233
  </div>
@@ -35,26 +35,26 @@
35
35
  <div class="selector-section">
36
36
  <div class="selector-header"><span class="selector-title">{{ t('claude.presetProviders') }}</span></div>
37
37
  <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
38
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
39
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
40
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
41
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Z.ai GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.z.ai/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Z.ai GLM</button>
42
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Qwen Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://coding.dashscope.aliyuncs.com/apps/anthropic'; newClaudeConfig.model = 'qwen3-coder'; showClaudeConfigModal = true">Qwen Coder</button>
43
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi k2'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.moonshot.cn/anthropic'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi k2</button>
44
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi For Coding'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.kimi.com/coding/'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi For Coding</button>
45
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showClaudeConfigModal = true">KAT-Coder</button>
46
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Longcat'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.longcat.chat/anthropic'; newClaudeConfig.model = 'LongCat-Flash-Chat'; showClaudeConfigModal = true">Longcat</button>
47
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimaxi.com/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax</button>
48
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax en'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimax.io/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax en</button>
49
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DouBaoSeed'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://ark.cn-beijing.volces.com/api/coding'; newClaudeConfig.model = 'doubao-seed-2-0-code-preview-latest'; showClaudeConfigModal = true">DouBaoSeed</button>
50
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'BaiLing'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.tbox.cn/api/anthropic'; newClaudeConfig.model = 'Ling-2.5-1T'; showClaudeConfigModal = true">BaiLing</button>
51
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'ModelScope'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api-inference.modelscope.cn'; newClaudeConfig.model = 'ZhipuAI/GLM-5'; showClaudeConfigModal = true">ModelScope</button>
52
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">AiHubMix</button>
53
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">DMXAPI</button>
54
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">PackyCode</button>
55
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AnyRouter'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://anyrouter.top'; newClaudeConfig.model = 'claude-opus-4-7[1m]'; showClaudeConfigModal = true">AnyRouter</button>
56
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi MiMo'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi MiMo</button>
57
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi Token Plan'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://token-plan-cn.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi Token Plan</button>
38
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Claude Official</button>
39
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DeepSeek</button>
40
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Zhipu GLM</button>
41
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Z.ai GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.z.ai/api/anthropic'; newClaudeConfig.model = 'glm-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Z.ai GLM</button>
42
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Qwen Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://coding.dashscope.aliyuncs.com/apps/anthropic'; newClaudeConfig.model = 'qwen3-coder'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Qwen Coder</button>
43
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi k2'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.moonshot.cn/anthropic'; newClaudeConfig.model = 'kimi-k2.5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Kimi k2</button>
44
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi For Coding'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.kimi.com/coding/'; newClaudeConfig.model = 'kimi-k2.5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Kimi For Coding</button>
45
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">KAT-Coder</button>
46
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Longcat'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.longcat.chat/anthropic'; newClaudeConfig.model = 'LongCat-Flash-Chat'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Longcat</button>
47
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimaxi.com/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">MiniMax</button>
48
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax en'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimax.io/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">MiniMax en</button>
49
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DouBaoSeed'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://ark.cn-beijing.volces.com/api/coding'; newClaudeConfig.model = 'doubao-seed-2-0-code-preview-latest'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DouBaoSeed</button>
50
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'BaiLing'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.tbox.cn/api/anthropic'; newClaudeConfig.model = 'Ling-2.5-1T'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">BaiLing</button>
51
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'ModelScope'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api-inference.modelscope.cn'; newClaudeConfig.model = 'ZhipuAI/GLM-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">ModelScope</button>
52
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">AiHubMix</button>
53
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DMXAPI</button>
54
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">PackyCode</button>
55
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AnyRouter'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://anyrouter.top'; newClaudeConfig.model = 'claude-opus-4-7[1m]'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">AnyRouter</button>
56
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi MiMo'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Xiaomi MiMo</button>
57
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi Token Plan'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://token-plan-cn.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Xiaomi Token Plan</button>
58
58
  </div>
59
59
  </div>
60
60
 
@@ -26,7 +26,7 @@
26
26
  </div>
27
27
  </template>
28
28
  <template v-else>
29
- <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
29
+ <button class="btn-add" @click="showAddProviderKey = false; showAddModal = true" v-if="!loading && !initError">
30
30
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 4v12M4 10h12"/></svg>
31
31
  {{ t('config.addProvider') }}
32
32
  </button>
@@ -36,7 +36,7 @@
36
36
  <span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
37
37
  </div>
38
38
  <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
39
- <button v-for="tpl in codexProviderTemplates" :key="tpl.name" type="button" class="btn-mini" @click="newProvider.name = tpl.name; newProvider.url = tpl.url; newProvider._suggestedModel = tpl.model || ''; newProvider.useTransform = !!tpl.useTransform; showAddModal = true">{{ tpl.label }}</button>
39
+ <button v-for="tpl in codexProviderTemplates" :key="tpl.name" type="button" class="btn-mini" @click="newProvider.name = tpl.name; newProvider.url = tpl.url; newProvider.model = tpl.model || ''; newProvider.useTransform = !!tpl.useTransform; showAddProviderKey = false; showAddModal = true">{{ tpl.label }}</button>
40
40
  </div>
41
41
  </div>
42
42
 
@@ -122,11 +122,11 @@
122
122
  <div v-if="!loading && !initError" class="card-list">
123
123
  <div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" @click="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.enter.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.space.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
124
124
  <div class="card-leading">
125
- <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
125
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" :title="t('config.transformProvider.title')"></span></div>
126
126
  <div class="card-content">
127
127
  <div v-if="provider.name === 'local'" class="bridge-pool-summary">
128
128
  <svg class="bridge-pool-summary-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
129
- <span class="bridge-pool-summary-text">已启用 {{ localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length }} / {{ localBridgeCandidateProviders().length }}</span>
129
+ <span class="bridge-pool-summary-text">{{ t('config.localBridge.enabledCount', { enabled: localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length, total: localBridgeCandidateProviders().length }) }}</span>
130
130
  </div>
131
131
  <div class="card-title">
132
132
  <span>{{ provider.name }}</span>
@@ -140,7 +140,7 @@
140
140
  <span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">{{ formatLatency(speedResults[provider.name]) }}</span>
141
141
  <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">{{ providerPillText(provider) }}</span>
142
142
  <div class="card-actions" @click.stop>
143
- <button v-if="provider.name === 'local'" class="card-action-btn bridge-pool-trigger" @click="showCodexBridgePoolModal = true" :aria-label="'轮询池设置'" :title="'轮询池设置'">
143
+ <button v-if="provider.name === 'local'" class="card-action-btn bridge-pool-trigger" @click="showCodexBridgePoolModal = true" :aria-label="t('config.localBridge.poolSettings')" :title="t('config.localBridge.poolSettings')">
144
144
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
145
145
  </button>
146
146
  <button class="card-action-btn" :class="{ loading: speedLoading[provider.name] }" :disabled="!!speedLoading[provider.name]" @click="runSpeedTest(provider.name, { silent: true })" :aria-label="t('config.availabilityTestAria', { name: provider.name })" :title="t('config.availabilityTest')">