codexmate 0.0.10 → 0.0.13

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 (50) hide show
  1. package/README.md +52 -12
  2. package/README.zh-CN.md +52 -12
  3. package/cli.js +3491 -563
  4. package/{CHANGELOG.md → doc/CHANGELOG.md} +6 -0
  5. package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +6 -0
  6. package/lib/mcp-stdio.js +440 -0
  7. package/package.json +22 -2
  8. package/res/logo.png +0 -0
  9. package/web-ui/app.js +1171 -149
  10. package/web-ui/index.html +1605 -0
  11. package/web-ui/logic.mjs +21 -21
  12. package/web-ui/styles.css +3213 -0
  13. package/web-ui.html +7 -3967
  14. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
  15. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
  16. package/.github/workflows/ci.yml +0 -26
  17. package/.github/workflows/release.yml +0 -159
  18. package/.planning/.fix-attempts +0 -1
  19. package/.planning/.lock +0 -6
  20. package/.planning/.verify-cache.json +0 -14
  21. package/.planning/CHECKPOINT.json +0 -46
  22. package/.planning/DESIGN.md +0 -26
  23. package/.planning/HISTORY.json +0 -124
  24. package/.planning/PLAN.md +0 -69
  25. package/.planning/REVIEW.md +0 -41
  26. package/.planning/STATE.md +0 -12
  27. package/.planning/STATS.json +0 -13
  28. package/.planning/VERIFICATION.md +0 -70
  29. package/.planning/daude-code-plan.md +0 -51
  30. package/.planning/research/architecture.md +0 -32
  31. package/.planning/research/conventions.md +0 -36
  32. package/.planning/task_1-REVIEW.md +0 -29
  33. package/.planning/task_1-SUMMARY.md +0 -32
  34. package/.planning/task_2-REVIEW.md +0 -24
  35. package/.planning/task_2-SUMMARY.md +0 -37
  36. package/.planning/task_3-REVIEW.md +0 -25
  37. package/.planning/task_3-SUMMARY.md +0 -31
  38. package/cmd/publish-npm.cmd +0 -65
  39. package/tests/e2e/helpers.js +0 -214
  40. package/tests/e2e/recent-health.e2e.js +0 -142
  41. package/tests/e2e/run.js +0 -154
  42. package/tests/e2e/test-claude.js +0 -21
  43. package/tests/e2e/test-config.js +0 -124
  44. package/tests/e2e/test-health-speed.js +0 -79
  45. package/tests/e2e/test-openclaw.js +0 -47
  46. package/tests/e2e/test-session-search.js +0 -114
  47. package/tests/e2e/test-sessions.js +0 -69
  48. package/tests/e2e/test-setup.js +0 -159
  49. package/tests/unit/run.mjs +0 -29
  50. package/tests/unit/web-ui-logic.test.mjs +0 -186
package/web-ui/app.js CHANGED
@@ -58,6 +58,7 @@
58
58
  currentProvider: '',
59
59
  currentModel: '',
60
60
  serviceTier: 'fast',
61
+ modelReasoningEffort: 'high',
61
62
  providersList: [],
62
63
  models: [],
63
64
  codexModelsLoading: false,
@@ -80,8 +81,10 @@
80
81
  showOpenclawConfigModal: false,
81
82
  showConfigTemplateModal: false,
82
83
  showAgentsModal: false,
84
+ showInstallModal: false,
83
85
  configTemplateContent: '',
84
86
  configTemplateApplying: false,
87
+ codexApplying: false,
85
88
  agentsContent: '',
86
89
  agentsPath: '',
87
90
  agentsExists: false,
@@ -91,9 +94,9 @@
91
94
  agentsContext: 'codex',
92
95
  agentsModalTitle: 'AGENTS.md 编辑器',
93
96
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
94
- sessionsList: [],
95
- sessionsLoading: false,
96
- sessionFilterSource: 'codex',
97
+ sessionsList: [],
98
+ sessionsLoading: false,
99
+ sessionFilterSource: 'all',
97
100
  sessionPathFilter: '',
98
101
  sessionQuery: '',
99
102
  sessionRoleFilter: 'all',
@@ -134,8 +137,35 @@
134
137
  claudeSpeedLoading: {},
135
138
  claudeShareLoading: {},
136
139
  providerShareLoading: {},
140
+ installPackageManager: 'npm',
141
+ installCommandAction: 'install',
142
+ installRegistryPreset: 'default',
143
+ installRegistryCustom: '',
144
+ installStatusTargets: [
145
+ {
146
+ id: 'claude',
147
+ name: 'Claude Code CLI',
148
+ packageName: '@anthropic-ai/claude-code',
149
+ installed: false,
150
+ bin: 'claude',
151
+ version: '',
152
+ commandPath: '',
153
+ error: ''
154
+ },
155
+ {
156
+ id: 'codex',
157
+ name: 'Codex CLI',
158
+ packageName: '@openai/codex',
159
+ installed: false,
160
+ bin: 'codex',
161
+ version: '',
162
+ commandPath: '',
163
+ error: ''
164
+ }
165
+ ],
137
166
  newProvider: { name: '', url: '', key: '' },
138
- editingProvider: { name: '', url: '', key: '' },
167
+ resetConfigLoading: false,
168
+ editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
139
169
  newModelName: '',
140
170
  currentClaudeConfig: '',
141
171
  currentClaudeModel: '',
@@ -201,7 +231,34 @@
201
231
  openclawMissingProviders: [],
202
232
  healthCheckLoading: false,
203
233
  healthCheckResult: null,
204
- healthCheckRemote: false
234
+ healthCheckRemote: false,
235
+ claudeDownloadLoading: false,
236
+ claudeDownloadProgress: 0,
237
+ claudeDownloadTimer: null,
238
+ codexDownloadLoading: false,
239
+ codexDownloadProgress: 0,
240
+ codexDownloadTimer: null,
241
+ claudeImportLoading: false,
242
+ codexImportLoading: false,
243
+ codexAuthProfiles: [],
244
+ codexAuthImportLoading: false,
245
+ codexAuthSwitching: {},
246
+ codexAuthDeleting: {},
247
+ proxySettings: {
248
+ enabled: false,
249
+ host: '127.0.0.1',
250
+ port: 8318,
251
+ provider: '',
252
+ authSource: 'provider',
253
+ timeoutMs: 30000
254
+ },
255
+ proxyRuntime: null,
256
+ proxyLoading: false,
257
+ proxySaving: false,
258
+ proxyStarting: false,
259
+ proxyStopping: false,
260
+ proxyApplying: false,
261
+ showProxyAdvanced: false
205
262
  }
206
263
  },
207
264
  mounted() {
@@ -248,19 +305,19 @@
248
305
  this.loadAll();
249
306
  },
250
307
 
