codexmate 0.0.12 → 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.
package/web-ui/app.js CHANGED
@@ -137,13 +137,35 @@
137
137
  claudeSpeedLoading: {},
138
138
  claudeShareLoading: {},
139
139
  providerShareLoading: {},
140
- installCommands: [
141
- 'npm install -g @anthropic-ai/claude-code',
142
- 'npm i -g @openai/codex'
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
+ }
143
165
  ],
144
166
  newProvider: { name: '', url: '', key: '' },
145
167
  resetConfigLoading: false,
146
- editingProvider: { name: '', url: '', key: '' },
168
+ editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
147
169
  newModelName: '',
148
170
  currentClaudeConfig: '',
149
171
  currentClaudeModel: '',
@@ -209,7 +231,34 @@
209
231
  openclawMissingProviders: [],
210
232
  healthCheckLoading: false,
211
233
  healthCheckResult: null,
212
- 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
213
262
  }
214
263
  },
215
264
  mounted() {
@@ -276,6 +325,162 @@
276
325
  list.unshift(current);
277
326
  }
278
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
+ ];
279
484
  }
280
485
  },
281
486
  methods: {
@@ -323,6 +528,15 @@
323
528
  } catch (e) {
324
529
  // loadModelsForProvider 内部已有 toast,这里吞掉防止抛出
325
530
  }
531
+
532
+ try {
533
+ await Promise.all([
534
+ this.loadCodexAuthProfiles(),
535
+ this.loadProxyStatus()
536
+ ]);
537
+ } catch (e) {
538
+ // 认证/代理状态加载失败不阻塞主界面
539
+ }
326
540
  },
327
541
 
328
542
  async loadModelsForProvider(providerName) {
@@ -704,12 +918,39 @@
704
918
  this.showMessage('复制失败', 'error');
705
919
  },
706
920
 
707
- copyInstallCommand(cmd) {
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) {
708
940
  const text = typeof cmd === 'string' ? cmd.trim() : '';
709
941
  if (!text) {
710
942
  this.showMessage('没有可复制内容', 'info');
711
943
  return;
712
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
+ }
713
954
  const ok = this.fallbackCopyText(text);
714
955
  if (ok) {
715
956
  this.showMessage('已复制命令', 'success');
@@ -778,6 +1019,10 @@
778
1019
  this.showMessage('参数无效', 'error');
779
1020
  return;
780
1021
  }
1022
+ if (!this.shouldAllowProviderShare(provider)) {
1023
+ this.showMessage('本地入口不可分享', 'info');
1024
+ return;
1025
+ }
781
1026
  if (this.providerShareLoading[name]) {
782
1027
  return;
783
1028
  }
@@ -1256,10 +1501,10 @@
1256
1501
  }
1257
1502
  },
1258
1503
 
1259
- downloadTextFile(fileName, content) {
1504
+ downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
1260
1505
  // 使用 UTF-8 BOM 确保文本编辑器正确识别编码
1261
1506
  const BOM = '\uFEFF';
1262
- const blob = new Blob([BOM + content], { type: 'text/markdown;charset=utf-8' });
1507
+ const blob = new Blob([BOM + content], { type: mimeType });
1263
1508
  const url = URL.createObjectURL(blob);
1264
1509
  const link = document.createElement('a');
1265
1510
  link.href = url;
@@ -1632,6 +1877,9 @@
1632
1877
  if (!name) {
1633
1878
  return this.showMessage('名称不能为空', 'error');
1634
1879
  }
1880
+ if (name.toLowerCase() === 'local') {
1881
+ return this.showMessage('local provider 为系统保留名称,不可新增', 'error');
1882
+ }
1635
1883
  if (this.providersList.some(item => item.name === name)) {
1636
1884
  return this.showMessage('名称已存在', 'error');
1637
1885
  }
@@ -1655,7 +1903,89 @@
1655
1903
  }
1656
1904
  },
1657
1905
 
1906
+ getCurrentCodexAuthProfile() {
1907
+ const list = Array.isArray(this.codexAuthProfiles) ? this.codexAuthProfiles : [];
1908
+ return list.find((item) => !!(item && item.current)) || null;
1909
+ },
1910
+
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);
1982
+ },
1983
+
1658
1984
  async deleteProvider(name) {
1985
+ if (this.isNonDeletableProvider(name)) {
1986
+ this.showMessage('该 provider 为保留项,不可删除', 'info');
1987
+ return;
1988
+ }
1659
1989
  const res = await api('delete-provider', { name });
1660
1990
  if (res.error) {
1661
1991
  this.showMessage(res.error, 'error');
@@ -1670,15 +2000,26 @@
1670
2000
  },
1671
2001
 
1672
2002
  openEditModal(provider) {
2003
+ if (!this.shouldShowProviderEdit(provider)) {
2004
+ this.showMessage('该 provider 为保留项,不可编辑', 'info');
2005
+ return;
2006
+ }
1673
2007
  this.editingProvider = {
1674
2008
  name: provider.name,
1675
2009
  url: provider.url || '',
1676
- key: ''
2010
+ key: '',
2011
+ readOnly: !!provider.readOnly,
2012
+ nonEditable: this.isNonDeletableProvider(provider)
1677
2013
  };
1678
2014
  this.showEditModal = true;
1679
2015
  },
1680
2016
 
1681
2017
  async updateProvider() {
2018
+ if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
2019
+ this.showMessage('该 provider 为保留项,不可编辑', 'error');
2020
+ this.closeEditModal();
2021
+ return;
2022
+ }
1682
2023
  if (!this.editingProvider.url) {
1683
2024
  return this.showMessage('URL 必填', 'error');
1684
2025
  }
@@ -1702,7 +2043,7 @@
1702
2043
 
1703
2044
  closeEditModal() {
1704
2045
  this.showEditModal = false;
1705
- this.editingProvider = { name: '', url: '', key: '' };
2046
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false };
1706
2047
  },
1707
2048
 
1708
2049
  async resetConfig() {
@@ -2751,6 +3092,136 @@
2751
3092
  this.resetOpenclawQuick();
2752
3093
  },
2753
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
+
2754
3225
  openInstallModal() {
2755
3226
  this.showInstallModal = true;
2756
3227
  },
@@ -2955,6 +3426,427 @@
2955
3426
  }
2956
3427
  },
2957
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
+
2958
3850
  showMessage(text, type) {
2959
3851
  this.message = text;
2960
3852
  this.messageType = type || 'info';
@@ -2968,3 +3860,4 @@
2968
3860
  app.mount('#app');
2969
3861
  });
2970
3862
 
3863
+