codexmate 0.0.42 → 0.0.43

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.
package/cli.js CHANGED
@@ -10461,6 +10461,7 @@ function assertRequestAuthorized(req, res) {
10461
10461
 
10462
10462
  function isProtectedWebSurfacePath(requestPath) {
10463
10463
  return requestPath === '/'
10464
+ || requestPath === '/session'
10464
10465
  || requestPath === '/web-ui/index.html'
10465
10466
  || requestPath.startsWith('/web-ui/')
10466
10467
  || requestPath.startsWith('/res/');
@@ -11889,8 +11890,8 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
11889
11890
  });
11890
11891
  fs.createReadStream(filePath).pipe(res);
11891
11892
  } else {
11892
- // Only serve HTML for root path; /web-ui returns 404.
11893
- if (requestPath === '/') {
11893
+ // Serve the SPA shell for routable entry points. Keep /web-ui as 404.
11894
+ if (requestPath === '/' || requestPath === '/session') {
11894
11895
  try {
11895
11896
  const html = readBundledWebUiHtml(htmlPath);
11896
11897
  res.writeHead(200, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -46,7 +46,6 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@iarna/toml": "^2.2.5",
49
- "@vue/compiler-dom": "^3.5.30",
50
49
  "json5": "^2.2.3",
51
50
  "yauzl": "^3.2.1",
52
51
  "zip-lib": "^1.2.1"
@@ -72,6 +71,7 @@
72
71
  "author": "ymkiux",
73
72
  "license": "Apache-2.0",
74
73
  "devDependencies": {
74
+ "@vue/compiler-dom": "^3.5.30",
75
75
  "vitepress": "^1.6.4"
76
76
  }
77
77
  }
package/web-ui/app.js CHANGED
@@ -366,7 +366,21 @@ document.addEventListener('DOMContentLoaded', () => {
366
366
  codexDownloadProgress: 0,
367
367
  codexDownloadTimer: null,
368
368
  settingsTab: 'general',
369
- toolConfigPermissions: { codex: false, claude: false },
369
+ toolConfigPermissions: (function() {
370
+ try {
371
+ const cached = localStorage.getItem('toolConfigPermissions');
372
+ if (cached) {
373
+ const parsed = JSON.parse(cached);
374
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
375
+ return {
376
+ codex: parsed.codex === true,
377
+ claude: parsed.claude === true
378
+ };
379
+ }
380
+ }
381
+ } catch (_) {}
382
+ return { codex: false, claude: false };
383
+ })(),
370
384
  toolConfigPermissionSaving: { codex: false, claude: false },
371
385
  sessionTrashEnabled: true,
372
386
  sessionTrashItems: [],
@@ -445,13 +459,9 @@ document.addEventListener('DOMContentLoaded', () => {
445
459
  window.location.replace(url.toString());
446
460
  return;
447
461
  }
448
- // 清理任何查询参数和 hash,保持 URL /
449
- if (window.location.search || window.location.hash) {
450
- const url = new URL(window.location.href);
451
- url.search = '';
452
- url.hash = '';
453
- window.history.replaceState(null, '', url.toString());
454
- }
462
+ // Do not strip query/hash during startup: /session uses them to identify the
463
+ // standalone session, and shareable tab/filter URLs are consumed below before
464
+ // later runtime canonicalization can clean the address bar.
455
465
  } catch (_) {}
456
466
 
457
467
  if (typeof this.initI18n === 'function') {
@@ -60,7 +60,7 @@ export function createAgentsMethods(options = {}) {
60
60
  if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
61
61
  return;
62
62
  }
63
- this.showMessage('加载文件失败', 'error');
63
+ this.showMessage(this.t('toast.load.fail'), 'error');
64
64
  } finally {
65
65
  if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
66
66
  this.agentsLoading = false;
@@ -92,7 +92,7 @@ export function createAgentsMethods(options = {}) {
92
92
  if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
93
93
  return;
94
94
  }
95
- this.showMessage('加载文件失败', 'error');
95
+ this.showMessage(this.t('toast.load.fail'), 'error');
96
96
  } finally {
97
97
  if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
98
98
  this.agentsLoading = false;
@@ -127,7 +127,7 @@ export function createAgentsMethods(options = {}) {
127
127
  if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
128
128
  return;
129
129
  }
130
- this.showMessage('加载文件失败', 'error');
130
+ this.showMessage(this.t('toast.load.fail'), 'error');
131
131
  } finally {
132
132
  if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
133
133
  this.agentsLoading = false;
@@ -171,7 +171,7 @@ export function createAgentsMethods(options = {}) {
171
171
  if (!isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
172
172
  return;
173
173
  }
174
- this.showMessage('加载文件失败', 'error');
174
+ this.showMessage(this.t('toast.load.fail'), 'error');
175
175
  } finally {
176
176
  if (isLatestRequestToken(this, '_agentsOpenRequestToken', requestToken)) {
177
177
  this.agentsLoading = false;
@@ -588,7 +588,7 @@ export function createAgentsMethods(options = {}) {
588
588
  }
589
589
  if (!this.agentsDiffVisible) {
590
590
  if (!this.hasAgentsContentChanged()) {
591
- this.showMessage('未检测到改动', 'info');
591
+ this.showMessage(this.t('toast.noChanges'), 'info');
592
592
  return;
593
593
  }
594
594
  await this.prepareAgentsDiff();
@@ -642,7 +642,7 @@ export function createAgentsMethods(options = {}) {
642
642
  this.showMessage(successLabel, 'success');
643
643
  this.closeAgentsModal({ force: true });
644
644
  } catch (e) {
645
- this.showMessage('保存失败', 'error');
645
+ this.showMessage(this.t('toast.save.fail'), 'error');
646
646
  } finally {
647
647
  this.agentsSaving = false;
648
648
  }
@@ -31,23 +31,23 @@ function getClaudeConfigValidationForContext(vm, mode = 'add') {
31
31
  };
32
32
 
33
33
  if (!name) {
34
- errors.name = '配置名称不能为空';
34
+ errors.name = vm.t('validation.claude.nameRequired');
35
35
  } else if (mode === 'add' && vm.claudeConfigs && vm.claudeConfigs[name]) {
36
- errors.name = '名称已存在';
36
+ errors.name = vm.t('validation.claude.nameExists');
37
37
  }
38
38
 
39
39
  if (!apiKey && !externalCredentialType) {
40
- errors.apiKey = 'API Key 必填';
40
+ errors.apiKey = vm.t('validation.claude.apiKeyRequired');
41
41
  }
42
42
 
43
43
  if (!baseUrl) {
44
- errors.baseUrl = 'Base URL 必填';
44
+ errors.baseUrl = vm.t('validation.claude.baseUrlRequired');
45
45
  } else if (!isValidClaudeHttpUrl(baseUrl)) {
46
- errors.baseUrl = 'Base URL 仅支持 http/https';
46
+ errors.baseUrl = vm.t('validation.claude.baseUrlHttpOnly');
47
47
  }
48
48
 
49
49
  if (!model) {
50
- errors.model = '模型名称必填';
50
+ errors.model = vm.t('validation.claude.modelRequired');
51
51
  }
52
52
 
53
53
  return {
@@ -79,7 +79,7 @@ export function createClaudeConfigMethods(options = {}) {
79
79
  }
80
80
  const model = (this.currentClaudeModel || '').trim();
81
81
  if (!model) {
82
- this.showMessage('请输入模型', 'error');
82
+ this.showMessage(this.t('toast.claude.modelRequired'), 'error');
83
83
  return;
84
84
  }
85
85
  const existing = this.claudeConfigs[name] || {};
@@ -89,7 +89,7 @@ export function createClaudeConfigMethods(options = {}) {
89
89
  this.saveClaudeConfigs();
90
90
  this.updateClaudeModelsCurrent();
91
91
  if (!this.claudeConfigs[name].apiKey && !this.claudeConfigs[name].externalCredentialType) {
92
- this.showMessage('请先配置 API Key', 'error');
92
+ this.showMessage(this.t('toast.claude.apiKeyRequired'), 'error');
93
93
  return;
94
94
  }
95
95
  this.applyClaudeConfig(name);
@@ -153,7 +153,7 @@ export function createClaudeConfigMethods(options = {}) {
153
153
  updateConfig() {
154
154
  const validation = getClaudeConfigValidationForContext(this, 'edit');
155
155
  if (!validation.ok) {
156
- return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
156
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || this.t('toast.claude.checkConfig'), 'error');
157
157
  }
158
158
  const name = validation.name;
159
159
  this.editingConfig.apiKey = validation.apiKey;
@@ -161,7 +161,7 @@ export function createClaudeConfigMethods(options = {}) {
161
161
  this.editingConfig.model = validation.model;
162
162
  this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
163
163
  this.saveClaudeConfigs();
164
- this.showMessage('操作成功', 'success');
164
+ this.showMessage(this.t('toast.operation.success'), 'success');
165
165
  this.closeEditConfigModal();
166
166
  if (name === this.currentClaudeConfig) {
167
167
  this.refreshClaudeModelContext();
@@ -181,7 +181,7 @@ export function createClaudeConfigMethods(options = {}) {
181
181
  async saveAndApplyConfig() {
182
182
  const validation = getClaudeConfigValidationForContext(this, 'edit');
183
183
  if (!validation.ok) {
184
- return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
184
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || this.t('toast.claude.checkConfig'), 'error');
185
185
  }
186
186
  const name = validation.name;
187
187
  this.editingConfig.apiKey = validation.apiKey;
@@ -192,7 +192,7 @@ export function createClaudeConfigMethods(options = {}) {
192
192
 
193
193
  const config = this.claudeConfigs[name];
194
194
  if (!config.apiKey) {
195
- this.showMessage('已保存(未填写 API Key)', 'info');
195
+ this.showMessage(this.t('toast.claude.savedWithoutKey'), 'info');
196
196
  this.closeEditConfigModal();
197
197
  if (name === this.currentClaudeConfig) {
198
198
  this.refreshClaudeModelContext();
@@ -204,25 +204,25 @@ export function createClaudeConfigMethods(options = {}) {
204
204
  try {
205
205
  const res = await api('apply-claude-config', { config });
206
206
  if (res.error || res.success === false) {
207
- this.showMessage(res.error || '应用配置失败', 'error');
207
+ this.showMessage(res.error || this.t('toast.apply.fail'), 'error');
208
208
  } else {
209
209
  this.currentClaudeConfig = name;
210
210
  if (this._lastAppliedClaudeKey !== _claudeKey) {
211
- this.showMessage('Claude 配置已生效', 'success');
211
+ this.showMessage(this.t('toast.claude.applied'), 'success');
212
212
  this._lastAppliedClaudeKey = _claudeKey;
213
213
  }
214
214
  this.closeEditConfigModal();
215
215
  this.refreshClaudeModelContext();
216
216
  }
217
217
  } catch (_) {
218
- this.showMessage('应用配置失败', 'error');
218
+ this.showMessage(this.t('toast.apply.fail'), 'error');
219
219
  }
220
220
  },
221
221
 
222
222
  addClaudeConfig() {
223
223
  const validation = getClaudeConfigValidationForContext(this, 'add');
224
224
  if (!validation.ok) {
225
- return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || '请检查 Claude 配置', 'error');
225
+ return this.showMessage(validation.errors.name || validation.errors.apiKey || validation.errors.baseUrl || validation.errors.model || this.t('toast.claude.checkConfig'), 'error');
226
226
  }
227
227
  this.newClaudeConfig.name = validation.name;
228
228
  this.newClaudeConfig.apiKey = validation.apiKey;
@@ -231,27 +231,27 @@ export function createClaudeConfigMethods(options = {}) {
231
231
  const name = validation.name;
232
232
  const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
233
233
  if (duplicateName) {
234
- return this.showMessage('配置已存在', 'info');
234
+ return this.showMessage(this.t('toast.claude.exists'), 'info');
235
235
  }
236
236
 
237
237
  this.claudeConfigs[name] = this.mergeClaudeConfig({}, this.newClaudeConfig);
238
238
 
239
239
  this.currentClaudeConfig = name;
240
240
  this.saveClaudeConfigs();
241
- this.showMessage('操作成功', 'success');
241
+ this.showMessage(this.t('toast.operation.success'), 'success');
242
242
  this.closeClaudeConfigModal();
243
243
  this.refreshClaudeModelContext();
244
244
  },
245
245
 
246
246
  async deleteClaudeConfig(name) {
247
247
  if (Object.keys(this.claudeConfigs).length <= 1) {
248
- return this.showMessage('至少保留一项', 'error');
248
+ return this.showMessage(this.t('toast.claude.keepOne'), 'error');
249
249
  }
250
250
  const confirmed = await this.requestConfirmDialog({
251
- title: '删除 Claude 配置',
252
- message: `确定删除配置 "${name}"?`,
253
- confirmText: '删除',
254
- cancelText: '取消',
251
+ title: this.t('modal.claudeDelete.title'),
252
+ message: this.t('modal.claudeDelete.message', { name }),
253
+ confirmText: this.t('modal.claudeDelete.confirm'),
254
+ cancelText: this.t('modal.claudeDelete.cancel'),
255
255
  danger: true
256
256
  });
257
257
  if (!confirmed) return;
@@ -261,7 +261,7 @@ export function createClaudeConfigMethods(options = {}) {
261
261
  this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
262
262
  }
263
263
  this.saveClaudeConfigs();
264
- this.showMessage('操作成功', 'success');
264
+ this.showMessage(this.t('toast.operation.success'), 'success');
265
265
  this.refreshClaudeModelContext();
266
266
  },
267
267
 
@@ -273,24 +273,24 @@ export function createClaudeConfigMethods(options = {}) {
273
273
 
274
274
  if (!config.apiKey) {
275
275
  if (config.externalCredentialType) {
276
- return this.showMessage('使用外部认证,无需 API Key', 'info');
276
+ return this.showMessage(this.t('toast.claude.externalAuth'), 'info');
277
277
  }
278
- return this.showMessage('请先配置 API Key', 'error');
278
+ return this.showMessage(this.t('toast.claude.apiKeyRequired'), 'error');
279
279
  }
280
280
 
281
281
  const _claudeKey2 = `${name}|${config.apiKey || ""}|${config.baseUrl || ""}|${config.model || ""}`;
282
282
  try {
283
283
  const res = await api('apply-claude-config', { config });
284
284
  if (res.error || res.success === false) {
285
- this.showMessage(res.error || '应用配置失败', 'error');
285
+ this.showMessage(res.error || this.t('toast.apply.fail'), 'error');
286
286
  } else {
287
287
  if (this._lastAppliedClaudeKey !== _claudeKey2) {
288
- this.showMessage('配置已应用', 'success');
288
+ this.showMessage(this.t('toast.apply.success'), 'success');
289
289
  this._lastAppliedClaudeKey = _claudeKey2;
290
290
  }
291
291
  }
292
292
  } catch (_) {
293
- this.showMessage('应用配置失败', 'error');
293
+ this.showMessage(this.t('toast.apply.fail'), 'error');
294
294
  }
295
295
  },
296
296
 
@@ -328,12 +328,12 @@ export function createClaudeConfigMethods(options = {}) {
328
328
  return;
329
329
  }
330
330
  if (enable) {
331
- this.showMessage('Claude 本地负载均衡已启用', 'success');
331
+ this.showMessage(this.t('toast.claude.balanceEnabled'), 'success');
332
332
  } else {
333
- this.showMessage('Claude 本地负载均衡已关闭', 'success');
333
+ this.showMessage(this.t('toast.claude.balanceDisabled'), 'success');
334
334
  }
335
335
  } catch (e) {
336
- this.showMessage('操作失败', 'error');
336
+ this.showMessage(this.t('toast.operation.fail'), 'error');
337
337
  }
338
338
  },
339
339
 
@@ -379,18 +379,18 @@ export function createClaudeConfigMethods(options = {}) {
379
379
 
380
380
  const candidates = this.claudeLocalBridgeCandidateProviders();
381
381
  if (candidates.length === 0) {
382
- return this.showMessage('请先添加并配置至少一个 Claude 提供商', 'error');
382
+ return this.showMessage(this.t('toast.claude.balanceRequireProvider'), 'error');
383
383
  }
384
384
 
385
385
  try {
386
386
  const res = await api('claude-local-bridge-toggle', { enable: true });
387
387
  if (res.error) {
388
- this.showMessage(res.error || '启用本地负载均衡失败', 'error');
388
+ this.showMessage(res.error || this.t('toast.claude.balanceEnableFail'), 'error');
389
389
  return;
390
390
  }
391
- this.showMessage('Claude 本地负载均衡已启用', 'success');
391
+ this.showMessage(this.t('toast.claude.balanceEnabled'), 'success');
392
392
  } catch (e) {
393
- this.showMessage('启用本地负载均衡失败', 'error');
393
+ this.showMessage(this.t('toast.claude.balanceEnableFail'), 'error');
394
394
  }
395
395
  },
396
396
 
@@ -405,7 +405,7 @@ export function createClaudeConfigMethods(options = {}) {
405
405
  this.configTemplateContext = 'claude';
406
406
  this.showConfigTemplateModal = true;
407
407
  } catch (e) {
408
- this.showMessage('加载 Claude settings 失败', 'error');
408
+ this.showMessage(this.t('toast.claude.loadSettingsFail'), 'error');
409
409
  }
410
410
  }
411
411
  };
@@ -66,7 +66,7 @@ export function createCodexConfigMethods(options = {}) {
66
66
  const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
67
67
  this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
68
68
  } else {
69
- this.showMessage('操作成功', 'success');
69
+ this.showMessage(this.t('toast.operation.success'), 'success');
70
70
  }
71
71
  } catch (e) {
72
72
  this.showMessage('导出失败', 'error');
@@ -325,7 +325,7 @@ export function createCodexConfigMethods(options = {}) {
325
325
  if (hasResponseError(res)) {
326
326
  this.healthCheckResult = null;
327
327
  if (!silent) {
328
- this.showMessage(getResponseMessage(res, '检查失败'), 'error');
328
+ this.showMessage(getResponseMessage(res, this.t('toast.check.fail')), 'error');
329
329
  }
330
330
  return;
331
331
  }
@@ -342,13 +342,13 @@ export function createCodexConfigMethods(options = {}) {
342
342
  this.healthCheckBatchDone = total;
343
343
  this.healthCheckBatchFailed = errors + warns;
344
344
  if (!silent && res.ok) {
345
- this.showMessage('检查通过', 'success');
345
+ this.showMessage(this.t('toast.check.success'), 'success');
346
346
  }
347
347
  return;
348
348
  }
349
349
  this.healthCheckResult = null;
350
350
  if (!silent) {
351
- this.showMessage('检查失败', 'error');
351
+ this.showMessage(this.t('toast.check.fail'), 'error');
352
352
  }
353
353
  return;
354
354
  }
@@ -473,7 +473,7 @@ export function createCodexConfigMethods(options = {}) {
473
473
  } else {
474
474
  this.healthCheckResult = null;
475
475
  if (!silent) {
476
- this.showMessage('检查失败', 'error');
476
+ this.showMessage(this.t('toast.check.fail'), 'error');
477
477
  }
478
478
  }
479
479
  } catch (e) {
@@ -561,7 +561,7 @@ export function createCodexConfigMethods(options = {}) {
561
561
  this.configTemplateContext = 'codex';
562
562
  this.showConfigTemplateModal = true;
563
563
  } catch (e) {
564
- this.showMessage('加载模板失败', 'error');
564
+ this.showMessage(this.t('toast.template.loadFail'), 'error');
565
565
  }
566
566
  },
567
567
 
@@ -797,7 +797,7 @@ export function createCodexConfigMethods(options = {}) {
797
797
  return;
798
798
  }
799
799
  if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
800
- this.showMessage('模板不能为空', 'error');
800
+ this.showMessage(this.t('toast.template.empty'), 'error');
801
801
  return;
802
802
  }
803
803
 
@@ -822,15 +822,15 @@ export function createCodexConfigMethods(options = {}) {
822
822
  this.showMessage(res.error, 'error');
823
823
  return;
824
824
  }
825
- this.showMessage('模板已应用', 'success');
825
+ this.showMessage(this.t('toast.template.applied'), 'success');
826
826
  this.closeConfigTemplateModal({ force: true });
827
827
  try {
828
828
  await this.loadAll();
829
829
  } catch (_) {
830
- this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
830
+ this.showMessage(this.t('toast.template.appliedButRefreshFail'), 'error');
831
831
  }
832
832
  } catch (e) {
833
- this.showMessage('应用模板失败', 'error');
833
+ this.showMessage(this.t('toast.template.applyFail'), 'error');
834
834
  } finally {
835
835
  this.configTemplateApplying = false;
836
836
  }
@@ -274,7 +274,7 @@ export function createOpenclawPersistMethods(options = {}) {
274
274
  try {
275
275
  const name = this.persistOpenclawConfig();
276
276
  if (!name) return;
277
- this.showMessage('操作成功', 'success');
277
+ this.showMessage(this.t('toast.operation.success'), 'success');
278
278
  } finally {
279
279
  this.openclawSaving = false;
280
280
  }
@@ -170,7 +170,7 @@ export function createProvidersMethods(options = {}) {
170
170
  normalizeProviderDraftState(this.newProvider);
171
171
  const validation = getProviderValidationForContext(this, 'add');
172
172
  if (!validation.ok) {
173
- return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || '名称、URL、API Key 和模型名称必填', 'error');
173
+ return this.showMessage(validation.errors.name || validation.errors.url || validation.errors.key || validation.errors.model || this.t('toast.provider.fieldsRequired'), 'error');
174
174
  }
175
175
 
176
176
  try {
@@ -206,7 +206,7 @@ export function createProvidersMethods(options = {}) {
206
206
  };
207
207
  this.providersList = [...this.providersList, newProvider];
208
208
 
209
- this.showMessage('操作成功', 'success');
209
+ this.showMessage(this.t('toast.operation.success'), 'success');
210
210
  this.closeAddModal();
211
211
 
212
212
  if (suggestedModel) {
@@ -214,7 +214,7 @@ export function createProvidersMethods(options = {}) {
214
214
  this.currentModels[validation.name] = suggestedModel;
215
215
  }
216
216
  } catch (e) {
217
- this.showMessage('添加失败', 'error');
217
+ this.showMessage(this.t('toast.provider.addFail'), 'error');
218
218
  }
219
219
  },
220
220
 
@@ -275,7 +275,7 @@ export function createProvidersMethods(options = {}) {
275
275
 
276
276
  async deleteProvider(name) {
277
277
  if (this.isNonDeletableProvider(name)) {
278
- this.showMessage('provider 为保留项,不可删除', 'info');
278
+ this.showMessage(this.t('toast.provider.notDeletable'), 'info');
279
279
  return;
280
280
  }
281
281
  try {
@@ -301,12 +301,13 @@ export function createProvidersMethods(options = {}) {
301
301
  ...p,
302
302
  current: p.name === res.provider
303
303
  }));
304
- this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
304
+ const modelSuffix = res.model ? ` / ${res.model}` : '';
305
+ this.showMessage(this.t('toast.provider.deletedAndSwitched', { provider: res.provider, model: modelSuffix }), 'success');
305
306
  } else {
306
- this.showMessage('操作成功', 'success');
307
+ this.showMessage(this.t('toast.operation.success'), 'success');
307
308
  }
308
309
  } catch (_) {
309
- this.showMessage('删除失败', 'error');
310
+ this.showMessage(this.t('toast.delete.fail'), 'error');
310
311
  }
311
312
  },
312
313
 
@@ -330,7 +331,7 @@ export function createProvidersMethods(options = {}) {
330
331
  const requestId = Symbol('openEditModal');
331
332
  this._openEditModalRequestId = requestId;
332
333
  if (!this.shouldShowProviderEdit(provider)) {
333
- this.showMessage('provider 为保留项,不可编辑', 'info');
334
+ this.showMessage(this.t('toast.provider.notEditable'), 'info');
334
335
  return;
335
336
  }
336
337
  const isTransformProvider = (() => {
@@ -398,14 +399,14 @@ export function createProvidersMethods(options = {}) {
398
399
 
399
400
  async updateProvider() {
400
401
  if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
401
- this.showMessage('provider 为保留项,不可编辑', 'error');
402
+ this.showMessage(this.t('toast.provider.notEditable'), 'error');
402
403
  this.closeEditModal();
403
404
  return;
404
405
  }
405
406
  normalizeProviderDraftState(this.editingProvider);
406
407
  const validation = getProviderValidationForContext(this, 'edit');
407
408
  if (!validation.ok) {
408
- return this.showMessage(validation.errors.name || validation.errors.url || 'URL 必填', 'error');
409
+ return this.showMessage(validation.errors.name || validation.errors.url || this.t('toast.provider.urlRequired'), 'error');
409
410
  }
410
411
 
411
412
  const params = { name: validation.name, url: validation.url };
@@ -441,9 +442,9 @@ export function createProvidersMethods(options = {}) {
441
442
  });
442
443
 
443
444
  this.closeEditModal();
444
- this.showMessage('操作成功', 'success');
445
+ this.showMessage(this.t('toast.operation.success'), 'success');
445
446
  } catch (e) {
446
- this.showMessage('更新失败', 'error');
447
+ this.showMessage(this.t('toast.provider.updateFail'), 'error');
447
448
  }
448
449
  },
449
450
 
@@ -469,10 +470,10 @@ export function createProvidersMethods(options = {}) {
469
470
  return;
470
471
  }
471
472
  const backup = res.backupFile ? `(已备份: ${res.backupFile})` : '';
472
- this.showMessage(`配置已重装${backup}`, 'success');
473
+ this.showMessage(this.t('toast.provider.resetSuccess', { backup }), 'success');
473
474
  await this.loadAll();
474
475
  } catch (e) {
475
- this.showMessage('重装失败', 'error');
476
+ this.showMessage(this.t('toast.provider.resetFail'), 'error');
476
477
  } finally {
477
478
  this.resetConfigLoading = false;
478
479
  }
@@ -501,7 +502,7 @@ export function createProvidersMethods(options = {}) {
501
502
  }
502
503
  return p;
503
504
  });
504
- this.showMessage('操作成功', 'success');
505
+ this.showMessage(this.t('toast.operation.success'), 'success');
505
506
  this.closeModelModal();
506
507
  }
507
508
  } catch (_) {
@@ -525,7 +526,7 @@ export function createProvidersMethods(options = {}) {
525
526
  }
526
527
  return p;
527
528
  });
528
- this.showMessage('操作成功', 'success');
529
+ this.showMessage(this.t('toast.operation.success'), 'success');
529
530
  }
530
531
  } catch (_) {
531
532
  this.showMessage('删除模型失败', 'error');
@@ -4,21 +4,21 @@ import {
4
4
  } from '../logic.mjs';
5
5
 
6
6
  const UI_MESSAGE_KEY_BY_TEXT = Object.freeze({
7
- '操作成功': 'toast.operationSuccess',
8
- '操作失败': 'toast.operationFailed',
9
- '添加失败': 'toast.addFailed',
10
- '更新失败': 'toast.updateFailed',
11
- '删除失败': 'toast.deleteFailed',
12
- '已删除': 'toast.deleted',
13
- '已复制': 'toast.copied',
14
- '复制失败': 'toast.copyFailed',
7
+ '操作成功': 'toast.operation.success',
8
+ '操作失败': 'toast.operation.fail',
9
+ '添加失败': 'toast.provider.addFail',
10
+ '更新失败': 'toast.provider.updateFail',
11
+ '删除失败': 'toast.delete.fail',
12
+ '已删除': 'toast.delete.ok',
13
+ '已复制': 'toast.copy.ok',
14
+ '复制失败': 'toast.copy.fail',
15
15
  '剪贴板为空': 'toast.clipboardEmpty',
16
16
  '无法读取剪贴板': 'toast.clipboardReadFailed',
17
17
  '已粘贴': 'toast.pasted',
18
18
  '未检测到改动': 'toast.noChanges',
19
- '配置已应用': 'toast.configApplied',
20
- '应用配置失败': 'toast.applyConfigFailed',
21
- '应用失败': 'toast.applyFailed',
19
+ '配置已应用': 'toast.apply.success',
20
+ '应用配置失败': 'toast.apply.fail',
21
+ '应用失败': 'toast.apply.fail',
22
22
  '配置已加载': 'toast.configLoaded',
23
23
  '配置就绪': 'toast.configReady',
24
24
  '加载配置失败': 'toast.loadConfigFailed',
@@ -26,12 +26,12 @@ const UI_MESSAGE_KEY_BY_TEXT = Object.freeze({
26
26
  '读取配置超时': 'toast.readConfigTimeout',
27
27
  '备份失败': 'toast.backupFailed',
28
28
  '备份成功,开始下载': 'toast.backupReadyDownload',
29
- '导入失败': 'toast.importFailed',
30
- '导入成功': 'toast.importSuccess',
29
+ '导入失败': 'toast.import.fail',
30
+ '导入成功': 'toast.import.ok',
31
31
  '导入 skill 失败': 'toast.importSkillFailed',
32
- '导出失败': 'toast.exportFailed',
33
- '保存失败': 'toast.saveFailed',
34
- '加载文件失败': 'toast.loadFileFailed',
32
+ '导出失败': 'toast.export.fail',
33
+ '保存失败': 'toast.save.fail',
34
+ '加载文件失败': 'toast.load.fail',
35
35
  '请填写名称': 'toast.nameRequired',
36
36
  '请输入名称': 'toast.nameRequired',
37
37
  '名称已存在': 'toast.nameExists',
@@ -45,8 +45,8 @@ const UI_MESSAGE_KEY_BY_TEXT = Object.freeze({
45
45
  '不可分享': 'toast.notShareable',
46
46
  '已移入回收站': 'toast.movedToTrash',
47
47
  '生成命令失败': 'toast.commandGenerationFailed',
48
- '没有可复制内容': 'toast.nothingToCopy',
49
- '没有可导出内容': 'toast.nothingToExport',
48
+ '没有可复制内容': 'toast.copy.empty',
49
+ '没有可导出内容': 'toast.export.empty',
50
50
  '会话已恢复': 'toast.sessionRestored',
51
51
  '恢复失败': 'toast.restoreFailed',
52
52
  '已彻底删除': 'toast.purged',
@@ -66,16 +66,22 @@ const UI_MESSAGE_PREFIX_ENTRIES = Object.freeze(
66
66
  Object.entries(UI_MESSAGE_KEY_BY_TEXT).sort((a, b) => b[0].length - a[0].length)
67
67
  );
68
68
 
69
- function translateUiMessage(context, text) {
69
+ export function translateUiMessage(context, text) {
70
70
  if (!context || typeof context.t !== 'function' || typeof text !== 'string') return text;
71
+ const translateKey = (key) => {
72
+ const translated = context.t(key);
73
+ return typeof translated === 'string' && translated && translated !== key ? translated : '';
74
+ };
71
75
  const exactKey = UI_MESSAGE_KEY_BY_TEXT[text];
72
- if (exactKey) return context.t(exactKey);
76
+ if (exactKey) return translateKey(exactKey) || text;
73
77
  const prefixEntry = UI_MESSAGE_PREFIX_ENTRIES.find(([sourceText]) => {
74
78
  return text.length > sourceText.length && text.startsWith(sourceText);
75
79
  });
76
80
  if (!prefixEntry) return text;
77
81
  const [sourceText, key] = prefixEntry;
78
- return `${context.t(key)}${text.slice(sourceText.length)}`;
82
+ const translatedPrefix = translateKey(key);
83
+ if (!translatedPrefix) return text;
84
+ return `${translatedPrefix}${text.slice(sourceText.length)}`;
79
85
  }
80
86
 
81
87
  function clearProgressResetTimer(context, timerKey) {
@@ -126,7 +126,7 @@ export function createSessionActionMethods(options = {}) {
126
126
  return;
127
127
  }
128
128
  } catch (_) {}
129
- this.showMessage('复制失败', 'error');
129
+ this.showMessage(this.t('toast.copy.fail'), 'error');
130
130
  },
131
131
 
132
132
  getSessionFilePath(session) {
@@ -152,7 +152,7 @@ export function createSessionActionMethods(options = {}) {
152
152
  return;
153
153
  }
154
154
  } catch (_) {}
155
- this.showMessage('复制失败', 'error');
155
+ this.showMessage(this.t('toast.copy.fail'), 'error');
156
156
  },
157
157
 
158
158
  getSessionExportKey(session) {
@@ -312,15 +312,15 @@ export function createSessionActionMethods(options = {}) {
312
312
  copyAgentsContent() {
313
313
  const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
314
314
  if (!text) {
315
- this.showMessage('没有可复制内容', 'info');
315
+ this.showMessage(this.t('toast.copy.empty'), 'info');
316
316
  return;
317
317
  }
318
318
  const ok = this.fallbackCopyText(text);
319
319
  if (ok) {
320
- this.showMessage('已复制', 'success');
320
+ this.showMessage(this.t('toast.copy.ok'), 'success');
321
321
  return;
322
322
  }
323
- this.showMessage('复制失败', 'error');
323
+ this.showMessage(this.t('toast.copy.fail'), 'error');
324
324
  },
325
325
 
326
326
  exportAgentsContent() {
@@ -344,7 +344,7 @@ export function createSessionActionMethods(options = {}) {
344
344
  async copyInstallCommand(cmd) {
345
345
  const text = typeof cmd === 'string' ? cmd.trim() : '';
346
346
  if (!text) {
347
- this.showMessage('没有可复制内容', 'info');
347
+ this.showMessage(this.t('toast.copy.empty'), 'info');
348
348
  return;
349
349
  }
350
350
  try {
@@ -359,7 +359,7 @@ export function createSessionActionMethods(options = {}) {
359
359
  this.showMessage('已复制命令', 'success');
360
360
  return;
361
361
  }
362
- this.showMessage('复制失败', 'error');
362
+ this.showMessage(this.t('toast.copy.fail'), 'error');
363
363
  },
364
364
 
365
365
  async copyResumeCommand(session) {
@@ -370,17 +370,17 @@ export function createSessionActionMethods(options = {}) {
370
370
  const command = this.buildResumeCommand(session);
371
371
  const ok = this.fallbackCopyText(command);
372
372
  if (ok) {
373
- this.showMessage('已复制', 'success');
373
+ this.showMessage(this.t('toast.copy.ok'), 'success');
374
374
  return;
375
375
  }
376
376
  try {
377
377
  if (navigator.clipboard && window.isSecureContext) {
378
378
  await navigator.clipboard.writeText(command);
379
- this.showMessage('已复制', 'success');
379
+ this.showMessage(this.t('toast.copy.ok'), 'success');
380
380
  return;
381
381
  }
382
382
  } catch (_) {}
383
- this.showMessage('复制失败', 'error');
383
+ this.showMessage(this.t('toast.copy.fail'), 'error');
384
384
  },
385
385
 
386
386
  buildProviderShareCommand(payload) {
@@ -446,17 +446,17 @@ export function createSessionActionMethods(options = {}) {
446
446
  }
447
447
  const ok = this.fallbackCopyText(command);
448
448
  if (ok) {
449
- this.showMessage('已复制', 'success');
449
+ this.showMessage(this.t('toast.copy.ok'), 'success');
450
450
  return;
451
451
  }
452
452
  try {
453
453
  if (navigator.clipboard && window.isSecureContext) {
454
454
  await navigator.clipboard.writeText(command);
455
- this.showMessage('已复制', 'success');
455
+ this.showMessage(this.t('toast.copy.ok'), 'success');
456
456
  return;
457
457
  }
458
458
  } catch (_) {}
459
- this.showMessage('复制失败', 'error');
459
+ this.showMessage(this.t('toast.copy.fail'), 'error');
460
460
  } catch (_) {
461
461
  this.showMessage('生成命令失败', 'error');
462
462
  } finally {
@@ -485,17 +485,17 @@ export function createSessionActionMethods(options = {}) {
485
485
  }
486
486
  const ok = this.fallbackCopyText(command);
487
487
  if (ok) {
488
- this.showMessage('已复制', 'success');
488
+ this.showMessage(this.t('toast.copy.ok'), 'success');
489
489
  return;
490
490
  }
491
491
  try {
492
492
  if (navigator.clipboard && window.isSecureContext) {
493
493
  await navigator.clipboard.writeText(command);
494
- this.showMessage('已复制', 'success');
494
+ this.showMessage(this.t('toast.copy.ok'), 'success');
495
495
  return;
496
496
  }
497
497
  } catch (_) {}
498
- this.showMessage('复制失败', 'error');
498
+ this.showMessage(this.t('toast.copy.fail'), 'error');
499
499
  } catch (_) {
500
500
  this.showMessage('生成命令失败', 'error');
501
501
  } finally {
@@ -524,7 +524,7 @@ export function createSessionActionMethods(options = {}) {
524
524
  return;
525
525
  }
526
526
 
527
- this.showMessage('操作成功', 'success');
527
+ this.showMessage(this.t('toast.operation.success'), 'success');
528
528
  if (typeof this.invalidateSessionsUsageData === 'function') {
529
529
  this.invalidateSessionsUsageData({ preserveList: true });
530
530
  }
@@ -608,7 +608,7 @@ export function createSessionActionMethods(options = {}) {
608
608
  // The delete already succeeded remotely; keep the success result.
609
609
  }
610
610
  } catch (_) {
611
- this.showMessage('删除失败', 'error');
611
+ this.showMessage(this.t('toast.delete.fail'), 'error');
612
612
  } finally {
613
613
  this.sessionDeleting[key] = false;
614
614
  }
@@ -125,6 +125,9 @@ export function createStartupClaudeMethods(options = {}) {
125
125
  codex: statusRes.toolConfigPermissions.codex === true,
126
126
  claude: statusRes.toolConfigPermissions.claude === true
127
127
  };
128
+ try {
129
+ localStorage.setItem('toolConfigPermissions', JSON.stringify(this.toolConfigPermissions));
130
+ } catch (_) {}
128
131
  }
129
132
  this.providersList = listRes.providers;
130
133
  if (typeof this.loadLocalBridgeExcluded === 'function') { this.loadLocalBridgeExcluded(); }
@@ -64,6 +64,9 @@ export function createToolConfigPermissionMethods(options = {}) {
64
64
  return;
65
65
  }
66
66
  this.toolConfigPermissions = normalizePermissions(res && res.permissions);
67
+ try {
68
+ localStorage.setItem('toolConfigPermissions', JSON.stringify(this.toolConfigPermissions));
69
+ } catch (_) {}
67
70
  this.showMessage(
68
71
  nextAllowWrite
69
72
  ? this.t('toolConfig.allowToast')
@@ -61,6 +61,8 @@ const en = Object.freeze({
61
61
  'common.notExistsWillCreateOnApply': 'Not found. Will be created on apply.',
62
62
  'common.notExistsWillCreateOnSave': 'Not found. Will be created on save.',
63
63
  'common.none': 'None',
64
+ 'common.configured': 'Configured',
65
+ 'common.notConfigured': 'Not configured',
64
66
  'cli.missing.title': '{name} CLI not installed',
65
67
  'cli.missing.subtitle': 'Install {name} CLI before using this page.',
66
68
  'cli.missing.openDocs': 'Open install guide',
@@ -358,7 +360,7 @@ const en = Object.freeze({
358
360
  'plugins.builtin.commentPolish.desc': 'Polish the following code comments {{code}}',
359
361
  'plugins.builtin.commentPolish.line1': 'Polish the following code comments',
360
362
  'plugins.builtin.ruleAck.name': 'Rule acknowledgement',
361
- 'plugins.builtin.ruleAck.desc': 'Please follow 【{{rule}}】, reply when received',
363
+ 'plugins.builtin.ruleAck.desc': 'Generate rule acknowledgement reply',
362
364
  'plugins.builtin.ruleAck.line1': 'Please follow 【{{rule}}】, reply when received',
363
365
 
364
366
  // Toasts
@@ -385,6 +387,50 @@ const en = Object.freeze({
385
387
  'toast.templates.nameRequired': 'Template name is required',
386
388
  'toast.templates.builtinNotDuplicable': 'Built-in templates cannot be duplicated',
387
389
  'toast.templates.builtinNotDeletable': 'Built-in templates cannot be deleted',
390
+ 'toast.operation.success': 'Operation successful',
391
+ 'toast.load.fail': 'Failed to load file',
392
+ 'toast.apply.success': 'Configuration applied',
393
+ 'toast.apply.fail': 'Failed to apply configuration',
394
+ 'toast.check.success': 'Check passed',
395
+ 'toast.check.fail': 'Check failed',
396
+ 'toast.noChanges': 'No changes detected',
397
+ 'toast.template.loadFail': 'Failed to load template',
398
+ 'toast.template.empty': 'Template cannot be empty',
399
+ 'toast.template.applied': 'Template applied',
400
+ 'toast.template.appliedButRefreshFail': 'Template applied, but UI refresh failed. Please refresh manually',
401
+ 'toast.template.applyFail': 'Failed to apply template',
402
+ 'toast.provider.addFail': 'Failed to add',
403
+ 'toast.provider.notDeletable': 'This provider is reserved and cannot be deleted',
404
+ 'toast.provider.deletedAndSwitched': 'Provider deleted, auto-switched to {provider}{model}',
405
+ 'toast.provider.notEditable': 'This provider is reserved and cannot be edited',
406
+ 'toast.provider.updateFail': 'Failed to update',
407
+ 'toast.provider.resetSuccess': 'Config reset{backup}',
408
+ 'toast.provider.resetFail': 'Reset failed',
409
+ 'toast.provider.fieldsRequired': 'Name, URL, API Key and model are required',
410
+ 'toast.provider.urlRequired': 'URL is required',
411
+ 'toast.claude.modelRequired': 'Please enter model',
412
+ 'toast.claude.apiKeyRequired': 'Please configure API Key first',
413
+ 'toast.claude.checkConfig': 'Please check Claude configuration',
414
+ 'toast.claude.savedWithoutKey': 'Saved (API Key not filled)',
415
+ 'toast.claude.applied': 'Claude config applied',
416
+ 'toast.claude.exists': 'Config already exists',
417
+ 'toast.claude.keepOne': 'Keep at least one',
418
+ 'toast.claude.externalAuth': 'Using external auth, no API Key needed',
419
+ 'toast.claude.balanceEnabled': 'Claude local load balancing enabled',
420
+ 'toast.claude.balanceDisabled': 'Claude local load balancing disabled',
421
+ 'toast.claude.balanceEnableFail': 'Failed to enable load balancing',
422
+ 'toast.claude.balanceRequireProvider': 'Please add and configure at least one Claude provider',
423
+ 'toast.claude.loadSettingsFail': 'Failed to load Claude settings',
424
+ 'validation.claude.nameRequired': 'Config name is required',
425
+ 'validation.claude.nameExists': 'Name already exists',
426
+ 'validation.claude.apiKeyRequired': 'API Key is required',
427
+ 'validation.claude.baseUrlRequired': 'Base URL is required',
428
+ 'validation.claude.baseUrlHttpOnly': 'Base URL only supports http/https',
429
+ 'validation.claude.modelRequired': 'Model name is required',
430
+ 'modal.claudeDelete.title': 'Delete Claude config',
431
+ 'modal.claudeDelete.message': 'Delete config "{name}"?',
432
+ 'modal.claudeDelete.confirm': 'Delete',
433
+ 'modal.claudeDelete.cancel': 'Cancel',
388
434
  'toast.templates.deleteTitle': 'Delete template',
389
435
  'toast.templates.deleteMessage': 'Delete “{name}”? This action cannot be undone.',
390
436
  'toast.templates.deleteConfirm': 'Delete',
@@ -62,6 +62,8 @@ const ja = Object.freeze({
62
62
  'common.notExistsWillCreateOnApply': '存在しません。適用時に作成されます',
63
63
  'common.notExistsWillCreateOnSave': '存在しません。保存時に作成されます',
64
64
  'common.none': 'なし',
65
+ 'common.configured': '設定済み',
66
+ 'common.notConfigured': '未設定',
65
67
  'cli.missing.title': '{name} CLI がインストールされていません',
66
68
  'cli.missing.subtitle': '{name} CLI をインストールしてからこのページをご利用ください。',
67
69
  'cli.missing.openDocs': 'インストールガイドを開く',
@@ -360,7 +362,7 @@ const ja = Object.freeze({
360
362
  'plugins.builtin.commentPolish.desc': '以下のコードコメントを軽く整えてください {{code}}',
361
363
  'plugins.builtin.commentPolish.line1': '以下のコードコメントを軽く整えてください',
362
364
  'plugins.builtin.ruleAck.name': 'ルール確認返信',
363
- 'plugins.builtin.ruleAck.desc': '【{{rule}}】に従って、受信確認を返してください',
365
+ 'plugins.builtin.ruleAck.desc': 'ルールに従うよう確認返信を指示',
364
366
  'plugins.builtin.ruleAck.line1': '【{{rule}}】に従って、受信確認を返してください',
365
367
 
366
368
  // Toasts
@@ -391,6 +393,50 @@ const ja = Object.freeze({
391
393
  'toast.templates.deleteMessage': '「{name}」を削除しますか?この操作は取り消せません。',
392
394
  'toast.templates.deleteConfirm': '削除',
393
395
  'toast.templates.deleteCancel': 'キャンセル',
396
+ 'toast.operation.success': '操作が成功しました',
397
+ 'toast.load.fail': 'ファイルの読み込みに失敗しました',
398
+ 'toast.apply.success': '設定が適用されました',
399
+ 'toast.apply.fail': '設定の適用に失敗しました',
400
+ 'toast.check.success': 'チェック成功',
401
+ 'toast.check.fail': 'チェック失敗',
402
+ 'toast.noChanges': '変更が検出されませんでした',
403
+ 'toast.template.loadFail': 'テンプレートの読み込みに失敗しました',
404
+ 'toast.template.empty': 'テンプレートは空にできません',
405
+ 'toast.template.applied': 'テンプレートが適用されました',
406
+ 'toast.template.appliedButRefreshFail': 'テンプレートは適用されましたが、UI の更新に失敗しました。手動で更新してください',
407
+ 'toast.template.applyFail': 'テンプレートの適用に失敗しました',
408
+ 'toast.provider.addFail': '追加に失敗しました',
409
+ 'toast.provider.notDeletable': 'このプロバイダーは予約済みのため削除できません',
410
+ 'toast.provider.deletedAndSwitched': 'プロバイダーを削除し、{provider}{model}に自動切り替えしました',
411
+ 'toast.provider.notEditable': 'このプロバイダーは予約済みのため編集できません',
412
+ 'toast.provider.updateFail': '更新に失敗しました',
413
+ 'toast.provider.resetSuccess': '設定をリセットしました{backup}',
414
+ 'toast.provider.resetFail': 'リセットに失敗しました',
415
+ 'toast.provider.fieldsRequired': '名前、URL、API Key、モデル名は必須です',
416
+ 'toast.provider.urlRequired': 'URL は必須です',
417
+ 'toast.claude.modelRequired': 'モデルを入力してください',
418
+ 'toast.claude.apiKeyRequired': '先に API Key を設定してください',
419
+ 'toast.claude.checkConfig': 'Claude 設定を確認してください',
420
+ 'toast.claude.savedWithoutKey': '保存しました(API Key 未入力)',
421
+ 'toast.claude.applied': 'Claude 設定が適用されました',
422
+ 'toast.claude.exists': '設定が既に存在します',
423
+ 'toast.claude.keepOne': '最低1つは残してください',
424
+ 'toast.claude.externalAuth': '外部認証を使用するため、API Key は不要です',
425
+ 'toast.claude.balanceEnabled': 'Claude ローカル負荷分散が有効になりました',
426
+ 'toast.claude.balanceDisabled': 'Claude ローカル負荷分散が無効になりました',
427
+ 'toast.claude.balanceEnableFail': '負荷分散の有効化に失敗しました',
428
+ 'toast.claude.balanceRequireProvider': '最低1つの Claude プロバイダーを追加・設定してください',
429
+ 'toast.claude.loadSettingsFail': 'Claude settings の読み込みに失敗しました',
430
+ 'validation.claude.nameRequired': '設定名は必須です',
431
+ 'validation.claude.nameExists': '名前が既に存在します',
432
+ 'validation.claude.apiKeyRequired': 'API Key は必須です',
433
+ 'validation.claude.baseUrlRequired': 'Base URL は必須です',
434
+ 'validation.claude.baseUrlHttpOnly': 'Base URL は http/https のみサポートします',
435
+ 'validation.claude.modelRequired': 'モデル名は必須です',
436
+ 'modal.claudeDelete.title': 'Claude 設定の削除',
437
+ 'modal.claudeDelete.message': '設定 "{name}" を削除しますか?',
438
+ 'modal.claudeDelete.confirm': '削除',
439
+ 'modal.claudeDelete.cancel': 'キャンセル',
394
440
 
395
441
  // Basic modals
396
442
  'modal.providerAdd.title': 'プロバイダー追加',
@@ -6,7 +6,7 @@ const vi = Object.freeze({
6
6
  'plugins.builtin.commentPolish.desc': 'Chỉnh nhẹ các chú thích mã sau {{code}}',
7
7
  'plugins.builtin.commentPolish.line1': 'Chỉnh nhẹ các chú thích mã sau',
8
8
  'plugins.builtin.ruleAck.name': 'Xác nhận quy tắc',
9
- 'plugins.builtin.ruleAck.desc': 'Hãy làm theo【{{rule}}】, nhận được thì phản hồi',
9
+ 'plugins.builtin.ruleAck.desc': 'Tạo phản hồi xác nhận quy tắc',
10
10
  'plugins.builtin.ruleAck.line1': 'Hãy làm theo【{{rule}}】, nhận được thì phản hồi',
11
11
  // Global
12
12
  'lang.zh': 'Tiếng Trung',
@@ -71,6 +71,8 @@ const vi = Object.freeze({
71
71
  'common.notExistsWillCreateOnApply': 'Không tồn tại. Sẽ được tạo khi áp dụng.',
72
72
  'common.notExistsWillCreateOnSave': 'Không tồn tại. Sẽ được tạo khi lưu.',
73
73
  'common.none': 'Không có',
74
+ 'common.configured': 'Đã cấu hình',
75
+ 'common.notConfigured': 'Chưa cấu hình',
74
76
  'common.notSelected': 'Chưa chọn',
75
77
 
76
78
  // Roles / labels
@@ -233,7 +235,60 @@ const vi = Object.freeze({
233
235
  'dashboard.doctor.title': 'Doctor',
234
236
  'dashboard.doctor.runChecks': 'Chạy kiểm tra',
235
237
  'dashboard.doctor.checking': 'Đang kiểm tra...',
236
- 'dashboard.doctor.export': 'Xuất báo cáo'
238
+ 'dashboard.doctor.export': 'Xuất báo cáo',
239
+
240
+ // Toasts
241
+ 'toast.copy.empty': 'Không có gì để sao chép',
242
+ 'toast.copy.ok': 'Đã sao chép',
243
+ 'toast.copy.fail': 'Sao chép thất bại',
244
+ 'toast.save.ok': 'Đã lưu',
245
+ 'toast.save.fail': 'Lưu thất bại',
246
+ 'toast.delete.ok': 'Đã xóa',
247
+ 'toast.delete.fail': 'Xóa thất bại',
248
+ 'toast.operation.success': 'Thao tác thành công',
249
+ 'toast.load.fail': 'Tải tệp thất bại',
250
+ 'toast.apply.success': 'Đã áp dụng cấu hình',
251
+ 'toast.apply.fail': 'Áp dụng cấu hình thất bại',
252
+ 'toast.check.success': 'Kiểm tra thành công',
253
+ 'toast.check.fail': 'Kiểm tra thất bại',
254
+ 'toast.noChanges': 'Không phát hiện thay đổi',
255
+ 'toast.template.loadFail': 'Tải mẫu thất bại',
256
+ 'toast.template.empty': 'Mẫu không được để trống',
257
+ 'toast.template.applied': 'Đã áp dụng mẫu',
258
+ 'toast.template.appliedButRefreshFail': 'Đã áp dụng mẫu, nhưng làm mới giao diện thất bại. Vui lòng làm mới thủ công',
259
+ 'toast.template.applyFail': 'Áp dụng mẫu thất bại',
260
+ 'toast.provider.addFail': 'Thêm thất bại',
261
+ 'toast.provider.notDeletable': 'Provider này là mục dành riêng, không thể xóa',
262
+ 'toast.provider.deletedAndSwitched': 'Đã xóa provider, tự động chuyển sang {provider}{model}',
263
+ 'toast.provider.notEditable': 'Provider này là mục dành riêng, không thể chỉnh sửa',
264
+ 'toast.provider.updateFail': 'Cập nhật thất bại',
265
+ 'toast.provider.resetSuccess': 'Đã cài đặt lại cấu hình{backup}',
266
+ 'toast.provider.resetFail': 'Cài đặt lại thất bại',
267
+ 'toast.provider.fieldsRequired': 'Tên, URL, API Key và tên mô hình là bắt buộc',
268
+ 'toast.provider.urlRequired': 'URL là bắt buộc',
269
+ 'toast.claude.modelRequired': 'Vui lòng nhập mô hình',
270
+ 'toast.claude.apiKeyRequired': 'Vui lòng cấu hình API Key trước',
271
+ 'toast.claude.checkConfig': 'Vui lòng kiểm tra cấu hình Claude',
272
+ 'toast.claude.savedWithoutKey': 'Đã lưu (chưa điền API Key)',
273
+ 'toast.claude.applied': 'Cấu hình Claude đã được áp dụng',
274
+ 'toast.claude.exists': 'Cấu hình đã tồn tại',
275
+ 'toast.claude.keepOne': 'Giữ ít nhất một mục',
276
+ 'toast.claude.externalAuth': 'Sử dụng xác thực bên ngoài, không cần API Key',
277
+ 'toast.claude.balanceEnabled': 'Đã bật cân bằng tải cục bộ Claude',
278
+ 'toast.claude.balanceDisabled': 'Đã tắt cân bằng tải cục bộ Claude',
279
+ 'toast.claude.balanceEnableFail': 'Không thể bật cân bằng tải',
280
+ 'toast.claude.balanceRequireProvider': 'Vui lòng thêm và cấu hình ít nhất một nhà cung cấp Claude',
281
+ 'toast.claude.loadSettingsFail': 'Tải cài đặt Claude thất bại',
282
+ 'validation.claude.nameRequired': 'Tên cấu hình là bắt buộc',
283
+ 'validation.claude.nameExists': 'Tên đã tồn tại',
284
+ 'validation.claude.apiKeyRequired': 'API Key là bắt buộc',
285
+ 'validation.claude.baseUrlRequired': 'Base URL là bắt buộc',
286
+ 'validation.claude.baseUrlHttpOnly': 'Base URL chỉ hỗ trợ http/https',
287
+ 'validation.claude.modelRequired': 'Tên mô hình là bắt buộc',
288
+ 'modal.claudeDelete.title': 'Xóa cấu hình Claude',
289
+ 'modal.claudeDelete.message': 'Xóa cấu hình "{name}"?',
290
+ 'modal.claudeDelete.confirm': 'Xóa',
291
+ 'modal.claudeDelete.cancel': 'Hủy',
237
292
  });
238
293
 
239
294
  export { vi };
@@ -61,6 +61,8 @@ const zh = Object.freeze({
61
61
  'common.notExistsWillCreateOnApply': '不存在,将在应用时创建',
62
62
  'common.notExistsWillCreateOnSave': '不存在,将在保存时创建',
63
63
  'common.none': '暂无',
64
+ 'common.configured': '已配置',
65
+ 'common.notConfigured': '未配置',
64
66
  'cli.missing.title': '{name} CLI 未安装',
65
67
  'cli.missing.subtitle': '请先安装 {name} CLI 后再继续使用此页面。',
66
68
  'cli.missing.openDocs': '打开安装指南',
@@ -358,7 +360,7 @@ const zh = Object.freeze({
358
360
  'plugins.builtin.commentPolish.desc': '轻微收敛以下代码注释 {{code}}',
359
361
  'plugins.builtin.commentPolish.line1': '轻微收敛以下代码注释',
360
362
  'plugins.builtin.ruleAck.name': '规则确认回复',
361
- 'plugins.builtin.ruleAck.desc': '请根据【{{rule}}】,收到请回复',
363
+ 'plugins.builtin.ruleAck.desc': '生成规则确认回复',
362
364
  'plugins.builtin.ruleAck.line1': '请根据【{{rule}}】,收到请回复',
363
365
 
364
366
  // Toasts
@@ -386,9 +388,53 @@ const zh = Object.freeze({
386
388
  'toast.templates.builtinNotDuplicable': '内置模板不可复制',
387
389
  'toast.templates.builtinNotDeletable': '内置模板不可删除',
388
390
  'toast.templates.deleteTitle': '删除模板',
389
- 'toast.templates.deleteMessage': '删除“{name}”?此操作无法撤销。',
391
+ 'toast.templates.deleteMessage': '删除”{name}”?此操作无法撤销。',
390
392
  'toast.templates.deleteConfirm': '删除',
391
393
  'toast.templates.deleteCancel': '取消',
394
+ 'toast.operation.success': '操作成功',
395
+ 'toast.load.fail': '加载文件失败',
396
+ 'toast.apply.success': '配置已应用',
397
+ 'toast.apply.fail': '应用配置失败',
398
+ 'toast.check.success': '检查通过',
399
+ 'toast.check.fail': '检查失败',
400
+ 'toast.noChanges': '未检测到改动',
401
+ 'toast.template.loadFail': '加载模板失败',
402
+ 'toast.template.empty': '模板不能为空',
403
+ 'toast.template.applied': '模板已应用',
404
+ 'toast.template.appliedButRefreshFail': '模板已应用,但界面刷新失败,请手动刷新',
405
+ 'toast.template.applyFail': '应用模板失败',
406
+ 'toast.provider.addFail': '添加失败',
407
+ 'toast.provider.notDeletable': '该 provider 为保留项,不可删除',
408
+ 'toast.provider.deletedAndSwitched': '已删除提供商,自动切换到 {provider}{model}',
409
+ 'toast.provider.notEditable': '该 provider 为保留项,不可编辑',
410
+ 'toast.provider.updateFail': '更新失败',
411
+ 'toast.provider.resetSuccess': '配置已重装{backup}',
412
+ 'toast.provider.resetFail': '重装失败',
413
+ 'toast.provider.fieldsRequired': '名称、URL、API Key 和模型名称必填',
414
+ 'toast.provider.urlRequired': 'URL 必填',
415
+ 'toast.claude.modelRequired': '请输入模型',
416
+ 'toast.claude.apiKeyRequired': '请先配置 API Key',
417
+ 'toast.claude.checkConfig': '请检查 Claude 配置',
418
+ 'toast.claude.savedWithoutKey': '已保存(未填写 API Key)',
419
+ 'toast.claude.applied': 'Claude 配置已生效',
420
+ 'toast.claude.exists': '配置已存在',
421
+ 'toast.claude.keepOne': '至少保留一项',
422
+ 'toast.claude.externalAuth': '使用外部认证,无需 API Key',
423
+ 'toast.claude.balanceEnabled': 'Claude 本地负载均衡已启用',
424
+ 'toast.claude.balanceDisabled': 'Claude 本地负载均衡已关闭',
425
+ 'toast.claude.balanceEnableFail': '启用本地负载均衡失败',
426
+ 'toast.claude.balanceRequireProvider': '请先添加并配置至少一个 Claude 提供商',
427
+ 'toast.claude.loadSettingsFail': '加载 Claude settings 失败',
428
+ 'validation.claude.nameRequired': '配置名称不能为空',
429
+ 'validation.claude.nameExists': '名称已存在',
430
+ 'validation.claude.apiKeyRequired': 'API Key 必填',
431
+ 'validation.claude.baseUrlRequired': 'Base URL 必填',
432
+ 'validation.claude.baseUrlHttpOnly': 'Base URL 仅支持 http/https',
433
+ 'validation.claude.modelRequired': '模型名称必填',
434
+ 'modal.claudeDelete.title': '删除 Claude 配置',
435
+ 'modal.claudeDelete.message': '确定删除配置 "{name}"?',
436
+ 'modal.claudeDelete.confirm': '删除',
437
+ 'modal.claudeDelete.cancel': '取消',
392
438
 
393
439
  // Basic modals
394
440
  'modal.providerAdd.title': '添加提供商',
@@ -154,7 +154,7 @@
154
154
  </div>
155
155
  <div class="card-trailing">
156
156
  <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">{{ formatLatency(claudeSpeedResults[name]) }}</span>
157
- <span :class="['pill', config.hasKey ? 'configured' : 'empty']">{{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}</span>
157
+ <span :class="['pill', config.hasKey ? 'configured' : 'empty']">{{ config.hasKey ? t('common.configured') : t('common.notConfigured') }}</span>
158
158
  <div class="card-actions" @click.stop>
159
159
  <button class="card-action-btn" @click="openEditConfigModal(name)" :aria-label="t('claude.action.editAria', { name })" :title="t('claude.action.edit')">
160
160
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
@@ -1926,7 +1926,7 @@ return function render(_ctx, _cache) {
1926
1926
  : _createCommentVNode("v-if", true),
1927
1927
  _createElementVNode("span", {
1928
1928
  class: _normalizeClass(['pill', config.hasKey ? 'configured' : 'empty'])
1929
- }, _toDisplayString(config.hasKey ? _ctx.t('claude.configured') : _ctx.t('claude.notConfigured')), 3 /* TEXT, CLASS */),
1929
+ }, _toDisplayString(config.hasKey ? _ctx.t('common.configured') : _ctx.t('common.notConfigured')), 3 /* TEXT, CLASS */),
1930
1930
  _createElementVNode("div", {
1931
1931
  class: "card-actions",
1932
1932
  onClick: _withModifiers(() => {}, ["stop"])
@@ -75,12 +75,18 @@
75
75
  卡片列表
76
76
  ============================================ */
77
77
  .card-list {
78
- display: flex;
79
- flex-direction: column;
78
+ display: grid;
79
+ grid-template-columns: repeat(2, 1fr);
80
80
  gap: 10px;
81
81
  margin-bottom: 8px;
82
82
  }
83
83
 
84
+ @media (max-width: 768px) {
85
+ .card-list {
86
+ grid-template-columns: 1fr;
87
+ }
88
+ }
89
+
84
90
  /* ============================================
85
91
  卡片
86
92
  ============================================ */
@@ -368,9 +374,9 @@
368
374
  状态徽章
369
375
  ============================================ */
370
376
  .pill {
371
- padding: 5px 11px;
377
+ padding: 4px 9px;
372
378
  border-radius: var(--radius-full);
373
- font-size: var(--font-size-caption);
379
+ font-size: 9px;
374
380
  font-weight: var(--font-weight-caption);
375
381
  background-color: rgba(255, 255, 255, 0.8);
376
382
  color: var(--color-text-tertiary);