251
- computed: {
252
- isSessionQueryEnabled() {
253
- return isSessionQueryEnabled(this.sessionFilterSource);
254
- },
255
- sessionQueryPlaceholder() {
256
- if (this.isSessionQueryEnabled) {
257
- return '关键词检索(支持 Codex/Claude,例:claude code)';
258
- }
259
- return '当前来源暂不支持关键词检索';
260
- },
261
- claudeModelHasList() {
262
- return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
263
- },
308
+ computed: {
309
+ isSessionQueryEnabled() {
310
+ return isSessionQueryEnabled(this.sessionFilterSource);
311
+ },
312
+ sessionQueryPlaceholder() {
313
+ if (this.isSessionQueryEnabled) {
314
+ return '关键词检索(支持 Codex/Claude,例:claude code)';
315
+ }
316
+ return '当前来源暂不支持关键词检索';
317
+ },
318
+ claudeModelHasList() {
319
+ return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
320
+ },
264
321
  claudeModelOptions() {
265
322
  const list = Array.isArray(this.claudeModels) ? [...this.claudeModels] : [];
266
323
  const current = (this.currentClaudeModel || '').trim();
@@ -268,6 +325,162 @@
268
325
  list.unshift(current);
269
326
  }
270
327
  return list;
328
+ },
329
+ proxyProviderOptions() {
330
+ const source = Array.isArray(this.providersList) ? this.providersList : [];
331
+ const list = source
332
+ .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
333
+ .filter((name) => name && name !== 'codexmate-proxy');
334
+ return Array.from(new Set(list));
335
+ },
336
+ proxyRuntimeDisplayProvider() {
337
+ if (!this.proxyRuntime) return '';
338
+ const value = typeof this.proxyRuntime.provider === 'string'
339
+ ? this.proxyRuntime.provider.trim()
340
+ : '';
341
+ return value || 'local';
342
+ },
343
+ installTargetCards() {
344
+ const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
345
+ const action = this.normalizeInstallAction(this.installCommandAction);
346
+ return targets.map((target) => {
347
+ const id = target && typeof target.id === 'string' ? target.id : '';
348
+ return {
349
+ ...target,
350
+ command: this.getInstallCommand(id, action)
351
+ };
352
+ });
353
+ },
354
+ installRegistryPreview() {
355
+ return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
356
+ },
357
+ inspectorMainTabLabel() {
358
+ if (this.mainTab === 'config') return '配置中心';
359
+ if (this.mainTab === 'sessions') return '会话浏览';
360
+ if (this.mainTab === 'settings') return '设置';
361
+ return '未知';
362
+ },
363
+ inspectorConfigModeLabel() {
364
+ if (this.mainTab !== 'config') return '--';
365
+ if (this.configMode === 'codex') return 'Codex';
366
+ if (this.configMode === 'claude') return 'Claude Code';
367
+ if (this.configMode === 'openclaw') return 'OpenClaw';
368
+ return '未选择';
369
+ },
370
+ inspectorCurrentConfigLabel() {
371
+ if (this.mainTab !== 'config') return '--';
372
+ if (this.configMode === 'codex') {
373
+ const provider = typeof this.currentProvider === 'string' ? this.currentProvider.trim() : '';
374
+ return provider || '未选择';
375
+ }
376
+ if (this.configMode === 'claude') {
377
+ const config = typeof this.currentClaudeConfig === 'string' ? this.currentClaudeConfig.trim() : '';
378
+ return config || '未选择';
379
+ }
380
+ const openclaw = typeof this.currentOpenclawConfig === 'string' ? this.currentOpenclawConfig.trim() : '';
381
+ return openclaw || '未选择';
382
+ },
383
+ inspectorCurrentModelLabel() {
384
+ if (this.mainTab !== 'config') return '--';
385
+ if (this.configMode === 'codex') {
386
+ const model = typeof this.currentModel === 'string' ? this.currentModel.trim() : '';
387
+ return model || '未选择';
388
+ }
389
+ if (this.configMode === 'claude') {
390
+ const model = typeof this.currentClaudeModel === 'string' ? this.currentClaudeModel.trim() : '';
391
+ return model || '未选择';
392
+ }
393
+ const model = this.openclawStructured && typeof this.openclawStructured.agentPrimary === 'string'
394
+ ? this.openclawStructured.agentPrimary.trim()
395
+ : '';
396
+ return model || '按配置文件';
397
+ },
398
+ inspectorTemplateStatus() {
399
+ if (this.mainTab !== 'config') return '--';
400
+ if (this.configMode === 'codex') {
401
+ if (this.configTemplateApplying || this.codexApplying) {
402
+ return '模板应用中';
403
+ }
404
+ return '模板可编辑(手动确认应用)';
405
+ }
406
+ if (this.configMode === 'claude') {
407
+ return '即时写入 Claude settings';
408
+ }
409
+ if (this.openclawApplying || this.openclawSaving) {
410
+ return 'OpenClaw 保存/应用中';
411
+ }
412
+ return 'JSON5 可保存并应用';
413
+ },
414
+ inspectorBusyStatus() {
415
+ const tasks = [];
416
+ if (this.loading) tasks.push('初始化');
417
+ if (this.sessionsLoading) tasks.push('会话加载');
418
+ if (this.codexModelsLoading || this.claudeModelsLoading) tasks.push('模型加载');
419
+ if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
420
+ if (this.agentsSaving) tasks.push('AGENTS 保存');
421
+ if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) tasks.push('代理更新');
422
+ return tasks.length ? tasks.join(' / ') : '空闲';
423
+ },
424
+ inspectorMessageSummary() {
425
+ const value = typeof this.message === 'string' ? this.message.trim() : '';
426
+ return value || '暂无提示';
427
+ },
428
+ inspectorSessionSourceLabel() {
429
+ if (this.sessionFilterSource === 'codex') return 'Codex';
430
+ if (this.sessionFilterSource === 'claude') return 'Claude Code';
431
+ return '全部';
432
+ },
433
+ inspectorSessionPathLabel() {
434
+ const value = typeof this.sessionPathFilter === 'string' ? this.sessionPathFilter.trim() : '';
435
+ return value || '全部路径';
436
+ },
437
+ inspectorSessionQueryLabel() {
438
+ if (!this.isSessionQueryEnabled) return '当前来源不支持';
439
+ const value = typeof this.sessionQuery === 'string' ? this.sessionQuery.trim() : '';
440
+ return value || '未设置';
441
+ },
442
+ inspectorHealthStatus() {
443
+ if (this.initError) return '读取失败';
444
+ if (this.loading) return '初始化中';
445
+ return '正常';
446
+ },
447
+ inspectorHealthTone() {
448
+ if (this.initError) return 'error';
449
+ if (this.loading) return 'warn';
450
+ return 'ok';
451
+ },
452
+ inspectorModelLoadStatus() {
453
+ if (this.codexModelsLoading || this.claudeModelsLoading) {
454
+ return '加载中';
455
+ }
456
+ if (this.modelsSource === 'error' || this.claudeModelsSource === 'error') {
457
+ return '加载异常';
458
+ }
459
+ return '正常';
460
+ },
461
+ inspectorProxyStatus() {
462
+ if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) {
463
+ return '状态更新中';
464
+ }
465
+ if (this.proxyRuntime && this.proxyRuntime.running === true) {
466
+ return `运行中(${this.proxyRuntimeDisplayProvider})`;
467
+ }
468
+ return '未运行';
469
+ },
470
+ installTroubleshootingTips() {
471
+ const platform = this.resolveInstallPlatform();
472
+ if (platform === 'win32') {
473
+ return [
474
+ 'PowerShell 报权限不足(EACCES/EPERM)时,请以管理员身份执行安装命令。',
475
+ '安装后若仍提示找不到命令,重开终端并执行:where codex / where claude。',
476
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
477
+ ];
478
+ }
479
+ return [
480
+ '出现 EACCES 权限错误时,优先修复 Node 全局目录权限,不建议直接 sudo npm。',
481
+ '安装后若命令未生效,重开终端并执行:which codex / which claude。',
482
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
483
+ ];
271
484
  }
272
485
  },
273
486
  methods: {
@@ -275,8 +488,7 @@
275
488
  this.loading = true;
276
489
  this.initError = '';
277
490
  try {
278
- const statusRes = await api('status');
279
- const listRes = await api('list');
491
+ const [statusRes, listRes] = await Promise.all([api('status'), api('list')]);
280
492
 
281
493
  if (statusRes.error) {
282
494
  this.initError = statusRes.error;
@@ -289,13 +501,18 @@
289
501
  : '';
290
502
  this.serviceTier = tier === 'fast' ? 'fast' : (tier ? 'standard' : 'fast');
291
503
  }
504
+ {
505
+ const effort = typeof statusRes.modelReasoningEffort === 'string'
506
+ ? statusRes.modelReasoningEffort.trim().toLowerCase()
507
+ : '';
508
+ this.modelReasoningEffort = effort || 'high';
509
+ }
292
510
  this.providersList = listRes.providers;
293
- await this.loadModelsForProvider(this.currentProvider);
294
511
  if (statusRes.configReady === false) {
295
- this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
512
+ this.showMessage('配置已加载', 'info');
296
513
  }
297
514
  if (statusRes.initNotice) {
298
- this.showMessage(statusRes.initNotice, 'info');
515
+ this.showMessage('配置就绪', 'info');
299
516
  }
300
517
  this.maybeShowStarPrompt();
301
518
  }
@@ -304,6 +521,22 @@
304
521
  } finally {
305
522
  this.loading = false;
306
523
  }
524
+
525
+ // 模型加载单独异步,不阻塞主 loading
526
+ try {
527
+ await this.loadModelsForProvider(this.currentProvider);
528
+ } catch (e) {
529
+ // loadModelsForProvider 内部已有 toast,这里吞掉防止抛出
530
+ }
531
+
532
+ try {
533
+ await Promise.all([
534
+ this.loadCodexAuthProfiles(),
535
+ this.loadProxyStatus()
536
+ ]);
537
+ } catch (e) {
538
+ // 认证/代理状态加载失败不阻塞主界面
539
+ }
307
540
  },
308
541
 
309
542
  async loadModelsForProvider(providerName) {
@@ -324,7 +557,7 @@
324
557
  return;
325
558
  }
326
559
  if (res.error) {
327
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
560
+ this.showMessage('获取模型列表失败', 'error');
328
561
  this.models = [];
329
562
  this.modelsSource = 'error';
330
563
  this.modelsHasCurrent = true;
@@ -335,7 +568,7 @@
335
568
  this.modelsSource = res.source || 'remote';
336
569
  this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
337
570
  } catch (e) {
338
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
571
+ this.showMessage('获取模型列表失败', 'error');
339
572
  this.models = [];
340
573
  this.modelsSource = 'error';
341
574
  this.modelsHasCurrent = true;
@@ -380,7 +613,7 @@
380
613
  const res = await api('get-claude-settings');
381
614
  if (res && res.error) {
382
615
  if (!silent) {
383
- this.showMessage('读取 Claude 配置失败: ' + res.error, 'error');
616
+ this.showMessage('读取配置失败', 'error');
384
617
  }
385
618
  return;
386
619
  }
@@ -403,7 +636,7 @@
403
636
  }
404
637
  } catch (e) {
405
638
  if (!silent) {
406
- this.showMessage('读取 Claude 配置失败: ' + e.message, 'error');
639
+ this.showMessage('读取配置失败', 'error');
407
640
  }
408
641
  }
409
642
  },
@@ -454,7 +687,7 @@
454
687
  return;
455
688
  }
456
689
  if (res.error) {
457
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
690
+ this.showMessage('获取模型列表失败', 'error');
458
691
  this.claudeModels = [];
459
692
  this.claudeModelsSource = 'error';
460
693
  this.claudeModelsHasCurrent = true;
@@ -465,7 +698,7 @@
465
698
  this.claudeModelsSource = res.source || 'remote';
466
699
  this.updateClaudeModelsCurrent();
467
700
  } catch (e) {
468
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
701
+ this.showMessage('获取模型列表失败', 'error');
469
702
  this.claudeModels = [];
470
703
  this.claudeModelsSource = 'error';
471
704
  this.claudeModelsHasCurrent = true;
@@ -483,7 +716,7 @@
483
716
  if (localStorage.getItem(storageKey)) {
484
717
  return;
485
718
  }
486
- this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star', 'info');
719
+ this.showMessage('欢迎到 GitHub Star', 'info');
487
720
  localStorage.setItem(storageKey, '1');
488
721
  },
489
722
 
@@ -593,7 +826,7 @@
593
826
  openSessionStandalone(session) {
594
827
  const url = this.buildSessionStandaloneUrl(session);
595
828
  if (!url) {
596
- this.showMessage('当前会话无法生成新页链接', 'error');
829
+ this.showMessage('无法生成链接', 'error');
597
830
  return;
598
831
  }
599
832
  window.open(url, '_blank', 'noopener');
@@ -674,38 +907,79 @@
674
907
  copyAgentsContent() {
675
908
  const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
676
909
  if (!text) {
677
- this.showMessage('没有可复制的内容', 'info');
910
+ this.showMessage('没有可复制内容', 'info');
911
+ return;
912
+ }
913
+ const ok = this.fallbackCopyText(text);
914
+ if (ok) {
915
+ this.showMessage('已复制', 'success');
916
+ return;
917
+ }
918
+ this.showMessage('复制失败', 'error');
919
+ },
920
+
921
+ exportAgentsContent() {
922
+ const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
923
+ if (!text) {
924
+ this.showMessage('没有可导出内容', 'info');
925
+ return;
926
+ }
927
+ const now = new Date();
928
+ const year = String(now.getFullYear());
929
+ const month = String(now.getMonth() + 1).padStart(2, '0');
930
+ const day = String(now.getDate()).padStart(2, '0');
931
+ const hour = String(now.getHours()).padStart(2, '0');
932
+ const minute = String(now.getMinutes()).padStart(2, '0');
933
+ const second = String(now.getSeconds()).padStart(2, '0');
934
+ const fileName = `agent-${year}${month}${day}-${hour}${minute}${second}.txt`;
935
+ this.downloadTextFile(fileName, text, 'text/plain;charset=utf-8');
936
+ this.showMessage(`已导出 ${fileName}`, 'success');
937
+ },
938
+
939
+ async copyInstallCommand(cmd) {
940
+ const text = typeof cmd === 'string' ? cmd.trim() : '';
941
+ if (!text) {
942
+ this.showMessage('没有可复制内容', 'info');
678
943
  return;
679
944
  }
945
+ try {
946
+ if (navigator.clipboard && window.isSecureContext) {
947
+ await navigator.clipboard.writeText(text);
948
+ this.showMessage('已复制命令', 'success');
949
+ return;
950
+ }
951
+ } catch (e) {
952
+ // fallback to legacy copy path
953
+ }
680
954
  const ok = this.fallbackCopyText(text);
681
955
  if (ok) {
682
- this.showMessage('已复制 AGENTS.md 内容', 'success');
956
+ this.showMessage('已复制命令', 'success');
683
957
  return;
684
958
  }
685
- this.showMessage('复制失败,请手动复制内容', 'error');
959
+ this.showMessage('复制失败', 'error');
686
960
  },
687
961
 
688
962
  async copyResumeCommand(session) {
689
963
  if (!this.isResumeCommandAvailable(session)) {
690
- this.showMessage('当前会话不支持生成恢复命令', 'error');
964
+ this.showMessage('不支持此操作', 'error');
691
965
  return;
692
966
  }
693
967
  const command = this.buildResumeCommand(session);
694
968
  const ok = this.fallbackCopyText(command);
695
969
  if (ok) {
696
- this.showMessage('已复制恢复命令', 'success');
970
+ this.showMessage('已复制', 'success');
697
971
  return;
698
972
  }
699
973
  try {
700
974
  if (navigator.clipboard && window.isSecureContext) {
701
975
  await navigator.clipboard.writeText(command);
702
- this.showMessage('已复制恢复命令', 'success');
976
+ this.showMessage('已复制', 'success');
703
977
  return;
704
978
  }
705
979
  } catch (e) {
706
980
  // keep fallback failure message
707
981
  }
708
- this.showMessage('复制失败,请手动复制命令', 'error');
982
+ this.showMessage('复制失败', 'error');
709
983
  },
710
984
 
711
985
  buildProviderShareCommand(payload) {
@@ -742,7 +1016,11 @@
742
1016
  async copyProviderShareCommand(provider) {
743
1017
  const name = provider && typeof provider.name === 'string' ? provider.name.trim() : '';
744
1018
  if (!name) {
745
- this.showMessage('提供商名称无效', 'error');
1019
+ this.showMessage('参数无效', 'error');
1020
+ return;
1021
+ }
1022
+ if (!this.shouldAllowProviderShare(provider)) {
1023
+ this.showMessage('本地入口不可分享', 'info');
746
1024
  return;
747
1025
  }
748
1026
  if (this.providerShareLoading[name]) {
@@ -757,26 +1035,26 @@
757
1035
  }
758
1036
  const command = this.buildProviderShareCommand(res && res.payload ? res.payload : null);
759
1037
  if (!command) {
760
- this.showMessage('分享命令生成失败', 'error');
1038
+ this.showMessage('生成命令失败', 'error');
761
1039
  return;
762
1040
  }
763
1041
  const ok = this.fallbackCopyText(command);
764
1042
  if (ok) {
765
- this.showMessage('已复制分享命令', 'success');
1043
+ this.showMessage('已复制', 'success');
766
1044
  return;
767
1045
  }
768
1046
  try {
769
1047
  if (navigator.clipboard && window.isSecureContext) {
770
1048
  await navigator.clipboard.writeText(command);
771
- this.showMessage('已复制分享命令', 'success');
1049
+ this.showMessage('已复制', 'success');
772
1050
  return;
773
1051
  }
774
1052
  } catch (e) {
775
1053
  // keep fallback failure message
776
1054
  }
777
- this.showMessage('复制失败,请手动复制命令', 'error');
1055
+ this.showMessage('复制失败', 'error');
778
1056
  } catch (e) {
779
- this.showMessage('生成分享命令失败: ' + e.message, 'error');
1057
+ this.showMessage('生成命令失败', 'error');
780
1058
  } finally {
781
1059
  this.providerShareLoading[name] = false;
782
1060
  }
@@ -798,26 +1076,26 @@
798
1076
  }
799
1077
  const command = this.buildClaudeShareCommand(res && res.payload ? res.payload : null);
800
1078
  if (!command) {
801
- this.showMessage('分享命令生成失败', 'error');
1079
+ this.showMessage('生成命令失败', 'error');
802
1080
  return;
803
1081
  }
804
1082
  const ok = this.fallbackCopyText(command);
805
1083
  if (ok) {
806
- this.showMessage('已复制分享命令', 'success');
1084
+ this.showMessage('已复制', 'success');
807
1085
  return;
808
1086
  }
809
1087
  try {
810
1088
  if (navigator.clipboard && window.isSecureContext) {
811
1089
  await navigator.clipboard.writeText(command);
812
- this.showMessage('已复制分享命令', 'success');
1090
+ this.showMessage('已复制', 'success');
813
1091
  return;
814
1092
  }
815
1093
  } catch (e) {
816
1094
  // fall through
817
1095
  }
818
- this.showMessage('复制失败,请手动复制命令', 'error');
1096
+ this.showMessage('复制失败', 'error');
819
1097
  } catch (e) {
820
- this.showMessage('生成分享命令失败: ' + e.message, 'error');
1098
+ this.showMessage('生成命令失败', 'error');
821
1099
  } finally {
822
1100
  this.claudeShareLoading[name] = false;
823
1101
  }
@@ -825,7 +1103,7 @@
825
1103
 
826
1104
  async cloneSession(session) {
827
1105
  if (!this.isCloneAvailable(session)) {
828
- this.showMessage('当前会话不支持克隆', 'error');
1106
+ this.showMessage('不支持此操作', 'error');
829
1107
  return;
830
1108
  }
831
1109
  const key = this.getSessionExportKey(session);
@@ -844,7 +1122,7 @@
844
1122
  return;
845
1123
  }
846
1124
 
847
- this.showMessage('会话已克隆', 'success');
1125
+ this.showMessage('操作成功', 'success');
848
1126
  await this.loadSessions();
849
1127
  if (res.sessionId) {
850
1128
  const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
@@ -853,7 +1131,7 @@
853
1131
  }
854
1132
  }
855
1133
  } catch (e) {
856
- this.showMessage('克隆失败: ' + e.message, 'error');
1134
+ this.showMessage('克隆失败', 'error');
857
1135
  } finally {
858
1136
  this.sessionCloning[key] = false;
859
1137
  }
@@ -861,7 +1139,7 @@
861
1139
 
862
1140
  async deleteSession(session) {
863
1141
  if (!this.isDeleteAvailable(session)) {
864
- this.showMessage('当前会话不支持删除', 'error');
1142
+ this.showMessage('不支持此操作', 'error');
865
1143
  return;
866
1144
  }
867
1145
  const key = this.getSessionExportKey(session);
@@ -879,10 +1157,10 @@
879
1157
  this.showMessage(res.error, 'error');
880
1158
  return;
881
1159
  }
882
- this.showMessage('会话已删除', 'success');
1160
+ this.showMessage('操作成功', 'success');
883
1161
  await this.loadSessions();
884
1162
  } catch (e) {
885
- this.showMessage('删除失败: ' + e.message, 'error');
1163
+ this.showMessage('删除失败', 'error');
886
1164
  } finally {
887
1165
  this.sessionDeleting[key] = false;
888
1166
  }
@@ -893,7 +1171,7 @@
893
1171
  return value.trim();
894
1172
  },
895
1173
 
896
- mergeSessionPathOptions(baseList = [], incomingList = []) {
1174
+ mergeSessionPathOptions(baseList = [], incomingList = []) {
897
1175
  const merged = [];
898
1176
  const seen = new Set();
899
1177
  const append = (items) => {
@@ -931,8 +1209,8 @@
931
1209
  return paths;
932
1210
  },
933
1211
 
934
- syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
935
- const targetSource = source === 'claude' ? 'claude' : 'codex';
1212
+ syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
1213
+ const targetSource = source === 'claude' ? 'claude' : (source === 'all' ? 'all' : 'codex');
936
1214
  const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
937
1215
  ? this.sessionPathOptionsMap[targetSource]
938
1216
  : [];
@@ -944,10 +1222,10 @@
944
1222
  [targetSource]: merged
945
1223
  };
946
1224
  this.refreshSessionPathOptions(targetSource);
947
- },
948
-
949
- refreshSessionPathOptions(source) {
950
- const targetSource = source === 'claude' ? 'claude' : 'codex';
1225
+ },
1226
+
1227
+ refreshSessionPathOptions(source) {
1228
+ const targetSource = source === 'claude' ? 'claude' : (source === 'all' ? 'all' : 'codex');
951
1229
  const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
952
1230
  ? [...this.sessionPathOptionsMap[targetSource]]
953
1231
  : [];
@@ -960,8 +1238,8 @@
960
1238
  }
961
1239
  },
962
1240
 
963
- async loadSessionPathOptions(options = {}) {
964
- const source = options.source === 'claude' ? 'claude' : 'codex';
1241
+ async loadSessionPathOptions(options = {}) {
1242
+ const source = options.source === 'claude' ? 'claude' : (options.source === 'all' ? 'all' : 'codex');
965
1243
  const forceRefresh = !!options.forceRefresh;
966
1244
  const loaded = !!this.sessionPathOptionsLoadedMap[source];
967
1245
  if (!forceRefresh && loaded) {
@@ -1013,12 +1291,12 @@
1013
1291
  await this.loadSessions();
1014
1292
  },
1015
1293
 
1016
- async clearSessionFilters() {
1017
- this.sessionFilterSource = 'codex';
1018
- this.sessionPathFilter = '';
1019
- this.sessionQuery = '';
1020
- this.sessionRoleFilter = 'all';
1021
- this.sessionTimePreset = 'all';
1294
+ async clearSessionFilters() {
1295
+ this.sessionFilterSource = 'all';
1296
+ this.sessionPathFilter = '';
1297
+ this.sessionQuery = '';
1298
+ this.sessionRoleFilter = 'all';
1299
+ this.sessionTimePreset = 'all';
1022
1300
  await this.onSessionSourceChange();
1023
1301
  },
1024
1302
 
@@ -1093,7 +1371,7 @@
1093
1371
  this.activeSession = null;
1094
1372
  this.activeSessionMessages = [];
1095
1373
  this.activeSessionDetailClipped = false;
1096
- this.showMessage('加载会话失败: ' + e.message, 'error');
1374
+ this.showMessage('加载会话失败', 'error');
1097
1375
  } finally {
1098
1376
  this.sessionsLoading = false;
1099
1377
  }
@@ -1223,8 +1501,10 @@
1223
1501
  }
1224
1502
  },
1225
1503
 
1226
- downloadTextFile(fileName, content) {
1227
- const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1504
+ downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
1505
+ // 使用 UTF-8 BOM 确保文本编辑器正确识别编码
1506
+ const BOM = '\uFEFF';
1507
+ const blob = new Blob([BOM + content], { type: mimeType });
1228
1508
  const url = URL.createObjectURL(blob);
1229
1509
  const link = document.createElement('a');
1230
1510
  link.href = url;
@@ -1255,10 +1535,10 @@
1255
1535
  const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
1256
1536
  this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
1257
1537
  } else {
1258
- this.showMessage('会话导出完成', 'success');
1538
+ this.showMessage('操作成功', 'success');
1259
1539
  }
1260
1540
  } catch (e) {
1261
- this.showMessage('导出失败: ' + e.message, 'error');
1541
+ this.showMessage('导出失败', 'error');
1262
1542
  } finally {
1263
1543
  this.sessionExporting[key] = false;
1264
1544
  }
@@ -1267,15 +1547,22 @@
1267
1547
  async switchProvider(name) {
1268
1548
  this.currentProvider = name;
1269
1549
  await this.loadModelsForProvider(name);
1270
- await this.openConfigTemplateEditor();
1550
+ if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
1551
+ this.currentModel = this.models[0];
1552
+ }
1553
+ await this.applyCodexConfigDirect({ silent: true });
1271
1554
  },
1272
1555
 
1273
1556
  async onModelChange() {
1274
- await this.openConfigTemplateEditor();
1557
+ await this.applyCodexConfigDirect();
1275
1558
  },
1276
1559
 
1277
1560
  async onServiceTierChange() {
1278
- await this.openConfigTemplateEditor();
1561
+ await this.applyCodexConfigDirect({ silent: true });
1562
+ },
1563
+
1564
+ async onReasoningEffortChange() {
1565
+ await this.applyCodexConfigDirect({ silent: true });
1279
1566
  },
1280
1567
 
1281
1568
  async runHealthCheck() {
@@ -1320,15 +1607,15 @@
1320
1607
  remote
1321
1608
  };
1322
1609
  if (ok) {
1323
- this.showMessage('健康检查通过', 'success');
1610
+ this.showMessage('检查通过', 'success');
1324
1611
  }
1325
1612
  } else {
1326
1613
  this.healthCheckResult = null;
1327
- this.showMessage('健康检查失败:返回数据异常', 'error');
1614
+ this.showMessage('检查失败', 'error');
1328
1615
  }
1329
1616
  } catch (e) {
1330
1617
  this.healthCheckResult = null;
1331
- this.showMessage('健康检查失败: ' + e.message, 'error');
1618
+ this.showMessage('检查失败', 'error');
1332
1619
  } finally {
1333
1620
  if (this.configMode === 'claude') {
1334
1621
  try {
@@ -1369,7 +1656,50 @@
1369
1656
  this.configTemplateContent = template;
1370
1657
  this.showConfigTemplateModal = true;
1371
1658
  } catch (e) {
1372
- this.showMessage('加载模板失败: ' + e.message, 'error');
1659
+ this.showMessage('加载模板失败', 'error');
1660
+ }
1661
+ },
1662
+
1663
+ async applyCodexConfigDirect(options = {}) {
1664
+ if (this.codexApplying) return;
1665
+
1666
+ const provider = (this.currentProvider || '').trim();
1667
+ const model = (this.currentModel || '').trim();
1668
+ if (!provider || !model) {
1669
+ this.showMessage('请选择提供商和模型', 'error');
1670
+ return;
1671
+ }
1672
+
1673
+ this.codexApplying = true;
1674
+ try {
1675
+ const tplRes = await api('get-config-template', {
1676
+ provider,
1677
+ model,
1678
+ serviceTier: this.serviceTier,
1679
+ reasoningEffort: this.modelReasoningEffort
1680
+ });
1681
+ if (tplRes.error) {
1682
+ this.showMessage('获取模板失败', 'error');
1683
+ return;
1684
+ }
1685
+
1686
+ const applyRes = await api('apply-config-template', {
1687
+ template: tplRes.template
1688
+ });
1689
+ if (applyRes.error) {
1690
+ this.showMessage('应用模板失败', 'error');
1691
+ return;
1692
+ }
1693
+
1694
+ if (options.silent !== true) {
1695
+ this.showMessage('配置已应用', 'success');
1696
+ }
1697
+
1698
+ await this.loadAll();
1699
+ } catch (e) {
1700
+ this.showMessage('应用失败', 'error');
1701
+ } finally {
1702
+ this.codexApplying = false;
1373
1703
  }
1374
1704
  },
1375
1705
 
@@ -1380,7 +1710,7 @@
1380
1710
 
1381
1711
  async applyConfigTemplate() {
1382
1712
  if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
1383
- this.showMessage('模板内容不能为空', 'error');
1713
+ this.showMessage('模板不能为空', 'error');
1384
1714
  return;
1385
1715
  }
1386
1716
 
@@ -1393,11 +1723,11 @@
1393
1723
  this.showMessage(res.error, 'error');
1394
1724
  return;
1395
1725
  }
1396
- this.showMessage('模板已应用到 config.toml', 'success');
1726
+ this.showMessage('模板已应用', 'success');
1397
1727
  this.closeConfigTemplateModal();
1398
1728
  await this.loadAll();
1399
1729
  } catch (e) {
1400
- this.showMessage('应用模板失败: ' + e.message, 'error');
1730
+ this.showMessage('应用模板失败', 'error');
1401
1731
  } finally {
1402
1732
  this.configTemplateApplying = false;
1403
1733
  }
@@ -1418,7 +1748,7 @@
1418
1748
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1419
1749
  this.showAgentsModal = true;
1420
1750
  } catch (e) {
1421
- this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
1751
+ this.showMessage('加载文件失败', 'error');
1422
1752
  } finally {
1423
1753
  this.agentsLoading = false;
1424
1754
  }
@@ -1442,7 +1772,7 @@
1442
1772
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1443
1773
  this.showAgentsModal = true;
1444
1774
  } catch (e) {
1445
- this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
1775
+ this.showMessage('加载文件失败', 'error');
1446
1776
  } finally {
1447
1777
  this.agentsLoading = false;
1448
1778
  }
@@ -1451,7 +1781,7 @@
1451
1781
  async openOpenclawWorkspaceEditor() {
1452
1782
  const fileName = (this.openclawWorkspaceFileName || '').trim();
1453
1783
  if (!fileName) {
1454
- this.showMessage('请输入工作区文件名', 'error');
1784
+ this.showMessage('请输入文件名', 'error');
1455
1785
  return;
1456
1786
  }
1457
1787
  this.setAgentsModalContext('openclaw-workspace', { fileName });
@@ -1471,7 +1801,7 @@
1471
1801
  this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
1472
1802
  this.showAgentsModal = true;
1473
1803
  } catch (e) {
1474
- this.showMessage('加载 OpenClaw 工作区文件失败: ' + e.message, 'error');
1804
+ this.showMessage('加载文件失败', 'error');
1475
1805
  } finally {
1476
1806
  this.agentsLoading = false;
1477
1807
  }
@@ -1533,7 +1863,7 @@
1533
1863
  this.showMessage(successLabel, 'success');
1534
1864
  this.closeAgentsModal();
1535
1865
  } catch (e) {
1536
- this.showMessage('保存文件失败: ' + e.message, 'error');
1866
+ this.showMessage('保存失败', 'error');
1537
1867
  } finally {
1538
1868
  this.agentsSaving = false;
1539
1869
  }
@@ -1547,82 +1877,214 @@
1547
1877
  if (!name) {
1548
1878
  return this.showMessage('名称不能为空', 'error');
1549
1879
  }
1880
+ if (name.toLowerCase() === 'local') {
1881
+ return this.showMessage('local provider 为系统保留名称,不可新增', 'error');
1882
+ }
1550
1883
  if (this.providersList.some(item => item.name === name)) {
1551
- return this.showMessage('提供商已存在', 'error');
1884
+ return this.showMessage('名称已存在', 'error');
1885
+ }
1886
+
1887
+ try {
1888
+ const res = await api('add-provider', {
1889
+ name,
1890
+ url: this.newProvider.url.trim(),
1891
+ key: this.newProvider.key || ''
1892
+ });
1893
+ if (res.error) {
1894
+ this.showMessage(res.error, 'error');
1895
+ return;
1896
+ }
1897
+
1898
+ this.showMessage('操作成功', 'success');
1899
+ this.closeAddModal();
1900
+ await this.loadAll();
1901
+ } catch (e) {
1902
+ this.showMessage('添加失败', 'error');
1552
1903
  }
1904
+ },
1553
1905
 
1554
- const safeName = this.escapeTomlString(name);
1555
- const safeUrl = this.escapeTomlString(this.newProvider.url.trim());
1556
- const safeKey = this.escapeTomlString(this.newProvider.key || '');
1557
- const newProviderBlock = `[model_providers.${safeName}]\nname = "${safeName}"\nbase_url = "${safeUrl}"\nwire_api = "responses"\nrequires_openai_auth = false\npreferred_auth_method = "${safeKey}"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000`;
1906
+ getCurrentCodexAuthProfile() {
1907
+ const list = Array.isArray(this.codexAuthProfiles) ? this.codexAuthProfiles : [];
1908
+ return list.find((item) => !!(item && item.current)) || null;
1909
+ },
1558
1910
 
1559
- this.currentProvider = name;
1560
- this.showMessage('已生成新增模板,请确认后应用', 'info');
1561
- this.closeAddModal();
1562
- await this.openConfigTemplateEditor({
1563
- appendHint: `新增 provider: ${name}(请检查字段后应用)`,
1564
- appendBlock: newProviderBlock
1565
- });
1911
+ isLocalLikeProvider(providerOrName) {
1912
+ if (!providerOrName) return false;
1913
+ const rawName = typeof providerOrName === 'object'
1914
+ ? String(providerOrName.name || '')
1915
+ : String(providerOrName);
1916
+ const normalized = rawName.trim().toLowerCase();
1917
+ return normalized === 'local' || normalized === 'codexmate-proxy';
1918
+ },
1919
+
1920
+ providerPillState(provider) {
1921
+ if (this.isLocalLikeProvider(provider)) {
1922
+ const currentProfile = this.getCurrentCodexAuthProfile();
1923
+ return currentProfile
1924
+ ? { configured: true, text: '已登录' }
1925
+ : { configured: false, text: '未登录' };
1926
+ }
1927
+ const configured = !!(provider && provider.hasKey);
1928
+ return {
1929
+ configured,
1930
+ text: configured ? '已配置' : '未配置'
1931
+ };
1932
+ },
1933
+
1934
+ providerPillConfigured(provider) {
1935
+ return this.providerPillState(provider).configured;
1936
+ },
1937
+
1938
+ providerPillText(provider) {
1939
+ return this.providerPillState(provider).text;
1940
+ },
1941
+
1942
+ isReadOnlyProvider(providerOrName) {
1943
+ if (!providerOrName) return false;
1944
+ if (typeof providerOrName === 'object') {
1945
+ return !!providerOrName.readOnly;
1946
+ }
1947
+ const name = String(providerOrName).trim();
1948
+ if (!name) return false;
1949
+ const target = (this.providersList || []).find((item) => item && item.name === name);
1950
+ return !!(target && target.readOnly);
1951
+ },
1952
+
1953
+ isNonDeletableProvider(providerOrName) {
1954
+ if (!providerOrName) return false;
1955
+ if (typeof providerOrName === 'object') {
1956
+ const directName = String(providerOrName.name || '').trim().toLowerCase();
1957
+ if (directName === 'local' || directName === 'codexmate-proxy') {
1958
+ return true;
1959
+ }
1960
+ return !!providerOrName.nonDeletable;
1961
+ }
1962
+ const name = String(providerOrName).trim();
1963
+ if (!name) return false;
1964
+ const normalized = name.toLowerCase();
1965
+ if (normalized === 'local' || normalized === 'codexmate-proxy') {
1966
+ return true;
1967
+ }
1968
+ const target = (this.providersList || []).find((item) => item && item.name === name);
1969
+ return !!(target && target.nonDeletable);
1970
+ },
1971
+
1972
+ shouldShowProviderDelete(provider) {
1973
+ return !this.isReadOnlyProvider(provider) && !this.isNonDeletableProvider(provider);
1974
+ },
1975
+
1976
+ shouldShowProviderEdit(provider) {
1977
+ return !this.isReadOnlyProvider(provider) && !this.isNonDeletableProvider(provider);
1978
+ },
1979
+
1980
+ shouldAllowProviderShare(provider) {
1981
+ return !this.isReadOnlyProvider(provider) && !this.isLocalLikeProvider(provider);
1566
1982
  },
1567
1983
 
1568
1984
  async deleteProvider(name) {
1569
- if (!confirm(`确定删除提供商 "${name}"?`)) return;
1570
- this.showMessage('请在模板中手动删除该 provider 配置块后应用', 'info');
1571
- await this.openConfigTemplateEditor({
1572
- appendHint: `请手动删除 [model_providers.${name}] 配置块,并确认 model_provider 指向有效 provider`
1573
- });
1985
+ if (this.isNonDeletableProvider(name)) {
1986
+ this.showMessage(' provider 为保留项,不可删除', 'info');
1987
+ return;
1988
+ }
1989
+ const res = await api('delete-provider', { name });
1990
+ if (res.error) {
1991
+ this.showMessage(res.error, 'error');
1992
+ return;
1993
+ }
1994
+ if (res.switched && res.provider) {
1995
+ this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
1996
+ } else {
1997
+ this.showMessage('操作成功', 'success');
1998
+ }
1999
+ await this.loadAll();
1574
2000
  },
1575
2001
 
1576
2002
  openEditModal(provider) {
2003
+ if (!this.shouldShowProviderEdit(provider)) {
2004
+ this.showMessage('该 provider 为保留项,不可编辑', 'info');
2005
+ return;
2006
+ }
1577
2007
  this.editingProvider = {
1578
2008
  name: provider.name,
1579
2009
  url: provider.url || '',
1580
- key: ''
2010
+ key: '',
2011
+ readOnly: !!provider.readOnly,
2012
+ nonEditable: this.isNonDeletableProvider(provider)
1581
2013
  };
1582
2014
  this.showEditModal = true;
1583
2015
  },
1584
2016
 
1585
2017
  async updateProvider() {
2018
+ if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
2019
+ this.showMessage('该 provider 为保留项,不可编辑', 'error');
2020
+ this.closeEditModal();
2021
+ return;
2022
+ }
1586
2023
  if (!this.editingProvider.url) {
1587
2024
  return this.showMessage('URL 必填', 'error');
1588
2025
  }
1589
2026
 
1590
2027
  const name = this.editingProvider.name;
1591
- const safeUrl = this.escapeTomlString(this.editingProvider.url.trim());
1592
- const safeKey = this.escapeTomlString(this.editingProvider.key || '');
2028
+ const url = this.editingProvider.url.trim();
2029
+ const key = this.editingProvider.key || '';
1593
2030
  this.closeEditModal();
1594
- this.showMessage('已生成更新模板,请确认后应用', 'info');
1595
- await this.openConfigTemplateEditor({
1596
- appendHint: `请将 [model_providers.${name}] 中 base_url 更新为 ${safeUrl}${safeKey ? ',并更新 preferred_auth_method' : ''}`
1597
- });
2031
+ try {
2032
+ const res = await api('update-provider', { name, url, key });
2033
+ if (res.error) {
2034
+ this.showMessage(res.error, 'error');
2035
+ return;
2036
+ }
2037
+ this.showMessage('操作成功', 'success');
2038
+ await this.loadAll();
2039
+ } catch (e) {
2040
+ this.showMessage('更新失败', 'error');
2041
+ }
1598
2042
  },
1599
2043
 
1600
2044
  closeEditModal() {
1601
2045
  this.showEditModal = false;
1602
- this.editingProvider = { name: '', url: '', key: '' };
2046
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false };
2047
+ },
2048
+
2049
+ async resetConfig() {
2050
+ if (this.resetConfigLoading) return;
2051
+ this.resetConfigLoading = true;
2052
+ try {
2053
+ const res = await api('reset-config');
2054
+ if (res.error) {
2055
+ this.showMessage(res.error, 'error');
2056
+ return;
2057
+ }
2058
+ const backup = res.backupFile ? `(已备份: ${res.backupFile})` : '';
2059
+ this.showMessage(`配置已重装${backup}`, 'success');
2060
+ await this.loadAll();
2061
+ } catch (e) {
2062
+ this.showMessage('重装失败', 'error');
2063
+ } finally {
2064
+ this.resetConfigLoading = false;
2065
+ }
1603
2066
  },
1604
2067
 
1605
2068
  async addModel() {
1606
2069
  if (!this.newModelName || !this.newModelName.trim()) {
1607
- return this.showMessage('请输入模型名称', 'error');
2070
+ return this.showMessage('请输入模型', 'error');
1608
2071
  }
1609
2072
  const res = await api('add-model', { model: this.newModelName.trim() });
1610
2073
  if (res.error) {
1611
2074
  this.showMessage(res.error, 'error');
1612
2075
  } else {
1613
- this.showMessage('已添加', 'success');
2076
+ this.showMessage('操作成功', 'success');
1614
2077
  this.closeModelModal();
1615
2078
  await this.loadAll();
1616
2079
  }
1617
2080
  },
1618
2081
 
1619
2082
  async removeModel(model) {
1620
- if (!confirm(`确定删除模型 "${model}"?`)) return;
1621
2083
  const res = await api('delete-model', { model });
1622
2084
  if (res.error) {
1623
2085
  this.showMessage(res.error, 'error');
1624
2086
  } else {
1625
- this.showMessage('已删除', 'success');
2087
+ this.showMessage('操作成功', 'success');
1626
2088
  await this.loadAll();
1627
2089
  }
1628
2090
  },
@@ -1677,7 +2139,7 @@
1677
2139
  this.saveClaudeConfigs();
1678
2140
  this.updateClaudeModelsCurrent();
1679
2141
  if (!this.claudeConfigs[name].apiKey) {
1680
- this.showMessage('该配置未设置 API Key,请先编辑', 'error');
2142
+ this.showMessage('请先配置 API Key', 'error');
1681
2143
  return;
1682
2144
  }
1683
2145
  this.applyClaudeConfig(name);
@@ -1707,7 +2169,7 @@
1707
2169
  hasKey: !!this.editingConfig.apiKey
1708
2170
  };
1709
2171
  this.saveClaudeConfigs();
1710
- this.showMessage('配置已更新', 'success');
2172
+ this.showMessage('操作成功', 'success');
1711
2173
  this.closeEditConfigModal();
1712
2174
  if (name === this.currentClaudeConfig) {
1713
2175
  this.refreshClaudeModelContext();
@@ -1731,7 +2193,7 @@
1731
2193
 
1732
2194
  const config = this.claudeConfigs[name];
1733
2195
  if (!config.apiKey) {
1734
- this.showMessage('已保存,未应用:请先输入 API Key', 'info');
2196
+ this.showMessage('已保存,未应用', 'info');
1735
2197
  this.closeEditConfigModal();
1736
2198
  if (name === this.currentClaudeConfig) {
1737
2199
  this.refreshClaudeModelContext();
@@ -1741,7 +2203,7 @@
1741
2203
 
1742
2204
  const res = await api('apply-claude-config', { config });
1743
2205
  if (res.error || res.success === false) {
1744
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
2206
+ this.showMessage(res.error || '应用配置失败', 'error');
1745
2207
  } else {
1746
2208
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
1747
2209
  this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
@@ -1754,15 +2216,15 @@
1754
2216
 
1755
2217
  addClaudeConfig() {
1756
2218
  if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
1757
- return this.showMessage('请输入配置名称', 'error');
2219
+ return this.showMessage('请输入名称', 'error');
1758
2220
  }
1759
2221
  const name = this.newClaudeConfig.name.trim();
1760
2222
  if (this.claudeConfigs[name]) {
1761
- return this.showMessage('配置名称已存在', 'error');
2223
+ return this.showMessage('名称已存在', 'error');
1762
2224
  }
1763
2225
  const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
1764
2226
  if (duplicateName) {
1765
- return this.showMessage('已存在相同配置,已忽略添加', 'info');
2227
+ return this.showMessage('配置已存在', 'info');
1766
2228
  }
1767
2229
 
1768
2230
  this.claudeConfigs[name] = {
@@ -1774,14 +2236,14 @@
1774
2236
 
1775
2237
  this.currentClaudeConfig = name;
1776
2238
  this.saveClaudeConfigs();
1777
- this.showMessage('配置已添加', 'success');
2239
+ this.showMessage('操作成功', 'success');
1778
2240
  this.closeClaudeConfigModal();
1779
2241
  this.refreshClaudeModelContext();
1780
2242
  },
1781
2243
 
1782
2244
  deleteClaudeConfig(name) {
1783
2245
  if (Object.keys(this.claudeConfigs).length <= 1) {
1784
- return this.showMessage('至少保留一个配置', 'error');
2246
+ return this.showMessage('至少保留一项', 'error');
1785
2247
  }
1786
2248
 
1787
2249
  if (!confirm(`确定删除配置 "${name}"?`)) return;
@@ -1791,7 +2253,7 @@
1791
2253
  this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
1792
2254
  }
1793
2255
  this.saveClaudeConfigs();
1794
- this.showMessage('配置已删除', 'success');
2256
+ this.showMessage('操作成功', 'success');
1795
2257
  this.refreshClaudeModelContext();
1796
2258
  },
1797
2259
 
@@ -1801,12 +2263,12 @@
1801
2263
  const config = this.claudeConfigs[name];
1802
2264
 
1803
2265
  if (!config.apiKey) {
1804
- return this.showMessage('该配置未设置 API Key,请先编辑', 'error');
2266
+ return this.showMessage('请先配置 API Key', 'error');
1805
2267
  }
1806
2268
 
1807
2269
  const res = await api('apply-claude-config', { config });
1808
2270
  if (res.error || res.success === false) {
1809
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
2271
+ this.showMessage(res.error || '应用配置失败', 'error');
1810
2272
  } else {
1811
2273
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
1812
2274
  this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
@@ -2014,7 +2476,7 @@
2014
2476
  }
2015
2477
  this.fillOpenclawQuickFromConfig(parsed.data);
2016
2478
  if (!silent) {
2017
- this.showMessage('已从编辑器读取快速配置', 'success');
2479
+ this.showMessage('已读取配置', 'success');
2018
2480
  }
2019
2481
  return true;
2020
2482
  },
@@ -2110,7 +2572,7 @@
2110
2572
  this.refreshOpenclawProviders(parsed.data);
2111
2573
  this.refreshOpenclawAgentsList(parsed.data);
2112
2574
  if (!silent) {
2113
- this.showMessage('已从文本刷新结构化配置', 'success');
2575
+ this.showMessage('已刷新配置', 'success');
2114
2576
  }
2115
2577
  return true;
2116
2578
  },
@@ -2408,7 +2870,7 @@
2408
2870
  this.refreshOpenclawProviders(config);
2409
2871
  this.refreshOpenclawAgentsList(config);
2410
2872
  this.fillOpenclawQuickFromConfig(config);
2411
- this.showMessage('已写入编辑器', 'success');
2873
+ this.showMessage('已写入', 'success');
2412
2874
  },
2413
2875
 
2414
2876
  applyOpenclawQuickToText() {
@@ -2421,7 +2883,7 @@
2421
2883
  const providerName = (this.openclawQuick.providerName || '').trim();
2422
2884
  const modelId = (this.openclawQuick.modelId || '').trim();
2423
2885
  if (!providerName) {
2424
- this.showMessage('请填写 Provider 名称', 'error');
2886
+ this.showMessage('请填写名称', 'error');
2425
2887
  return;
2426
2888
  }
2427
2889
  if (providerName.includes('/')) {
@@ -2429,7 +2891,7 @@
2429
2891
  return;
2430
2892
  }
2431
2893
  if (!modelId) {
2432
- this.showMessage('请填写模型 ID', 'error');
2894
+ this.showMessage('请填写模型', 'error');
2433
2895
  return;
2434
2896
  }
2435
2897
 
@@ -2440,7 +2902,7 @@
2440
2902
  const provider = ensureObject(providers[providerName]);
2441
2903
  const baseUrl = (this.openclawQuick.baseUrl || '').trim();
2442
2904
  if (!baseUrl && !provider.baseUrl) {
2443
- this.showMessage('请填写 Base URL', 'error');
2905
+ this.showMessage('请填写 URL', 'error');
2444
2906
  return;
2445
2907
  }
2446
2908
 
@@ -2526,7 +2988,7 @@
2526
2988
  this.fillOpenclawStructured(config);
2527
2989
  this.refreshOpenclawProviders(config);
2528
2990
  this.refreshOpenclawAgentsList(config);
2529
- this.showMessage('快速配置已写入编辑器', 'success');
2991
+ this.showMessage('配置已写入', 'success');
2530
2992
  },
2531
2993
 
2532
2994
  addOpenclawFallback() {
@@ -2630,6 +3092,144 @@
2630
3092
  this.resetOpenclawQuick();
2631
3093
  },
2632
3094
 
3095
+ normalizeInstallPackageManager(value) {
3096
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3097
+ if (normalized === 'pnpm' || normalized === 'bun' || normalized === 'npm') {
3098
+ return normalized;
3099
+ }
3100
+ return 'npm';
3101
+ },
3102
+
3103
+ normalizeInstallAction(value) {
3104
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3105
+ if (normalized === 'update' || normalized === 'uninstall' || normalized === 'install') {
3106
+ return normalized;
3107
+ }
3108
+ return 'install';
3109
+ },
3110
+
3111
+ normalizeInstallRegistryPreset(value) {
3112
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3113
+ if (normalized === 'default' || normalized === 'npmmirror' || normalized === 'tencent' || normalized === 'custom') {
3114
+ return normalized;
3115
+ }
3116
+ return 'default';
3117
+ },
3118
+
3119
+ normalizeInstallRegistryUrl(value) {
3120
+ const normalized = typeof value === 'string' ? value.trim() : '';
3121
+ if (!normalized) return '';
3122
+ if (!/^https?:\/\//i.test(normalized)) {
3123
+ return '';
3124
+ }
3125
+ return normalized.replace(/\/+$/, '');
3126
+ },
3127
+
3128
+ resolveInstallRegistryUrl(presetValue, customValue) {
3129
+ const preset = this.normalizeInstallRegistryPreset(presetValue);
3130
+ if (preset === 'npmmirror') {
3131
+ return 'https://registry.npmmirror.com';
3132
+ }
3133
+ if (preset === 'tencent') {
3134
+ return 'https://mirrors.cloud.tencent.com/npm';
3135
+ }
3136
+ if (preset === 'custom') {
3137
+ return this.normalizeInstallRegistryUrl(customValue);
3138
+ }
3139
+ return '';
3140
+ },
3141
+
3142
+ appendInstallRegistryOption(command, actionName) {
3143
+ const base = typeof command === 'string' ? command.trim() : '';
3144
+ if (!base) return '';
3145
+ const action = this.normalizeInstallAction(actionName);
3146
+ if (action === 'uninstall') {
3147
+ return base;
3148
+ }
3149
+ const registry = this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
3150
+ if (!registry) {
3151
+ return base;
3152
+ }
3153
+ return `${base} --registry=${registry}`;
3154
+ },
3155
+
3156
+ resolveInstallPlatform() {
3157
+ const navPlatform = typeof navigator !== 'undefined' && typeof navigator.platform === 'string'
3158
+ ? navigator.platform.trim().toLowerCase()
3159
+ : '';
3160
+ if (navPlatform.includes('win')) return 'win32';
3161
+ if (navPlatform.includes('mac')) return 'darwin';
3162
+ return 'linux';
3163
+ },
3164
+
3165
+ buildInstallCommandMatrix(packageManager) {
3166
+ const manager = this.normalizeInstallPackageManager(packageManager);
3167
+ const matrix = {
3168
+ claude: {
3169
+ install: '',
3170
+ update: '',
3171
+ uninstall: ''
3172
+ },
3173
+ codex: {
3174
+ install: '',
3175
+ update: '',
3176
+ uninstall: ''
3177
+ }
3178
+ };
3179
+ if (manager === 'pnpm') {
3180
+ matrix.claude.install = 'pnpm add -g @anthropic-ai/claude-code';
3181
+ matrix.claude.update = 'pnpm up -g @anthropic-ai/claude-code';
3182
+ matrix.claude.uninstall = 'pnpm remove -g @anthropic-ai/claude-code';
3183
+ matrix.codex.install = 'pnpm add -g @openai/codex';
3184
+ matrix.codex.update = 'pnpm up -g @openai/codex';
3185
+ matrix.codex.uninstall = 'pnpm remove -g @openai/codex';
3186
+ return matrix;
3187
+ }
3188
+ if (manager === 'bun') {
3189
+ matrix.claude.install = 'bun add -g @anthropic-ai/claude-code';
3190
+ matrix.claude.update = 'bun update -g @anthropic-ai/claude-code';
3191
+ matrix.claude.uninstall = 'bun remove -g @anthropic-ai/claude-code';
3192
+ matrix.codex.install = 'bun add -g @openai/codex';
3193
+ matrix.codex.update = 'bun update -g @openai/codex';
3194
+ matrix.codex.uninstall = 'bun remove -g @openai/codex';
3195
+ return matrix;
3196
+ }
3197
+ matrix.claude.install = 'npm install -g @anthropic-ai/claude-code';
3198
+ matrix.claude.update = 'npm update -g @anthropic-ai/claude-code';
3199
+ matrix.claude.uninstall = 'npm uninstall -g @anthropic-ai/claude-code';
3200
+ matrix.codex.install = 'npm install -g @openai/codex';
3201
+ matrix.codex.update = 'npm update -g @openai/codex';
3202
+ matrix.codex.uninstall = 'npm uninstall -g @openai/codex';
3203
+ return matrix;
3204
+ },
3205
+
3206
+ getInstallCommand(targetId, actionName) {
3207
+ const targetKey = typeof targetId === 'string' ? targetId.trim() : '';
3208
+ if (!targetKey) return '';
3209
+ const action = this.normalizeInstallAction(actionName);
3210
+ const currentMap = this.buildInstallCommandMatrix(this.installPackageManager);
3211
+ const current = currentMap[targetKey] && typeof currentMap[targetKey][action] === 'string'
3212
+ ? currentMap[targetKey][action]
3213
+ : '';
3214
+ return this.appendInstallRegistryOption(current, action);
3215
+ },
3216
+
3217
+ setInstallCommandAction(actionName) {
3218
+ this.installCommandAction = this.normalizeInstallAction(actionName);
3219
+ },
3220
+
3221
+ setInstallRegistryPreset(presetName) {
3222
+ this.installRegistryPreset = this.normalizeInstallRegistryPreset(presetName);
3223
+ },
3224
+
3225
+ openInstallModal() {
3226
+ this.showInstallModal = true;
3227
+ },
3228
+
3229
+ closeInstallModal() {
3230
+ this.showInstallModal = false;
3231
+ },
3232
+
2633
3233
  async loadOpenclawConfigFromFile(options = {}) {
2634
3234
  const silent = !!options.silent;
2635
3235
  const force = !!options.force;
@@ -2655,11 +3255,11 @@
2655
3255
  }
2656
3256
  this.syncOpenclawStructuredFromText({ silent: true });
2657
3257
  if (!silent) {
2658
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
3258
+ this.showMessage('加载完成', 'success');
2659
3259
  }
2660
3260
  } catch (e) {
2661
3261
  if (!silent) {
2662
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
3262
+ this.showMessage('加载配置失败', 'error');
2663
3263
  }
2664
3264
  } finally {
2665
3265
  this.openclawFileLoading = false;
@@ -2668,12 +3268,12 @@
2668
3268
 
2669
3269
  persistOpenclawConfig({ closeModal = true } = {}) {
2670
3270
  if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
2671
- this.showMessage('请输入配置名称', 'error');
3271
+ this.showMessage('请输入名称', 'error');
2672
3272
  return '';
2673
3273
  }
2674
3274
  const name = this.openclawEditing.name.trim();
2675
3275
  if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
2676
- this.showMessage('配置名称已存在', 'error');
3276
+ this.showMessage('名称已存在', 'error');
2677
3277
  return '';
2678
3278
  }
2679
3279
  if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
@@ -2697,7 +3297,7 @@
2697
3297
  try {
2698
3298
  const name = this.persistOpenclawConfig();
2699
3299
  if (!name) return;
2700
- this.showMessage('OpenClaw 配置已保存', 'success');
3300
+ this.showMessage('操作成功', 'success');
2701
3301
  } finally {
2702
3302
  this.openclawSaving = false;
2703
3303
  }
@@ -2714,7 +3314,7 @@
2714
3314
  lineEnding: this.openclawLineEnding
2715
3315
  });
2716
3316
  if (res.error || res.success === false) {
2717
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3317
+ this.showMessage(res.error || '应用配置失败', 'error');
2718
3318
  return;
2719
3319
  }
2720
3320
  this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
@@ -2723,7 +3323,7 @@
2723
3323
  this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
2724
3324
  this.closeOpenclawConfigModal();
2725
3325
  } catch (e) {
2726
- this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
3326
+ this.showMessage('应用配置失败', 'error');
2727
3327
  } finally {
2728
3328
  this.openclawApplying = false;
2729
3329
  }
@@ -2731,7 +3331,7 @@
2731
3331
 
2732
3332
  deleteOpenclawConfig(name) {
2733
3333
  if (Object.keys(this.openclawConfigs).length <= 1) {
2734
- return this.showMessage('至少保留一个配置', 'error');
3334
+ return this.showMessage('至少保留一项', 'error');
2735
3335
  }
2736
3336
  if (!confirm(`确定删除配置 "${name}"?`)) return;
2737
3337
  delete this.openclawConfigs[name];
@@ -2739,21 +3339,21 @@
2739
3339
  this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
2740
3340
  }
2741
3341
  this.saveOpenclawConfigs();
2742
- this.showMessage('OpenClaw 配置已删除', 'success');
3342
+ this.showMessage('操作成功', 'success');
2743
3343
  },
2744
3344
 
2745
3345
  async applyOpenclawConfig(name) {
2746
3346
  this.currentOpenclawConfig = name;
2747
3347
  const config = this.openclawConfigs[name];
2748
3348
  if (!this.openclawHasContent(config)) {
2749
- return this.showMessage('该配置为空,请先编辑', 'error');
3349
+ return this.showMessage('配置为空', 'error');
2750
3350
  }
2751
3351
  const res = await api('apply-openclaw-config', {
2752
3352
  content: config.content,
2753
3353
  lineEnding: this.openclawLineEnding
2754
3354
  });
2755
3355
  if (res.error || res.success === false) {
2756
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3356
+ this.showMessage(res.error || '应用配置失败', 'error');
2757
3357
  } else {
2758
3358
  this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
2759
3359
  this.openclawConfigExists = true;
@@ -2826,6 +3426,427 @@
2826
3426
  }
2827
3427
  },
2828
3428
 
3429
+ async downloadClaudeDirectory() {
3430
+ if (this.claudeDownloadLoading) return;
3431
+ this.claudeDownloadLoading = true;
3432
+ this.claudeDownloadProgress = 5;
3433
+ this.claudeDownloadTimer = setInterval(() => {
3434
+ if (this.claudeDownloadProgress < 90) {
3435
+ this.claudeDownloadProgress += 5;
3436
+ }
3437
+ }, 400);
3438
+ try {
3439
+ const res = await api('download-claude-dir');
3440
+ if (res && res.error) {
3441
+ this.showMessage(res.error, 'error');
3442
+ return;
3443
+ }
3444
+ if (!res || res.success !== true || !res.fileName) {
3445
+ this.showMessage('备份失败', 'error');
3446
+ return;
3447
+ }
3448
+ this.claudeDownloadProgress = 100;
3449
+ const downloadUrl = `/download/${encodeURIComponent(res.fileName)}`;
3450
+ const link = document.createElement('a');
3451
+ link.href = downloadUrl;
3452
+ link.download = res.fileName;
3453
+ document.body.appendChild(link);
3454
+ link.click();
3455
+ document.body.removeChild(link);
3456
+ this.showMessage('备份成功,开始下载', 'success');
3457
+ } catch (e) {
3458
+ this.showMessage('备份失败:' + (e && e.message ? e.message : '未知错误'), 'error');
3459
+ } finally {
3460
+ if (this.claudeDownloadTimer) {
3461
+ clearInterval(this.claudeDownloadTimer);
3462
+ this.claudeDownloadTimer = null;
3463
+ }
3464
+ this.claudeDownloadLoading = false;
3465
+ setTimeout(() => {
3466
+ this.claudeDownloadProgress = 0;
3467
+ }, 800);
3468
+ }
3469
+ },
3470
+
3471
+ async downloadCodexDirectory() {
3472
+ if (this.codexDownloadLoading) return;
3473
+ this.codexDownloadLoading = true;
3474
+ this.codexDownloadProgress = 5;
3475
+ this.codexDownloadTimer = setInterval(() => {
3476
+ if (this.codexDownloadProgress < 90) {
3477
+ this.codexDownloadProgress += 5;
3478
+ }
3479
+ }, 400);
3480
+ try {
3481
+ const res = await api('download-codex-dir');
3482
+ if (res && res.error) {
3483
+ this.showMessage(res.error, 'error');
3484
+ return;
3485
+ }
3486
+ if (!res || res.success !== true || !res.fileName) {
3487
+ this.showMessage('备份失败', 'error');
3488
+ return;
3489
+ }
3490
+ this.codexDownloadProgress = 100;
3491
+ const downloadUrl = `/download/${encodeURIComponent(res.fileName)}`;
3492
+ const link = document.createElement('a');
3493
+ link.href = downloadUrl;
3494
+ link.download = res.fileName;
3495
+ document.body.appendChild(link);
3496
+ link.click();
3497
+ document.body.removeChild(link);
3498
+ this.showMessage('备份成功,开始下载', 'success');
3499
+ } catch (e) {
3500
+ this.showMessage('备份失败:' + (e && e.message ? e.message : '未知错误'), 'error');
3501
+ } finally {
3502
+ if (this.codexDownloadTimer) {
3503
+ clearInterval(this.codexDownloadTimer);
3504
+ this.codexDownloadTimer = null;
3505
+ }
3506
+ this.codexDownloadLoading = false;
3507
+ setTimeout(() => {
3508
+ this.codexDownloadProgress = 0;
3509
+ }, 800);
3510
+ }
3511
+ },
3512
+
3513
+ triggerClaudeImport() {
3514
+ const input = this.$refs.claudeImportInput;
3515
+ if (input) {
3516
+ input.value = '';
3517
+ input.click();
3518
+ }
3519
+ },
3520
+
3521
+ triggerCodexImport() {
3522
+ const input = this.$refs.codexImportInput;
3523
+ if (input) {
3524
+ input.value = '';
3525
+ input.click();
3526
+ }
3527
+ },
3528
+
3529
+ handleClaudeImportChange(event) {
3530
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
3531
+ if (file) {
3532
+ void this.importBackupFile('claude', file);
3533
+ }
3534
+ },
3535
+
3536
+ handleCodexImportChange(event) {
3537
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
3538
+ if (file) {
3539
+ void this.importBackupFile('codex', file);
3540
+ }
3541
+ },
3542
+
3543
+ async importBackupFile(type, file) {
3544
+ const maxSize = 200 * 1024 * 1024;
3545
+ const loadingKey = type === 'claude' ? 'claudeImportLoading' : 'codexImportLoading';
3546
+ if (file.size > maxSize) {
3547
+ this.showMessage('备份文件过大,限制 200MB', 'error');
3548
+ this.resetImportInput(type);
3549
+ return;
3550
+ }
3551
+ this[loadingKey] = true;
3552
+ try {
3553
+ const base64 = await this.readFileAsBase64(file);
3554
+ const action = type === 'claude' ? 'restore-claude-dir' : 'restore-codex-dir';
3555
+ const res = await api(action, {
3556
+ fileName: file.name || `${type}-backup.zip`,
3557
+ fileBase64: base64
3558
+ });
3559
+ if (res && res.error) {
3560
+ this.showMessage(res.error, 'error');
3561
+ return;
3562
+ }
3563
+ const backupTip = res && res.backupPath ? `,原配置已备份到临时文件:${res.backupPath}` : '';
3564
+ this.showMessage(`导入成功${backupTip}`, 'success');
3565
+ if (type === 'claude') {
3566
+ await this.refreshClaudeSelectionFromSettings({ silent: true });
3567
+ } else {
3568
+ await this.loadAll();
3569
+ }
3570
+ } catch (e) {
3571
+ this.showMessage('导入失败:' + (e && e.message ? e.message : '未知错误'), 'error');
3572
+ } finally {
3573
+ this[loadingKey] = false;
3574
+ this.resetImportInput(type);
3575
+ }
3576
+ },
3577
+
3578
+ readFileAsBase64(file) {
3579
+ return new Promise((resolve, reject) => {
3580
+ const reader = new FileReader();
3581
+ reader.onload = () => {
3582
+ const result = reader.result;
3583
+ if (result instanceof ArrayBuffer) {
3584
+ resolve(this.arrayBufferToBase64(result));
3585
+ return;
3586
+ }
3587
+ if (typeof result === 'string') {
3588
+ const idx = result.indexOf('base64,');
3589
+ resolve(idx >= 0 ? result.slice(idx + 7) : result);
3590
+ return;
3591
+ }
3592
+ reject(new Error('不支持的文件读取结果'));
3593
+ };
3594
+ reader.onerror = () => reject(new Error('读取文件失败'));
3595
+ reader.readAsArrayBuffer(file);
3596
+ });
3597
+ },
3598
+
3599
+ arrayBufferToBase64(buffer) {
3600
+ const bytes = new Uint8Array(buffer);
3601
+ const chunkSize = 0x8000;
3602
+ let binary = '';
3603
+ for (let i = 0; i < bytes.byteLength; i += chunkSize) {
3604
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
3605
+ }
3606
+ return btoa(binary);
3607
+ },
3608
+
3609
+ resetImportInput(type) {
3610
+ const refName = type === 'claude' ? 'claudeImportInput' : 'codexImportInput';
3611
+ const el = this.$refs[refName];
3612
+ if (el) {
3613
+ el.value = '';
3614
+ }
3615
+ },
3616
+
3617
+ async loadCodexAuthProfiles(options = {}) {
3618
+ const silent = !!options.silent;
3619
+ try {
3620
+ const res = await api('list-auth-profiles');
3621
+ if (res && res.error) {
3622
+ if (!silent) {
3623
+ this.showMessage(res.error, 'error');
3624
+ }
3625
+ return;
3626
+ }
3627
+ const list = Array.isArray(res && res.profiles) ? res.profiles : [];
3628
+ this.codexAuthProfiles = list.sort((a, b) => {
3629
+ if (!!a.current !== !!b.current) {
3630
+ return a.current ? -1 : 1;
3631
+ }
3632
+ return String(a.name || '').localeCompare(String(b.name || ''));
3633
+ });
3634
+ } catch (e) {
3635
+ if (!silent) {
3636
+ this.showMessage('读取认证列表失败', 'error');
3637
+ }
3638
+ }
3639
+ },
3640
+
3641
+ triggerCodexAuthUpload() {
3642
+ const input = this.$refs.codexAuthImportInput;
3643
+ if (input) {
3644
+ input.value = '';
3645
+ input.click();
3646
+ }
3647
+ },
3648
+
3649
+ handleCodexAuthImportChange(event) {
3650
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
3651
+ if (file) {
3652
+ void this.importCodexAuthFile(file);
3653
+ }
3654
+ },
3655
+
3656
+ resetCodexAuthImportInput() {
3657
+ const el = this.$refs.codexAuthImportInput;
3658
+ if (el) {
3659
+ el.value = '';
3660
+ }
3661
+ },
3662
+
3663
+ async importCodexAuthFile(file) {
3664
+ this.codexAuthImportLoading = true;
3665
+ try {
3666
+ const base64 = await this.readFileAsBase64(file);
3667
+ const res = await api('import-auth-profile', {
3668
+ fileName: file.name || 'codex-auth.json',
3669
+ fileBase64: base64,
3670
+ activate: true
3671
+ });
3672
+ if (res && res.error) {
3673
+ this.showMessage(res.error, 'error');
3674
+ return;
3675
+ }
3676
+ await this.loadCodexAuthProfiles({ silent: true });
3677
+ this.showMessage('认证文件已导入并切换', 'success');
3678
+ } catch (e) {
3679
+ this.showMessage('导入认证文件失败', 'error');
3680
+ } finally {
3681
+ this.codexAuthImportLoading = false;
3682
+ this.resetCodexAuthImportInput();
3683
+ }
3684
+ },
3685
+
3686
+ async switchCodexAuthProfile(name) {
3687
+ const key = String(name || '').trim();
3688
+ if (!key || this.codexAuthSwitching[key]) return;
3689
+ this.codexAuthSwitching[key] = true;
3690
+ try {
3691
+ const res = await api('switch-auth-profile', { name: key });
3692
+ if (res && res.error) {
3693
+ this.showMessage(res.error, 'error');
3694
+ return;
3695
+ }
3696
+ await this.loadCodexAuthProfiles({ silent: true });
3697
+ this.showMessage(`已切换认证: ${key}`, 'success');
3698
+ } catch (e) {
3699
+ this.showMessage('切换认证失败', 'error');
3700
+ } finally {
3701
+ this.codexAuthSwitching[key] = false;
3702
+ }
3703
+ },
3704
+
3705
+ async deleteCodexAuthProfile(name) {
3706
+ const key = String(name || '').trim();
3707
+ if (!key || this.codexAuthDeleting[key]) return;
3708
+ this.codexAuthDeleting[key] = true;
3709
+ try {
3710
+ const res = await api('delete-auth-profile', { name: key });
3711
+ if (res && res.error) {
3712
+ this.showMessage(res.error, 'error');
3713
+ return;
3714
+ }
3715
+ await this.loadCodexAuthProfiles({ silent: true });
3716
+ const switchedTip = res && res.switchedTo ? `,已切换到 ${res.switchedTo}` : '';
3717
+ this.showMessage(`已删除认证${switchedTip}`, 'success');
3718
+ } catch (e) {
3719
+ this.showMessage('删除认证失败', 'error');
3720
+ } finally {
3721
+ this.codexAuthDeleting[key] = false;
3722
+ }
3723
+ },
3724
+
3725
+ mergeProxySettings(nextSettings) {
3726
+ const safe = nextSettings && typeof nextSettings === 'object' ? nextSettings : {};
3727
+ const port = parseInt(String(safe.port), 10);
3728
+ const timeoutMs = parseInt(String(safe.timeoutMs), 10);
3729
+ this.proxySettings = {
3730
+ enabled: safe.enabled !== false,
3731
+ host: typeof safe.host === 'string' && safe.host.trim() ? safe.host.trim() : '127.0.0.1',
3732
+ port: Number.isFinite(port) ? port : 8318,
3733
+ provider: typeof safe.provider === 'string' ? safe.provider.trim() : '',
3734
+ authSource: safe.authSource === 'profile' || safe.authSource === 'none' ? safe.authSource : 'provider',
3735
+ timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 30000
3736
+ };
3737
+ },
3738
+
3739
+ async loadProxyStatus(options = {}) {
3740
+ const silent = !!options.silent;
3741
+ this.proxyLoading = true;
3742
+ try {
3743
+ const res = await api('proxy-status');
3744
+ if (res && res.error) {
3745
+ if (!silent) {
3746
+ this.showMessage(res.error, 'error');
3747
+ }
3748
+ return;
3749
+ }
3750
+ this.mergeProxySettings(res && res.settings ? res.settings : {});
3751
+ this.proxyRuntime = res && res.runtime ? { running: true, ...res.runtime } : null;
3752
+ } catch (e) {
3753
+ if (!silent) {
3754
+ this.showMessage('读取代理状态失败', 'error');
3755
+ }
3756
+ } finally {
3757
+ this.proxyLoading = false;
3758
+ }
3759
+ },
3760
+
3761
+ async saveProxySettings(options = {}) {
3762
+ const silent = !!options.silent;
3763
+ this.proxySaving = true;
3764
+ try {
3765
+ const res = await api('proxy-save-config', this.proxySettings);
3766
+ if (res && res.error) {
3767
+ if (!silent) {
3768
+ this.showMessage(res.error, 'error');
3769
+ }
3770
+ return;
3771
+ }
3772
+ if (res && res.settings) {
3773
+ this.mergeProxySettings(res.settings);
3774
+ }
3775
+ if (!silent) {
3776
+ this.showMessage('代理配置已保存', 'success');
3777
+ }
3778
+ } catch (e) {
3779
+ if (!silent) {
3780
+ this.showMessage('保存代理配置失败', 'error');
3781
+ }
3782
+ } finally {
3783
+ this.proxySaving = false;
3784
+ }
3785
+ },
3786
+
3787
+ async startBuiltinProxy() {
3788
+ this.proxyStarting = true;
3789
+ try {
3790
+ const res = await api('proxy-start', {
3791
+ ...this.proxySettings,
3792
+ enabled: true
3793
+ });
3794
+ if (res && res.error) {
3795
+ this.showMessage(res.error, 'error');
3796
+ return;
3797
+ }
3798
+ if (res && res.settings) {
3799
+ this.mergeProxySettings(res.settings);
3800
+ }
3801
+ await this.loadProxyStatus({ silent: true });
3802
+ const listenTip = res && res.listenUrl ? `:${res.listenUrl}` : '';
3803
+ this.showMessage(`代理已启动${listenTip}`, 'success');
3804
+ } catch (e) {
3805
+ this.showMessage('启动代理失败', 'error');
3806
+ } finally {
3807
+ this.proxyStarting = false;
3808
+ }
3809
+ },
3810
+
3811
+ async stopBuiltinProxy() {
3812
+ this.proxyStopping = true;
3813
+ try {
3814
+ const res = await api('proxy-stop');
3815
+ if (res && res.error) {
3816
+ this.showMessage(res.error, 'error');
3817
+ return;
3818
+ }
3819
+ await this.loadProxyStatus({ silent: true });
3820
+ this.showMessage('代理已停止', 'success');
3821
+ } catch (e) {
3822
+ this.showMessage('停止代理失败', 'error');
3823
+ } finally {
3824
+ this.proxyStopping = false;
3825
+ }
3826
+ },
3827
+
3828
+ async applyBuiltinProxyProvider() {
3829
+ this.proxyApplying = true;
3830
+ try {
3831
+ const saveRes = await api('proxy-save-config', this.proxySettings);
3832
+ if (saveRes && saveRes.error) {
3833
+ this.showMessage(saveRes.error, 'error');
3834
+ return;
3835
+ }
3836
+ const res = await api('proxy-apply-provider', { switchToProxy: true });
3837
+ if (res && res.error) {
3838
+ this.showMessage(res.error, 'error');
3839
+ return;
3840
+ }
3841
+ await this.loadAll();
3842
+ this.showMessage('本地代理 provider 已写入并切换', 'success');
3843
+ } catch (e) {
3844
+ this.showMessage('应用代理 provider 失败', 'error');
3845
+ } finally {
3846
+ this.proxyApplying = false;
3847
+ }
3848
+ },
3849
+
2829
3850
  showMessage(text, type) {
2830
3851
  this.message = text;
2831
3852
  this.messageType = type || 'info';
@@ -2839,3 +3860,4 @@
2839
3860
  app.mount('#app');
2840
3861
  });
2841
3862
 
3863
+