codexmate 0.0.12 → 0.0.14

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
@@ -7,7 +7,12 @@
7
7
  formatLatency,
8
8
  buildSpeedTestIssue,
9
9
  isSessionQueryEnabled,
10
- buildSessionListParams
10
+ buildSessionListParams,
11
+ normalizeSessionSource,
12
+ normalizeSessionPathFilter,
13
+ buildSessionFilterCacheState,
14
+ buildSessionTimelineNodes,
15
+ normalizeSessionMessageRole
11
16
  } from './logic.mjs';
12
17
 
13
18
  document.addEventListener('DOMContentLoaded', () => {
@@ -81,6 +86,7 @@
81
86
  showOpenclawConfigModal: false,
82
87
  showConfigTemplateModal: false,
83
88
  showAgentsModal: false,
89
+ showSkillsModal: false,
84
90
  showInstallModal: false,
85
91
  configTemplateContent: '',
86
92
  configTemplateApplying: false,
@@ -94,6 +100,17 @@
94
100
  agentsContext: 'codex',
95
101
  agentsModalTitle: 'AGENTS.md 编辑器',
96
102
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
103
+ skillsRootPath: '',
104
+ skillsList: [],
105
+ skillsSelectedNames: [],
106
+ skillsLoading: false,
107
+ skillsDeleting: false,
108
+ skillsKeyword: '',
109
+ skillsStatusFilter: 'all',
110
+ skillsImportList: [],
111
+ skillsImportSelectedKeys: [],
112
+ skillsScanningImports: false,
113
+ skillsImporting: false,
97
114
  sessionsList: [],
98
115
  sessionsLoading: false,
99
116
  sessionFilterSource: 'all',
@@ -124,6 +141,13 @@
124
141
  activeSessionDetailClipped: false,
125
142
  sessionDetailLoading: false,
126
143
  sessionDetailRequestSeq: 0,
144
+ sessionTimelineActiveKey: '',
145
+ sessionTimelineRafId: 0,
146
+ sessionMessageRefMap: Object.create(null),
147
+ sessionPreviewScrollEl: null,
148
+ sessionPreviewContainerEl: null,
149
+ sessionPreviewHeaderEl: null,
150
+ sessionPreviewHeaderResizeObserver: null,
127
151
  sessionStandalone: false,
128
152
  sessionStandaloneError: '',
129
153
  sessionStandaloneText: '',
@@ -137,13 +161,35 @@
137
161
  claudeSpeedLoading: {},
138
162
  claudeShareLoading: {},
139
163
  providerShareLoading: {},
140
- installCommands: [
141
- 'npm install -g @anthropic-ai/claude-code',
142
- 'npm i -g @openai/codex'
164
+ installPackageManager: 'npm',
165
+ installCommandAction: 'install',
166
+ installRegistryPreset: 'default',
167
+ installRegistryCustom: '',
168
+ installStatusTargets: [
169
+ {
170
+ id: 'claude',
171
+ name: 'Claude Code CLI',
172
+ packageName: '@anthropic-ai/claude-code',
173
+ installed: false,
174
+ bin: 'claude',
175
+ version: '',
176
+ commandPath: '',
177
+ error: ''
178
+ },
179
+ {
180
+ id: 'codex',
181
+ name: 'Codex CLI',
182
+ packageName: '@openai/codex',
183
+ installed: false,
184
+ bin: 'codex',
185
+ version: '',
186
+ commandPath: '',
187
+ error: ''
188
+ }
143
189
  ],
144
190
  newProvider: { name: '', url: '', key: '' },
145
191
  resetConfigLoading: false,
146
- editingProvider: { name: '', url: '', key: '' },
192
+ editingProvider: { name: '', url: '', key: '', readOnly: false, nonEditable: false },
147
193
  newModelName: '',
148
194
  currentClaudeConfig: '',
149
195
  currentClaudeModel: '',
@@ -209,7 +255,34 @@
209
255
  openclawMissingProviders: [],
210
256
  healthCheckLoading: false,
211
257
  healthCheckResult: null,
212
- healthCheckRemote: false
258
+ healthCheckRemote: false,
259
+ claudeDownloadLoading: false,
260
+ claudeDownloadProgress: 0,
261
+ claudeDownloadTimer: null,
262
+ codexDownloadLoading: false,
263
+ codexDownloadProgress: 0,
264
+ codexDownloadTimer: null,
265
+ claudeImportLoading: false,
266
+ codexImportLoading: false,
267
+ codexAuthProfiles: [],
268
+ codexAuthImportLoading: false,
269
+ codexAuthSwitching: {},
270
+ codexAuthDeleting: {},
271
+ proxySettings: {
272
+ enabled: false,
273
+ host: '127.0.0.1',
274
+ port: 8318,
275
+ provider: '',
276
+ authSource: 'provider',
277
+ timeoutMs: 30000
278
+ },
279
+ proxyRuntime: null,
280
+ proxyLoading: false,
281
+ proxySaving: false,
282
+ proxyStarting: false,
283
+ proxyStopping: false,
284
+ proxyApplying: false,
285
+ showProxyAdvanced: false
213
286
  }
214
287
  },
215
288
  mounted() {
@@ -220,6 +293,8 @@
220
293
  } else if (savedSessionYolo === '1' || savedSessionYolo === 'true') {
221
294
  this.sessionResumeWithYolo = true;
222
295
  }
296
+ this.restoreSessionFilterCache();
297
+ window.addEventListener('resize', this.onWindowResize);
223
298
  const savedConfigs = localStorage.getItem('claudeConfigs');
224
299
  if (savedConfigs) {
225
300
  try {
@@ -255,11 +330,31 @@
255
330
  }
256
331
  this.loadAll();
257
332
  },
333
+ beforeUnmount() {
334
+ this.cancelSessionTimelineSync();
335
+ this.disconnectSessionPreviewHeaderResizeObserver();
336
+ window.removeEventListener('resize', this.onWindowResize);
337
+ this.sessionPreviewScrollEl = null;
338
+ this.sessionPreviewContainerEl = null;
339
+ this.sessionPreviewHeaderEl = null;
340
+ this.sessionMessageRefMap = Object.create(null);
341
+ },
258
342
 
259
343
  computed: {
260
344
  isSessionQueryEnabled() {
261
345
  return isSessionQueryEnabled(this.sessionFilterSource);
262
346
  },
347
+ sessionTimelineNodes() {
348
+ return buildSessionTimelineNodes(this.activeSessionMessages, {
349
+ getKey: (message, index) => this.getRecordRenderKey(message, index)
350
+ });
351
+ },
352
+ sessionTimelineActiveTitle() {
353
+ if (!this.sessionTimelineActiveKey) return '';
354
+ const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
355
+ const matched = nodes.find(node => node.key === this.sessionTimelineActiveKey);
356
+ return matched ? matched.title : '';
357
+ },
263
358
  sessionQueryPlaceholder() {
264
359
  if (this.isSessionQueryEnabled) {
265
360
  return '关键词检索(支持 Codex/Claude,例:claude code)';
@@ -276,6 +371,241 @@
276
371
  list.unshift(current);
277
372
  }
278
373
  return list;
374
+ },
375
+ proxyProviderOptions() {
376
+ const source = Array.isArray(this.providersList) ? this.providersList : [];
377
+ const list = source
378
+ .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
379
+ .filter((name) => name && name !== 'codexmate-proxy');
380
+ return Array.from(new Set(list));
381
+ },
382
+ proxyRuntimeDisplayProvider() {
383
+ if (!this.proxyRuntime) return '';
384
+ const value = typeof this.proxyRuntime.provider === 'string'
385
+ ? this.proxyRuntime.provider.trim()
386
+ : '';
387
+ return value || 'local';
388
+ },
389
+ installTargetCards() {
390
+ const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
391
+ const action = this.normalizeInstallAction(this.installCommandAction);
392
+ return targets.map((target) => {
393
+ const id = target && typeof target.id === 'string' ? target.id : '';
394
+ return {
395
+ ...target,
396
+ command: this.getInstallCommand(id, action)
397
+ };
398
+ });
399
+ },
400
+ installRegistryPreview() {
401
+ return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
402
+ },
403
+ filteredSkillsList() {
404
+ const list = Array.isArray(this.skillsList) ? this.skillsList : [];
405
+ const keyword = typeof this.skillsKeyword === 'string' ? this.skillsKeyword.trim().toLowerCase() : '';
406
+ const status = typeof this.skillsStatusFilter === 'string' ? this.skillsStatusFilter : 'all';
407
+ return list.filter((item) => {
408
+ const safe = item && typeof item === 'object' ? item : {};
409
+ const hasSkillFile = !!safe.hasSkillFile;
410
+ if (status === 'with-skill-file' && !hasSkillFile) return false;
411
+ if (status === 'missing-skill-file' && hasSkillFile) return false;
412
+ if (!keyword) return true;
413
+ const fields = [
414
+ safe.name,
415
+ safe.displayName,
416
+ safe.description,
417
+ safe.path
418
+ ];
419
+ return fields.some((value) => typeof value === 'string' && value.toLowerCase().includes(keyword));
420
+ });
421
+ },
422
+ skillsSelectableNames() {
423
+ const list = Array.isArray(this.filteredSkillsList) ? this.filteredSkillsList : [];
424
+ return list
425
+ .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
426
+ .filter(Boolean);
427
+ },
428
+ skillsConfiguredCount() {
429
+ const list = Array.isArray(this.skillsList) ? this.skillsList : [];
430
+ return list.filter((item) => !!(item && item.hasSkillFile)).length;
431
+ },
432
+ skillsMissingSkillFileCount() {
433
+ const list = Array.isArray(this.skillsList) ? this.skillsList : [];
434
+ return list.filter((item) => !(item && item.hasSkillFile)).length;
435
+ },
436
+ skillsFilterDirty() {
437
+ const keyword = typeof this.skillsKeyword === 'string' ? this.skillsKeyword.trim() : '';
438
+ const status = typeof this.skillsStatusFilter === 'string' ? this.skillsStatusFilter : 'all';
439
+ return keyword.length > 0 || status !== 'all';
440
+ },
441
+ skillsSelectedCount() {
442
+ const selected = Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : [];
443
+ return Array.from(new Set(selected.map((item) => String(item || '').trim()).filter(Boolean))).length;
444
+ },
445
+ skillsVisibleSelectedCount() {
446
+ const selectable = this.skillsSelectableNames;
447
+ const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
448
+ return selectable.filter((name) => selectedSet.has(name)).length;
449
+ },
450
+ skillsAllSelected() {
451
+ const selectable = this.skillsSelectableNames;
452
+ if (!selectable.length) return false;
453
+ const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
454
+ return selectable.every((name) => selectedSet.has(name));
455
+ },
456
+ skillsImportSelectableKeys() {
457
+ const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
458
+ return list
459
+ .map((item) => this.buildSkillImportKey(item))
460
+ .filter(Boolean);
461
+ },
462
+ skillsImportSelectedCount() {
463
+ const selectable = this.skillsImportSelectableKeys;
464
+ const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
465
+ return selectable.filter((key) => selectedSet.has(key)).length;
466
+ },
467
+ skillsImportAllSelected() {
468
+ const selectable = this.skillsImportSelectableKeys;
469
+ if (!selectable.length) return false;
470
+ const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
471
+ return selectable.every((key) => selectedSet.has(key));
472
+ },
473
+ skillsImportConfiguredCount() {
474
+ const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
475
+ return list.filter((item) => !!(item && item.hasSkillFile)).length;
476
+ },
477
+ skillsImportMissingSkillFileCount() {
478
+ const list = Array.isArray(this.skillsImportList) ? this.skillsImportList : [];
479
+ return list.filter((item) => !(item && item.hasSkillFile)).length;
480
+ },
481
+ inspectorMainTabLabel() {
482
+ if (this.mainTab === 'config') return '配置中心';
483
+ if (this.mainTab === 'sessions') return '会话浏览';
484
+ if (this.mainTab === 'settings') return '设置';
485
+ return '未知';
486
+ },
487
+ inspectorConfigModeLabel() {
488
+ if (this.mainTab !== 'config') return '--';
489
+ if (this.configMode === 'codex') return 'Codex';
490
+ if (this.configMode === 'claude') return 'Claude Code';
491
+ if (this.configMode === 'openclaw') return 'OpenClaw';
492
+ return '未选择';
493
+ },
494
+ inspectorCurrentConfigLabel() {
495
+ if (this.mainTab !== 'config') return '--';
496
+ if (this.configMode === 'codex') {
497
+ const provider = typeof this.currentProvider === 'string' ? this.currentProvider.trim() : '';
498
+ return provider || '未选择';
499
+ }
500
+ if (this.configMode === 'claude') {
501
+ const config = typeof this.currentClaudeConfig === 'string' ? this.currentClaudeConfig.trim() : '';
502
+ return config || '未选择';
503
+ }
504
+ const openclaw = typeof this.currentOpenclawConfig === 'string' ? this.currentOpenclawConfig.trim() : '';
505
+ return openclaw || '未选择';
506
+ },
507
+ inspectorCurrentModelLabel() {
508
+ if (this.mainTab !== 'config') return '--';
509
+ if (this.configMode === 'codex') {
510
+ const model = typeof this.currentModel === 'string' ? this.currentModel.trim() : '';
511
+ return model || '未选择';
512
+ }
513
+ if (this.configMode === 'claude') {
514
+ const model = typeof this.currentClaudeModel === 'string' ? this.currentClaudeModel.trim() : '';
515
+ return model || '未选择';
516
+ }
517
+ const model = this.openclawStructured && typeof this.openclawStructured.agentPrimary === 'string'
518
+ ? this.openclawStructured.agentPrimary.trim()
519
+ : '';
520
+ return model || '按配置文件';
521
+ },
522
+ inspectorTemplateStatus() {
523
+ if (this.mainTab !== 'config') return '--';
524
+ if (this.configMode === 'codex') {
525
+ if (this.configTemplateApplying || this.codexApplying) {
526
+ return '模板应用中';
527
+ }
528
+ return '模板可编辑(手动确认应用)';
529
+ }
530
+ if (this.configMode === 'claude') {
531
+ return '即时写入 Claude settings';
532
+ }
533
+ if (this.openclawApplying || this.openclawSaving) {
534
+ return 'OpenClaw 保存/应用中';
535
+ }
536
+ return 'JSON5 可保存并应用';
537
+ },
538
+ inspectorBusyStatus() {
539
+ const tasks = [];
540
+ if (this.loading) tasks.push('初始化');
541
+ if (this.sessionsLoading) tasks.push('会话加载');
542
+ if (this.codexModelsLoading || this.claudeModelsLoading) tasks.push('模型加载');
543
+ if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
544
+ if (this.agentsSaving) tasks.push('AGENTS 保存');
545
+ if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting) tasks.push('Skills 管理');
546
+ if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) tasks.push('代理更新');
547
+ return tasks.length ? tasks.join(' / ') : '空闲';
548
+ },
549
+ inspectorMessageSummary() {
550
+ const value = typeof this.message === 'string' ? this.message.trim() : '';
551
+ return value || '暂无提示';
552
+ },
553
+ inspectorSessionSourceLabel() {
554
+ if (this.sessionFilterSource === 'codex') return 'Codex';
555
+ if (this.sessionFilterSource === 'claude') return 'Claude Code';
556
+ return '全部';
557
+ },
558
+ inspectorSessionPathLabel() {
559
+ const value = typeof this.sessionPathFilter === 'string' ? this.sessionPathFilter.trim() : '';
560
+ return value || '全部路径';
561
+ },
562
+ inspectorSessionQueryLabel() {
563
+ if (!this.isSessionQueryEnabled) return '当前来源不支持';
564
+ const value = typeof this.sessionQuery === 'string' ? this.sessionQuery.trim() : '';
565
+ return value || '未设置';
566
+ },
567
+ inspectorHealthStatus() {
568
+ if (this.initError) return '读取失败';
569
+ if (this.loading) return '初始化中';
570
+ return '正常';
571
+ },
572
+ inspectorHealthTone() {
573
+ if (this.initError) return 'error';
574
+ if (this.loading) return 'warn';
575
+ return 'ok';
576
+ },
577
+ inspectorModelLoadStatus() {
578
+ if (this.codexModelsLoading || this.claudeModelsLoading) {
579
+ return '加载中';
580
+ }
581
+ if (this.modelsSource === 'error' || this.claudeModelsSource === 'error') {
582
+ return '加载异常';
583
+ }
584
+ return '正常';
585
+ },
586
+ inspectorProxyStatus() {
587
+ if (this.proxySaving || this.proxyApplying || this.proxyStarting || this.proxyStopping) {
588
+ return '状态更新中';
589
+ }
590
+ if (this.proxyRuntime && this.proxyRuntime.running === true) {
591
+ return `运行中(${this.proxyRuntimeDisplayProvider})`;
592
+ }
593
+ return '未运行';
594
+ },
595
+ installTroubleshootingTips() {
596
+ const platform = this.resolveInstallPlatform();
597
+ if (platform === 'win32') {
598
+ return [
599
+ 'PowerShell 报权限不足(EACCES/EPERM)时,请以管理员身份执行安装命令。',
600
+ '安装后若仍提示找不到命令,重开终端并执行:where codex / where claude。',
601
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
602
+ ];
603
+ }
604
+ return [
605
+ '出现 EACCES 权限错误时,优先修复 Node 全局目录权限,不建议直接 sudo npm。',
606
+ '安装后若命令未生效,重开终端并执行:which codex / which claude。',
607
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
608
+ ];
279
609
  }
280
610
  },
281
611
  methods: {
@@ -323,6 +653,15 @@
323
653
  } catch (e) {
324
654
  // loadModelsForProvider 内部已有 toast,这里吞掉防止抛出
325
655
  }
656
+
657
+ try {
658
+ await Promise.all([
659
+ this.loadCodexAuthProfiles(),
660
+ this.loadProxyStatus()
661
+ ]);
662
+ } catch (e) {
663
+ // 认证/代理状态加载失败不阻塞主界面
664
+ }
326
665
  },
327
666
 
328
667
  async loadModelsForProvider(providerName) {
@@ -585,6 +924,9 @@
585
924
  this.activeSessionMessages = [];
586
925
  this.activeSessionDetailError = '';
587
926
  this.activeSessionDetailClipped = false;
927
+ this.cancelSessionTimelineSync();
928
+ this.sessionTimelineActiveKey = '';
929
+ this.sessionMessageRefMap = Object.create(null);
588
930
  this.sessionStandaloneError = '';
589
931
  this.sessionStandaloneText = '';
590
932
  this.sessionStandaloneTitle = this.activeSession.title || '会话';
@@ -704,12 +1046,39 @@
704
1046
  this.showMessage('复制失败', 'error');
705
1047
  },
706
1048
 
707
- copyInstallCommand(cmd) {
1049
+ exportAgentsContent() {
1050
+ const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
1051
+ if (!text) {
1052
+ this.showMessage('没有可导出内容', 'info');
1053
+ return;
1054
+ }
1055
+ const now = new Date();
1056
+ const year = String(now.getFullYear());
1057
+ const month = String(now.getMonth() + 1).padStart(2, '0');
1058
+ const day = String(now.getDate()).padStart(2, '0');
1059
+ const hour = String(now.getHours()).padStart(2, '0');
1060
+ const minute = String(now.getMinutes()).padStart(2, '0');
1061
+ const second = String(now.getSeconds()).padStart(2, '0');
1062
+ const fileName = `agent-${year}${month}${day}-${hour}${minute}${second}.txt`;
1063
+ this.downloadTextFile(fileName, text, 'text/plain;charset=utf-8');
1064
+ this.showMessage(`已导出 ${fileName}`, 'success');
1065
+ },
1066
+
1067
+ async copyInstallCommand(cmd) {
708
1068
  const text = typeof cmd === 'string' ? cmd.trim() : '';
709
1069
  if (!text) {
710
1070
  this.showMessage('没有可复制内容', 'info');
711
1071
  return;
712
1072
  }
1073
+ try {
1074
+ if (navigator.clipboard && window.isSecureContext) {
1075
+ await navigator.clipboard.writeText(text);
1076
+ this.showMessage('已复制命令', 'success');
1077
+ return;
1078
+ }
1079
+ } catch (e) {
1080
+ // fallback to legacy copy path
1081
+ }
713
1082
  const ok = this.fallbackCopyText(text);
714
1083
  if (ok) {
715
1084
  this.showMessage('已复制命令', 'success');
@@ -778,6 +1147,10 @@
778
1147
  this.showMessage('参数无效', 'error');
779
1148
  return;
780
1149
  }
1150
+ if (!this.shouldAllowProviderShare(provider)) {
1151
+ this.showMessage('本地入口不可分享', 'info');
1152
+ return;
1153
+ }
781
1154
  if (this.providerShareLoading[name]) {
782
1155
  return;
783
1156
  }
@@ -922,8 +1295,7 @@
922
1295
  },
923
1296
 
924
1297
  normalizeSessionPathValue(value) {
925
- if (typeof value !== 'string') return '';
926
- return value.trim();
1298
+ return normalizeSessionPathFilter(value);
927
1299
  },
928
1300
 
929
1301
  mergeSessionPathOptions(baseList = [], incomingList = []) {
@@ -1032,13 +1404,32 @@
1032
1404
  const value = this.sessionResumeWithYolo ? '1' : '0';
1033
1405
  localStorage.setItem('codexmateSessionResumeYolo', value);
1034
1406
  },
1407
+ restoreSessionFilterCache() {
1408
+ const sourceCache = localStorage.getItem('codexmateSessionFilterSource');
1409
+ const pathCache = localStorage.getItem('codexmateSessionPathFilter');
1410
+ const cached = buildSessionFilterCacheState(sourceCache, pathCache);
1411
+ this.sessionFilterSource = cached.source;
1412
+ this.sessionPathFilter = cached.pathFilter;
1413
+ this.refreshSessionPathOptions(this.sessionFilterSource);
1414
+ },
1415
+ persistSessionFilterCache() {
1416
+ const cached = buildSessionFilterCacheState(this.sessionFilterSource, this.sessionPathFilter);
1417
+ localStorage.setItem('codexmateSessionFilterSource', cached.source);
1418
+ if (cached.pathFilter) {
1419
+ localStorage.setItem('codexmateSessionPathFilter', cached.pathFilter);
1420
+ } else {
1421
+ localStorage.removeItem('codexmateSessionPathFilter');
1422
+ }
1423
+ },
1035
1424
 
1036
1425
  async onSessionSourceChange() {
1037
1426
  this.refreshSessionPathOptions(this.sessionFilterSource);
1427
+ this.persistSessionFilterCache();
1038
1428
  await this.loadSessions();
1039
1429
  },
1040
1430
 
1041
1431
  async onSessionPathFilterChange() {
1432
+ this.persistSessionFilterCache();
1042
1433
  await this.loadSessions();
1043
1434
  },
1044
1435
 
@@ -1052,8 +1443,156 @@
1052
1443
  this.sessionQuery = '';
1053
1444
  this.sessionRoleFilter = 'all';
1054
1445
  this.sessionTimePreset = 'all';
1446
+ this.persistSessionFilterCache();
1055
1447
  await this.onSessionSourceChange();
1056
1448
  },
1449
+ setSessionPreviewContainerRef(el) {
1450
+ this.sessionPreviewContainerEl = el || null;
1451
+ this.updateSessionTimelineOffset();
1452
+ },
1453
+ disconnectSessionPreviewHeaderResizeObserver() {
1454
+ if (!this.sessionPreviewHeaderResizeObserver) return;
1455
+ this.sessionPreviewHeaderResizeObserver.disconnect();
1456
+ this.sessionPreviewHeaderResizeObserver = null;
1457
+ },
1458
+ observeSessionPreviewHeaderResize() {
1459
+ this.disconnectSessionPreviewHeaderResizeObserver();
1460
+ if (!this.sessionPreviewHeaderEl || typeof ResizeObserver !== 'function') return;
1461
+ this.sessionPreviewHeaderResizeObserver = new ResizeObserver(() => {
1462
+ this.updateSessionTimelineOffset();
1463
+ });
1464
+ this.sessionPreviewHeaderResizeObserver.observe(this.sessionPreviewHeaderEl);
1465
+ },
1466
+ setSessionPreviewHeaderRef(el) {
1467
+ this.disconnectSessionPreviewHeaderResizeObserver();
1468
+ this.sessionPreviewHeaderEl = el || null;
1469
+ this.observeSessionPreviewHeaderResize();
1470
+ this.updateSessionTimelineOffset();
1471
+ },
1472
+ setSessionPreviewScrollRef(el) {
1473
+ this.sessionPreviewScrollEl = el || null;
1474
+ if (this.sessionPreviewScrollEl) {
1475
+ this.scheduleSessionTimelineSync();
1476
+ } else {
1477
+ this.cancelSessionTimelineSync();
1478
+ }
1479
+ this.updateSessionTimelineOffset();
1480
+ },
1481
+ updateSessionTimelineOffset() {
1482
+ const container = this.sessionPreviewContainerEl || this.$refs.sessionPreviewContainer;
1483
+ if (!container || !container.style) return;
1484
+ const header = this.sessionPreviewHeaderEl
1485
+ || (this.sessionPreviewScrollEl ? this.sessionPreviewScrollEl.querySelector('.session-preview-header') : null)
1486
+ || container.querySelector('.session-preview-header');
1487
+ const headerHeight = header ? Math.ceil(header.getBoundingClientRect().height) : 0;
1488
+ const offset = headerHeight > 0 ? (headerHeight + 12) : 72;
1489
+ container.style.setProperty('--session-preview-header-offset', `${offset}px`);
1490
+ },
1491
+ bindSessionMessageRef(messageKey, el) {
1492
+ if (!messageKey) return;
1493
+ if (el) {
1494
+ this.sessionMessageRefMap[messageKey] = el;
1495
+ } else {
1496
+ delete this.sessionMessageRefMap[messageKey];
1497
+ }
1498
+ },
1499
+ cancelSessionTimelineSync() {
1500
+ if (!this.sessionTimelineRafId) return;
1501
+ if (typeof cancelAnimationFrame === 'function') {
1502
+ cancelAnimationFrame(this.sessionTimelineRafId);
1503
+ }
1504
+ this.sessionTimelineRafId = 0;
1505
+ },
1506
+ scheduleSessionTimelineSync() {
1507
+ if (this.sessionTimelineRafId) return;
1508
+ if (typeof requestAnimationFrame === 'function') {
1509
+ this.sessionTimelineRafId = requestAnimationFrame(() => {
1510
+ this.sessionTimelineRafId = 0;
1511
+ this.syncSessionTimelineActiveFromScroll();
1512
+ });
1513
+ return;
1514
+ }
1515
+ this.syncSessionTimelineActiveFromScroll();
1516
+ },
1517
+ onSessionPreviewScroll() {
1518
+ this.scheduleSessionTimelineSync();
1519
+ },
1520
+ onWindowResize() {
1521
+ this.updateSessionTimelineOffset();
1522
+ this.scheduleSessionTimelineSync();
1523
+ },
1524
+ syncSessionTimelineActiveFromScroll() {
1525
+ const nodes = Array.isArray(this.sessionTimelineNodes) ? this.sessionTimelineNodes : [];
1526
+ if (!nodes.length) {
1527
+ this.sessionTimelineActiveKey = '';
1528
+ return;
1529
+ }
1530
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1531
+ if (!scrollEl) {
1532
+ this.sessionTimelineActiveKey = nodes[0].key;
1533
+ return;
1534
+ }
1535
+ const scrollRect = scrollEl.getBoundingClientRect();
1536
+ const headerEl = scrollEl.querySelector('.session-preview-header');
1537
+ const headerHeight = headerEl ? headerEl.getBoundingClientRect().height : 0;
1538
+ const anchorLine = scrollRect.top + headerHeight + 8;
1539
+ let activeKey = nodes[0].key;
1540
+ for (const node of nodes) {
1541
+ const messageEl = this.sessionMessageRefMap[node.key];
1542
+ if (!messageEl) continue;
1543
+ const messageRect = messageEl.getBoundingClientRect();
1544
+ if (messageRect.top <= anchorLine) {
1545
+ activeKey = node.key;
1546
+ continue;
1547
+ }
1548
+ break;
1549
+ }
1550
+ this.sessionTimelineActiveKey = activeKey;
1551
+ },
1552
+ jumpToSessionTimelineNode(messageKey) {
1553
+ if (!messageKey) return;
1554
+ const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
1555
+ if (!scrollEl) return;
1556
+ const messageEl = this.sessionMessageRefMap[messageKey];
1557
+ if (!messageEl) return;
1558
+ const headerEl = scrollEl.querySelector('.session-preview-header');
1559
+ const stickyOffset = headerEl ? (headerEl.offsetHeight + 8) : 8;
1560
+ const scrollRect = scrollEl.getBoundingClientRect();
1561
+ const messageRect = messageEl.getBoundingClientRect();
1562
+ const targetScrollTop = scrollEl.scrollTop + (messageRect.top - scrollRect.top) - stickyOffset;
1563
+ this.sessionTimelineActiveKey = messageKey;
1564
+ if (typeof scrollEl.scrollTo === 'function') {
1565
+ scrollEl.scrollTo({
1566
+ top: Math.max(0, targetScrollTop),
1567
+ behavior: 'smooth'
1568
+ });
1569
+ } else {
1570
+ messageEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
1571
+ }
1572
+ },
1573
+
1574
+ normalizeSessionMessage(message) {
1575
+ const fallback = {
1576
+ role: 'assistant',
1577
+ normalizedRole: 'assistant',
1578
+ roleLabel: 'Assistant',
1579
+ text: typeof message === 'string' ? message : '',
1580
+ timestamp: ''
1581
+ };
1582
+ const safeMessage = message && typeof message === 'object' ? message : fallback;
1583
+ const normalizedRole = normalizeSessionMessageRole(
1584
+ safeMessage.normalizedRole || safeMessage.role
1585
+ );
1586
+ const roleLabel = normalizedRole === 'user'
1587
+ ? 'User'
1588
+ : (normalizedRole === 'system' ? 'System' : 'Assistant');
1589
+ return {
1590
+ ...safeMessage,
1591
+ role: normalizedRole,
1592
+ normalizedRole,
1593
+ roleLabel
1594
+ };
1595
+ },
1057
1596
 
1058
1597
  getRecordKey(message) {
1059
1598
  if (!message || !Number.isInteger(message.recordLineIndex) || message.recordLineIndex < 0) {
@@ -1102,6 +1641,9 @@
1102
1641
  this.activeSession = null;
1103
1642
  this.activeSessionMessages = [];
1104
1643
  this.activeSessionDetailClipped = false;
1644
+ this.cancelSessionTimelineSync();
1645
+ this.sessionTimelineActiveKey = '';
1646
+ this.sessionMessageRefMap = Object.create(null);
1105
1647
  } else {
1106
1648
  this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
1107
1649
  this.syncSessionPathOptionsForSource(
@@ -1113,10 +1655,19 @@
1113
1655
  this.activeSession = null;
1114
1656
  this.activeSessionMessages = [];
1115
1657
  this.activeSessionDetailClipped = false;
1658
+ this.cancelSessionTimelineSync();
1659
+ this.sessionTimelineActiveKey = '';
1660
+ this.sessionMessageRefMap = Object.create(null);
1116
1661
  } else {
1117
1662
  const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
1118
1663
  const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
1119
1664
  this.activeSession = matched || this.sessionsList[0];
1665
+ this.activeSessionMessages = [];
1666
+ this.activeSessionDetailError = '';
1667
+ this.activeSessionDetailClipped = false;
1668
+ this.cancelSessionTimelineSync();
1669
+ this.sessionTimelineActiveKey = '';
1670
+ this.sessionMessageRefMap = Object.create(null);
1120
1671
  await this.loadActiveSessionDetail();
1121
1672
  }
1122
1673
  void this.loadSessionPathOptions({ source: this.sessionFilterSource });
@@ -1126,6 +1677,9 @@
1126
1677
  this.activeSession = null;
1127
1678
  this.activeSessionMessages = [];
1128
1679
  this.activeSessionDetailClipped = false;
1680
+ this.cancelSessionTimelineSync();
1681
+ this.sessionTimelineActiveKey = '';
1682
+ this.sessionMessageRefMap = Object.create(null);
1129
1683
  this.showMessage('加载会话失败', 'error');
1130
1684
  } finally {
1131
1685
  this.sessionsLoading = false;
@@ -1139,6 +1693,9 @@
1139
1693
  this.activeSessionMessages = [];
1140
1694
  this.activeSessionDetailError = '';
1141
1695
  this.activeSessionDetailClipped = false;
1696
+ this.cancelSessionTimelineSync();
1697
+ this.sessionTimelineActiveKey = '';
1698
+ this.sessionMessageRefMap = Object.create(null);
1142
1699
  await this.loadActiveSessionDetail();
1143
1700
  },
1144
1701
 
@@ -1192,6 +1749,9 @@
1192
1749
  this.activeSessionMessages = [];
1193
1750
  this.activeSessionDetailError = '';
1194
1751
  this.activeSessionDetailClipped = false;
1752
+ this.cancelSessionTimelineSync();
1753
+ this.sessionTimelineActiveKey = '';
1754
+ this.sessionMessageRefMap = Object.create(null);
1195
1755
  return;
1196
1756
  }
1197
1757
 
@@ -1214,10 +1774,14 @@
1214
1774
  this.activeSessionMessages = [];
1215
1775
  this.activeSessionDetailClipped = false;
1216
1776
  this.activeSessionDetailError = res.error;
1777
+ this.cancelSessionTimelineSync();
1778
+ this.sessionTimelineActiveKey = '';
1779
+ this.sessionMessageRefMap = Object.create(null);
1217
1780
  return;
1218
1781
  }
1219
1782
 
1220
- this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
1783
+ const rawMessages = Array.isArray(res.messages) ? res.messages : [];
1784
+ this.activeSessionMessages = rawMessages.map((message) => this.normalizeSessionMessage(message));
1221
1785
  this.activeSessionDetailClipped = !!res.clipped;
1222
1786
  if (this.activeSession) {
1223
1787
  if (res.sourceLabel) {
@@ -1242,6 +1806,10 @@
1242
1806
  if (Number.isFinite(res.totalMessages)) {
1243
1807
  this.syncActiveSessionMessageCount(res.totalMessages);
1244
1808
  }
1809
+ this.$nextTick(() => {
1810
+ this.updateSessionTimelineOffset();
1811
+ this.scheduleSessionTimelineSync();
1812
+ });
1245
1813
  } catch (e) {
1246
1814
  if (requestSeq !== this.sessionDetailRequestSeq) {
1247
1815
  return;
@@ -1249,6 +1817,9 @@
1249
1817
  this.activeSessionMessages = [];
1250
1818
  this.activeSessionDetailClipped = false;
1251
1819
  this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
1820
+ this.cancelSessionTimelineSync();
1821
+ this.sessionTimelineActiveKey = '';
1822
+ this.sessionMessageRefMap = Object.create(null);
1252
1823
  } finally {
1253
1824
  if (requestSeq === this.sessionDetailRequestSeq) {
1254
1825
  this.sessionDetailLoading = false;
@@ -1256,10 +1827,10 @@
1256
1827
  }
1257
1828
  },
1258
1829
 
1259
- downloadTextFile(fileName, content) {
1830
+ downloadTextFile(fileName, content, mimeType = 'text/markdown;charset=utf-8') {
1260
1831
  // 使用 UTF-8 BOM 确保文本编辑器正确识别编码
1261
1832
  const BOM = '\uFEFF';
1262
- const blob = new Blob([BOM + content], { type: 'text/markdown;charset=utf-8' });
1833
+ const blob = new Blob([BOM + content], { type: mimeType });
1263
1834
  const url = URL.createObjectURL(blob);
1264
1835
  const link = document.createElement('a');
1265
1836
  link.href = url;
@@ -1509,6 +2080,191 @@
1509
2080
  }
1510
2081
  },
1511
2082
 
2083
+ async openSkillsManager() {
2084
+ this.skillsSelectedNames = [];
2085
+ this.skillsKeyword = '';
2086
+ this.skillsStatusFilter = 'all';
2087
+ this.skillsImportList = [];
2088
+ this.skillsImportSelectedKeys = [];
2089
+ this.showSkillsModal = true;
2090
+ await this.refreshSkillsList({ silent: false });
2091
+ },
2092
+
2093
+ closeSkillsModal() {
2094
+ this.showSkillsModal = false;
2095
+ this.skillsSelectedNames = [];
2096
+ this.skillsImportSelectedKeys = [];
2097
+ },
2098
+
2099
+ async refreshSkillsList(options = {}) {
2100
+ this.skillsLoading = true;
2101
+ try {
2102
+ const res = await api('list-codex-skills');
2103
+ if (res.error) {
2104
+ this.showMessage(res.error, 'error');
2105
+ return;
2106
+ }
2107
+ this.skillsRootPath = res.root || '';
2108
+ this.skillsList = Array.isArray(res.items) ? res.items : [];
2109
+ const currentNames = new Set((Array.isArray(this.skillsList) ? this.skillsList : [])
2110
+ .map((item) => (item && typeof item.name === 'string' ? item.name.trim() : ''))
2111
+ .filter(Boolean));
2112
+ this.skillsSelectedNames = (Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : [])
2113
+ .filter((name) => currentNames.has(name));
2114
+ if (!options.silent) {
2115
+ const exists = res.exists !== false;
2116
+ if (!exists) {
2117
+ this.showMessage('skills 目录不存在,已按空列表显示', 'info');
2118
+ }
2119
+ }
2120
+ } catch (e) {
2121
+ this.showMessage('加载 skills 失败', 'error');
2122
+ } finally {
2123
+ this.skillsLoading = false;
2124
+ }
2125
+ },
2126
+
2127
+ resetSkillsFilters() {
2128
+ this.skillsKeyword = '';
2129
+ this.skillsStatusFilter = 'all';
2130
+ },
2131
+
2132
+ toggleAllSkillsSelection() {
2133
+ const selectable = this.skillsSelectableNames;
2134
+ if (this.skillsAllSelected) {
2135
+ const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
2136
+ selectable.forEach((name) => selectedSet.delete(name));
2137
+ this.skillsSelectedNames = Array.from(selectedSet);
2138
+ return;
2139
+ }
2140
+ const selectedSet = new Set(Array.isArray(this.skillsSelectedNames) ? this.skillsSelectedNames : []);
2141
+ selectable.forEach((name) => selectedSet.add(name));
2142
+ this.skillsSelectedNames = Array.from(selectedSet);
2143
+ },
2144
+
2145
+ buildSkillImportKey(item) {
2146
+ const safe = item && typeof item === 'object' ? item : {};
2147
+ const sourceApp = typeof safe.sourceApp === 'string' ? safe.sourceApp.trim().toLowerCase() : '';
2148
+ const name = typeof safe.name === 'string' ? safe.name.trim() : '';
2149
+ if (!sourceApp || !name) return '';
2150
+ return `${sourceApp}:${name}`;
2151
+ },
2152
+
2153
+ toggleAllSkillsImportSelection() {
2154
+ const selectable = this.skillsImportSelectableKeys;
2155
+ if (this.skillsImportAllSelected) {
2156
+ this.skillsImportSelectedKeys = [];
2157
+ return;
2158
+ }
2159
+ this.skillsImportSelectedKeys = [...selectable];
2160
+ },
2161
+
2162
+ async scanImportableSkills() {
2163
+ if (this.skillsScanningImports || this.skillsImporting) return;
2164
+ this.skillsScanningImports = true;
2165
+ try {
2166
+ const res = await api('scan-unmanaged-codex-skills');
2167
+ if (res.error) {
2168
+ this.showMessage(res.error, 'error');
2169
+ return;
2170
+ }
2171
+ this.skillsImportList = Array.isArray(res.items) ? res.items : [];
2172
+ const availableKeys = new Set(this.skillsImportSelectableKeys);
2173
+ this.skillsImportSelectedKeys = (Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : [])
2174
+ .filter((key) => availableKeys.has(key));
2175
+ if (this.skillsImportList.length === 0) {
2176
+ this.showMessage('未扫描到可导入 skill', 'info');
2177
+ } else {
2178
+ this.showMessage(`扫描到 ${this.skillsImportList.length} 个可导入 skill`, 'success');
2179
+ }
2180
+ } catch (e) {
2181
+ this.showMessage('扫描可导入 skill 失败', 'error');
2182
+ } finally {
2183
+ this.skillsScanningImports = false;
2184
+ }
2185
+ },
2186
+
2187
+ async importSelectedSkills() {
2188
+ if (this.skillsImporting) return;
2189
+ const selectedSet = new Set(Array.isArray(this.skillsImportSelectedKeys) ? this.skillsImportSelectedKeys : []);
2190
+ const selectedItems = (Array.isArray(this.skillsImportList) ? this.skillsImportList : [])
2191
+ .filter((item) => selectedSet.has(this.buildSkillImportKey(item)))
2192
+ .map((item) => ({
2193
+ name: item.name,
2194
+ sourceApp: item.sourceApp
2195
+ }));
2196
+ if (!selectedItems.length) {
2197
+ this.showMessage('请先选择要导入的 skill', 'error');
2198
+ return;
2199
+ }
2200
+
2201
+ this.skillsImporting = true;
2202
+ try {
2203
+ const res = await api('import-codex-skills', { items: selectedItems });
2204
+ if (res.error) {
2205
+ this.showMessage(res.error, 'error');
2206
+ return;
2207
+ }
2208
+ const importedCount = Array.isArray(res.imported) ? res.imported.length : 0;
2209
+ const failedCount = Array.isArray(res.failed) ? res.failed.length : 0;
2210
+ if (failedCount > 0 && importedCount > 0) {
2211
+ this.showMessage(`已导入 ${importedCount} 个,失败 ${failedCount} 个`, 'error');
2212
+ } else if (failedCount > 0) {
2213
+ const first = res.failed[0] && res.failed[0].error ? res.failed[0].error : '导入失败';
2214
+ this.showMessage(first, 'error');
2215
+ } else {
2216
+ this.showMessage(`已导入 ${importedCount} 个 skill`, 'success');
2217
+ }
2218
+ await this.refreshSkillsList({ silent: true });
2219
+ } catch (e) {
2220
+ this.showMessage('导入 skill 失败', 'error');
2221
+ } finally {
2222
+ this.skillsImporting = false;
2223
+ await this.scanImportableSkills();
2224
+ }
2225
+ },
2226
+
2227
+ async deleteSelectedSkills() {
2228
+ if (this.skillsDeleting) return;
2229
+ const selected = Array.isArray(this.skillsSelectedNames)
2230
+ ? Array.from(new Set(this.skillsSelectedNames.map((item) => String(item || '').trim()).filter(Boolean)))
2231
+ : [];
2232
+ if (!selected.length) {
2233
+ this.showMessage('请先选择要删除的 skill', 'error');
2234
+ return;
2235
+ }
2236
+ const confirmed = window.confirm(`确认删除 ${selected.length} 个 skill 吗?此操作不可撤销。`);
2237
+ if (!confirmed) {
2238
+ return;
2239
+ }
2240
+
2241
+ this.skillsDeleting = true;
2242
+ try {
2243
+ const res = await api('delete-codex-skills', { names: selected });
2244
+ if (res.error) {
2245
+ this.showMessage(res.error, 'error');
2246
+ return;
2247
+ }
2248
+
2249
+ const deletedCount = Array.isArray(res.deleted) ? res.deleted.length : 0;
2250
+ const failedList = Array.isArray(res.failed) ? res.failed : [];
2251
+ const failedCount = failedList.length;
2252
+ if (failedCount > 0 && deletedCount > 0) {
2253
+ this.showMessage(`已删除 ${deletedCount} 个,失败 ${failedCount} 个`, 'error');
2254
+ } else if (failedCount > 0) {
2255
+ const first = failedList[0] && failedList[0].error ? failedList[0].error : '删除失败';
2256
+ this.showMessage(first, 'error');
2257
+ } else {
2258
+ this.showMessage(`已删除 ${deletedCount} 个 skill`, 'success');
2259
+ }
2260
+ await this.refreshSkillsList({ silent: true });
2261
+ } catch (e) {
2262
+ this.showMessage('删除 skill 失败', 'error');
2263
+ } finally {
2264
+ this.skillsDeleting = false;
2265
+ }
2266
+ },
2267
+
1512
2268
  async openOpenclawAgentsEditor() {
1513
2269
  this.setAgentsModalContext('openclaw');
1514
2270
  this.agentsLoading = true;
@@ -1632,6 +2388,9 @@
1632
2388
  if (!name) {
1633
2389
  return this.showMessage('名称不能为空', 'error');
1634
2390
  }
2391
+ if (name.toLowerCase() === 'local') {
2392
+ return this.showMessage('local provider 为系统保留名称,不可新增', 'error');
2393
+ }
1635
2394
  if (this.providersList.some(item => item.name === name)) {
1636
2395
  return this.showMessage('名称已存在', 'error');
1637
2396
  }
@@ -1655,7 +2414,89 @@
1655
2414
  }
1656
2415
  },
1657
2416
 
2417
+ getCurrentCodexAuthProfile() {
2418
+ const list = Array.isArray(this.codexAuthProfiles) ? this.codexAuthProfiles : [];
2419
+ return list.find((item) => !!(item && item.current)) || null;
2420
+ },
2421
+
2422
+ isLocalLikeProvider(providerOrName) {
2423
+ if (!providerOrName) return false;
2424
+ const rawName = typeof providerOrName === 'object'
2425
+ ? String(providerOrName.name || '')
2426
+ : String(providerOrName);
2427
+ const normalized = rawName.trim().toLowerCase();
2428
+ return normalized === 'local' || normalized === 'codexmate-proxy';
2429
+ },
2430
+
2431
+ providerPillState(provider) {
2432
+ if (this.isLocalLikeProvider(provider)) {
2433
+ const currentProfile = this.getCurrentCodexAuthProfile();
2434
+ return currentProfile
2435
+ ? { configured: true, text: '已登录' }
2436
+ : { configured: false, text: '未登录' };
2437
+ }
2438
+ const configured = !!(provider && provider.hasKey);
2439
+ return {
2440
+ configured,
2441
+ text: configured ? '已配置' : '未配置'
2442
+ };
2443
+ },
2444
+
2445
+ providerPillConfigured(provider) {
2446
+ return this.providerPillState(provider).configured;
2447
+ },
2448
+
2449
+ providerPillText(provider) {
2450
+ return this.providerPillState(provider).text;
2451
+ },
2452
+
2453
+ isReadOnlyProvider(providerOrName) {
2454
+ if (!providerOrName) return false;
2455
+ if (typeof providerOrName === 'object') {
2456
+ return !!providerOrName.readOnly;
2457
+ }
2458
+ const name = String(providerOrName).trim();
2459
+ if (!name) return false;
2460
+ const target = (this.providersList || []).find((item) => item && item.name === name);
2461
+ return !!(target && target.readOnly);
2462
+ },
2463
+
2464
+ isNonDeletableProvider(providerOrName) {
2465
+ if (!providerOrName) return false;
2466
+ if (typeof providerOrName === 'object') {
2467
+ const directName = String(providerOrName.name || '').trim().toLowerCase();
2468
+ if (directName === 'local' || directName === 'codexmate-proxy') {
2469
+ return true;
2470
+ }
2471
+ return !!providerOrName.nonDeletable;
2472
+ }
2473
+ const name = String(providerOrName).trim();
2474
+ if (!name) return false;
2475
+ const normalized = name.toLowerCase();
2476
+ if (normalized === 'local' || normalized === 'codexmate-proxy') {
2477
+ return true;
2478
+ }
2479
+ const target = (this.providersList || []).find((item) => item && item.name === name);
2480
+ return !!(target && target.nonDeletable);
2481
+ },
2482
+
2483
+ shouldShowProviderDelete(provider) {
2484
+ return !this.isReadOnlyProvider(provider) && !this.isNonDeletableProvider(provider);
2485
+ },
2486
+
2487
+ shouldShowProviderEdit(provider) {
2488
+ return !this.isReadOnlyProvider(provider) && !this.isNonDeletableProvider(provider);
2489
+ },
2490
+
2491
+ shouldAllowProviderShare(provider) {
2492
+ return !this.isReadOnlyProvider(provider) && !this.isLocalLikeProvider(provider);
2493
+ },
2494
+
1658
2495
  async deleteProvider(name) {
2496
+ if (this.isNonDeletableProvider(name)) {
2497
+ this.showMessage('该 provider 为保留项,不可删除', 'info');
2498
+ return;
2499
+ }
1659
2500
  const res = await api('delete-provider', { name });
1660
2501
  if (res.error) {
1661
2502
  this.showMessage(res.error, 'error');
@@ -1670,15 +2511,26 @@
1670
2511
  },
1671
2512
 
1672
2513
  openEditModal(provider) {
2514
+ if (!this.shouldShowProviderEdit(provider)) {
2515
+ this.showMessage('该 provider 为保留项,不可编辑', 'info');
2516
+ return;
2517
+ }
1673
2518
  this.editingProvider = {
1674
2519
  name: provider.name,
1675
2520
  url: provider.url || '',
1676
- key: ''
2521
+ key: '',
2522
+ readOnly: !!provider.readOnly,
2523
+ nonEditable: this.isNonDeletableProvider(provider)
1677
2524
  };
1678
2525
  this.showEditModal = true;
1679
2526
  },
1680
2527
 
1681
2528
  async updateProvider() {
2529
+ if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
2530
+ this.showMessage('该 provider 为保留项,不可编辑', 'error');
2531
+ this.closeEditModal();
2532
+ return;
2533
+ }
1682
2534
  if (!this.editingProvider.url) {
1683
2535
  return this.showMessage('URL 必填', 'error');
1684
2536
  }
@@ -1702,7 +2554,7 @@
1702
2554
 
1703
2555
  closeEditModal() {
1704
2556
  this.showEditModal = false;
1705
- this.editingProvider = { name: '', url: '', key: '' };
2557
+ this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false };
1706
2558
  },
1707
2559
 
1708
2560
  async resetConfig() {
@@ -2751,6 +3603,136 @@
2751
3603
  this.resetOpenclawQuick();
2752
3604
  },
2753
3605
 
3606
+ normalizeInstallPackageManager(value) {
3607
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3608
+ if (normalized === 'pnpm' || normalized === 'bun' || normalized === 'npm') {
3609
+ return normalized;
3610
+ }
3611
+ return 'npm';
3612
+ },
3613
+
3614
+ normalizeInstallAction(value) {
3615
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3616
+ if (normalized === 'update' || normalized === 'uninstall' || normalized === 'install') {
3617
+ return normalized;
3618
+ }
3619
+ return 'install';
3620
+ },
3621
+
3622
+ normalizeInstallRegistryPreset(value) {
3623
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
3624
+ if (normalized === 'default' || normalized === 'npmmirror' || normalized === 'tencent' || normalized === 'custom') {
3625
+ return normalized;
3626
+ }
3627
+ return 'default';
3628
+ },
3629
+
3630
+ normalizeInstallRegistryUrl(value) {
3631
+ const normalized = typeof value === 'string' ? value.trim() : '';
3632
+ if (!normalized) return '';
3633
+ if (!/^https?:\/\//i.test(normalized)) {
3634
+ return '';
3635
+ }
3636
+ return normalized.replace(/\/+$/, '');
3637
+ },
3638
+
3639
+ resolveInstallRegistryUrl(presetValue, customValue) {
3640
+ const preset = this.normalizeInstallRegistryPreset(presetValue);
3641
+ if (preset === 'npmmirror') {
3642
+ return 'https://registry.npmmirror.com';
3643
+ }
3644
+ if (preset === 'tencent') {
3645
+ return 'https://mirrors.cloud.tencent.com/npm';
3646
+ }
3647
+ if (preset === 'custom') {
3648
+ return this.normalizeInstallRegistryUrl(customValue);
3649
+ }
3650
+ return '';
3651
+ },
3652
+
3653
+ appendInstallRegistryOption(command, actionName) {
3654
+ const base = typeof command === 'string' ? command.trim() : '';
3655
+ if (!base) return '';
3656
+ const action = this.normalizeInstallAction(actionName);
3657
+ if (action === 'uninstall') {
3658
+ return base;
3659
+ }
3660
+ const registry = this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
3661
+ if (!registry) {
3662
+ return base;
3663
+ }
3664
+ return `${base} --registry=${registry}`;
3665
+ },
3666
+
3667
+ resolveInstallPlatform() {
3668
+ const navPlatform = typeof navigator !== 'undefined' && typeof navigator.platform === 'string'
3669
+ ? navigator.platform.trim().toLowerCase()
3670
+ : '';
3671
+ if (navPlatform.includes('win')) return 'win32';
3672
+ if (navPlatform.includes('mac')) return 'darwin';
3673
+ return 'linux';
3674
+ },
3675
+
3676
+ buildInstallCommandMatrix(packageManager) {
3677
+ const manager = this.normalizeInstallPackageManager(packageManager);
3678
+ const matrix = {
3679
+ claude: {
3680
+ install: '',
3681
+ update: '',
3682
+ uninstall: ''
3683
+ },
3684
+ codex: {
3685
+ install: '',
3686
+ update: '',
3687
+ uninstall: ''
3688
+ }
3689
+ };
3690
+ if (manager === 'pnpm') {
3691
+ matrix.claude.install = 'pnpm add -g @anthropic-ai/claude-code';
3692
+ matrix.claude.update = 'pnpm up -g @anthropic-ai/claude-code';
3693
+ matrix.claude.uninstall = 'pnpm remove -g @anthropic-ai/claude-code';
3694
+ matrix.codex.install = 'pnpm add -g @openai/codex';
3695
+ matrix.codex.update = 'pnpm up -g @openai/codex';
3696
+ matrix.codex.uninstall = 'pnpm remove -g @openai/codex';
3697
+ return matrix;
3698
+ }
3699
+ if (manager === 'bun') {
3700
+ matrix.claude.install = 'bun add -g @anthropic-ai/claude-code';
3701
+ matrix.claude.update = 'bun update -g @anthropic-ai/claude-code';
3702
+ matrix.claude.uninstall = 'bun remove -g @anthropic-ai/claude-code';
3703
+ matrix.codex.install = 'bun add -g @openai/codex';
3704
+ matrix.codex.update = 'bun update -g @openai/codex';
3705
+ matrix.codex.uninstall = 'bun remove -g @openai/codex';
3706
+ return matrix;
3707
+ }
3708
+ matrix.claude.install = 'npm install -g @anthropic-ai/claude-code';
3709
+ matrix.claude.update = 'npm update -g @anthropic-ai/claude-code';
3710
+ matrix.claude.uninstall = 'npm uninstall -g @anthropic-ai/claude-code';
3711
+ matrix.codex.install = 'npm install -g @openai/codex';
3712
+ matrix.codex.update = 'npm update -g @openai/codex';
3713
+ matrix.codex.uninstall = 'npm uninstall -g @openai/codex';
3714
+ return matrix;
3715
+ },
3716
+
3717
+ getInstallCommand(targetId, actionName) {
3718
+ const targetKey = typeof targetId === 'string' ? targetId.trim() : '';
3719
+ if (!targetKey) return '';
3720
+ const action = this.normalizeInstallAction(actionName);
3721
+ const currentMap = this.buildInstallCommandMatrix(this.installPackageManager);
3722
+ const current = currentMap[targetKey] && typeof currentMap[targetKey][action] === 'string'
3723
+ ? currentMap[targetKey][action]
3724
+ : '';
3725
+ return this.appendInstallRegistryOption(current, action);
3726
+ },
3727
+
3728
+ setInstallCommandAction(actionName) {
3729
+ this.installCommandAction = this.normalizeInstallAction(actionName);
3730
+ },
3731
+
3732
+ setInstallRegistryPreset(presetName) {
3733
+ this.installRegistryPreset = this.normalizeInstallRegistryPreset(presetName);
3734
+ },
3735
+
2754
3736
  openInstallModal() {
2755
3737
  this.showInstallModal = true;
2756
3738
  },
@@ -2955,6 +3937,427 @@
2955
3937
  }
2956
3938
  },
2957
3939
 
3940
+ async downloadClaudeDirectory() {
3941
+ if (this.claudeDownloadLoading) return;
3942
+ this.claudeDownloadLoading = true;
3943
+ this.claudeDownloadProgress = 5;
3944
+ this.claudeDownloadTimer = setInterval(() => {
3945
+ if (this.claudeDownloadProgress < 90) {
3946
+ this.claudeDownloadProgress += 5;
3947
+ }
3948
+ }, 400);
3949
+ try {
3950
+ const res = await api('download-claude-dir');
3951
+ if (res && res.error) {
3952
+ this.showMessage(res.error, 'error');
3953
+ return;
3954
+ }
3955
+ if (!res || res.success !== true || !res.fileName) {
3956
+ this.showMessage('备份失败', 'error');
3957
+ return;
3958
+ }
3959
+ this.claudeDownloadProgress = 100;
3960
+ const downloadUrl = `/download/${encodeURIComponent(res.fileName)}`;
3961
+ const link = document.createElement('a');
3962
+ link.href = downloadUrl;
3963
+ link.download = res.fileName;
3964
+ document.body.appendChild(link);
3965
+ link.click();
3966
+ document.body.removeChild(link);
3967
+ this.showMessage('备份成功,开始下载', 'success');
3968
+ } catch (e) {
3969
+ this.showMessage('备份失败:' + (e && e.message ? e.message : '未知错误'), 'error');
3970
+ } finally {
3971
+ if (this.claudeDownloadTimer) {
3972
+ clearInterval(this.claudeDownloadTimer);
3973
+ this.claudeDownloadTimer = null;
3974
+ }
3975
+ this.claudeDownloadLoading = false;
3976
+ setTimeout(() => {
3977
+ this.claudeDownloadProgress = 0;
3978
+ }, 800);
3979
+ }
3980
+ },
3981
+
3982
+ async downloadCodexDirectory() {
3983
+ if (this.codexDownloadLoading) return;
3984
+ this.codexDownloadLoading = true;
3985
+ this.codexDownloadProgress = 5;
3986
+ this.codexDownloadTimer = setInterval(() => {
3987
+ if (this.codexDownloadProgress < 90) {
3988
+ this.codexDownloadProgress += 5;
3989
+ }
3990
+ }, 400);
3991
+ try {
3992
+ const res = await api('download-codex-dir');
3993
+ if (res && res.error) {
3994
+ this.showMessage(res.error, 'error');
3995
+ return;
3996
+ }
3997
+ if (!res || res.success !== true || !res.fileName) {
3998
+ this.showMessage('备份失败', 'error');
3999
+ return;
4000
+ }
4001
+ this.codexDownloadProgress = 100;
4002
+ const downloadUrl = `/download/${encodeURIComponent(res.fileName)}`;
4003
+ const link = document.createElement('a');
4004
+ link.href = downloadUrl;
4005
+ link.download = res.fileName;
4006
+ document.body.appendChild(link);
4007
+ link.click();
4008
+ document.body.removeChild(link);
4009
+ this.showMessage('备份成功,开始下载', 'success');
4010
+ } catch (e) {
4011
+ this.showMessage('备份失败:' + (e && e.message ? e.message : '未知错误'), 'error');
4012
+ } finally {
4013
+ if (this.codexDownloadTimer) {
4014
+ clearInterval(this.codexDownloadTimer);
4015
+ this.codexDownloadTimer = null;
4016
+ }
4017
+ this.codexDownloadLoading = false;
4018
+ setTimeout(() => {
4019
+ this.codexDownloadProgress = 0;
4020
+ }, 800);
4021
+ }
4022
+ },
4023
+
4024
+ triggerClaudeImport() {
4025
+ const input = this.$refs.claudeImportInput;
4026
+ if (input) {
4027
+ input.value = '';
4028
+ input.click();
4029
+ }
4030
+ },
4031
+
4032
+ triggerCodexImport() {
4033
+ const input = this.$refs.codexImportInput;
4034
+ if (input) {
4035
+ input.value = '';
4036
+ input.click();
4037
+ }
4038
+ },
4039
+
4040
+ handleClaudeImportChange(event) {
4041
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
4042
+ if (file) {
4043
+ void this.importBackupFile('claude', file);
4044
+ }
4045
+ },
4046
+
4047
+ handleCodexImportChange(event) {
4048
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
4049
+ if (file) {
4050
+ void this.importBackupFile('codex', file);
4051
+ }
4052
+ },
4053
+
4054
+ async importBackupFile(type, file) {
4055
+ const maxSize = 200 * 1024 * 1024;
4056
+ const loadingKey = type === 'claude' ? 'claudeImportLoading' : 'codexImportLoading';
4057
+ if (file.size > maxSize) {
4058
+ this.showMessage('备份文件过大,限制 200MB', 'error');
4059
+ this.resetImportInput(type);
4060
+ return;
4061
+ }
4062
+ this[loadingKey] = true;
4063
+ try {
4064
+ const base64 = await this.readFileAsBase64(file);
4065
+ const action = type === 'claude' ? 'restore-claude-dir' : 'restore-codex-dir';
4066
+ const res = await api(action, {
4067
+ fileName: file.name || `${type}-backup.zip`,
4068
+ fileBase64: base64
4069
+ });
4070
+ if (res && res.error) {
4071
+ this.showMessage(res.error, 'error');
4072
+ return;
4073
+ }
4074
+ const backupTip = res && res.backupPath ? `,原配置已备份到临时文件:${res.backupPath}` : '';
4075
+ this.showMessage(`导入成功${backupTip}`, 'success');
4076
+ if (type === 'claude') {
4077
+ await this.refreshClaudeSelectionFromSettings({ silent: true });
4078
+ } else {
4079
+ await this.loadAll();
4080
+ }
4081
+ } catch (e) {
4082
+ this.showMessage('导入失败:' + (e && e.message ? e.message : '未知错误'), 'error');
4083
+ } finally {
4084
+ this[loadingKey] = false;
4085
+ this.resetImportInput(type);
4086
+ }
4087
+ },
4088
+
4089
+ readFileAsBase64(file) {
4090
+ return new Promise((resolve, reject) => {
4091
+ const reader = new FileReader();
4092
+ reader.onload = () => {
4093
+ const result = reader.result;
4094
+ if (result instanceof ArrayBuffer) {
4095
+ resolve(this.arrayBufferToBase64(result));
4096
+ return;
4097
+ }
4098
+ if (typeof result === 'string') {
4099
+ const idx = result.indexOf('base64,');
4100
+ resolve(idx >= 0 ? result.slice(idx + 7) : result);
4101
+ return;
4102
+ }
4103
+ reject(new Error('不支持的文件读取结果'));
4104
+ };
4105
+ reader.onerror = () => reject(new Error('读取文件失败'));
4106
+ reader.readAsArrayBuffer(file);
4107
+ });
4108
+ },
4109
+
4110
+ arrayBufferToBase64(buffer) {
4111
+ const bytes = new Uint8Array(buffer);
4112
+ const chunkSize = 0x8000;
4113
+ let binary = '';
4114
+ for (let i = 0; i < bytes.byteLength; i += chunkSize) {
4115
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
4116
+ }
4117
+ return btoa(binary);
4118
+ },
4119
+
4120
+ resetImportInput(type) {
4121
+ const refName = type === 'claude' ? 'claudeImportInput' : 'codexImportInput';
4122
+ const el = this.$refs[refName];
4123
+ if (el) {
4124
+ el.value = '';
4125
+ }
4126
+ },
4127
+
4128
+ async loadCodexAuthProfiles(options = {}) {
4129
+ const silent = !!options.silent;
4130
+ try {
4131
+ const res = await api('list-auth-profiles');
4132
+ if (res && res.error) {
4133
+ if (!silent) {
4134
+ this.showMessage(res.error, 'error');
4135
+ }
4136
+ return;
4137
+ }
4138
+ const list = Array.isArray(res && res.profiles) ? res.profiles : [];
4139
+ this.codexAuthProfiles = list.sort((a, b) => {
4140
+ if (!!a.current !== !!b.current) {
4141
+ return a.current ? -1 : 1;
4142
+ }
4143
+ return String(a.name || '').localeCompare(String(b.name || ''));
4144
+ });
4145
+ } catch (e) {
4146
+ if (!silent) {
4147
+ this.showMessage('读取认证列表失败', 'error');
4148
+ }
4149
+ }
4150
+ },
4151
+
4152
+ triggerCodexAuthUpload() {
4153
+ const input = this.$refs.codexAuthImportInput;
4154
+ if (input) {
4155
+ input.value = '';
4156
+ input.click();
4157
+ }
4158
+ },
4159
+
4160
+ handleCodexAuthImportChange(event) {
4161
+ const file = event && event.target && event.target.files ? event.target.files[0] : null;
4162
+ if (file) {
4163
+ void this.importCodexAuthFile(file);
4164
+ }
4165
+ },
4166
+
4167
+ resetCodexAuthImportInput() {
4168
+ const el = this.$refs.codexAuthImportInput;
4169
+ if (el) {
4170
+ el.value = '';
4171
+ }
4172
+ },
4173
+
4174
+ async importCodexAuthFile(file) {
4175
+ this.codexAuthImportLoading = true;
4176
+ try {
4177
+ const base64 = await this.readFileAsBase64(file);
4178
+ const res = await api('import-auth-profile', {
4179
+ fileName: file.name || 'codex-auth.json',
4180
+ fileBase64: base64,
4181
+ activate: true
4182
+ });
4183
+ if (res && res.error) {
4184
+ this.showMessage(res.error, 'error');
4185
+ return;
4186
+ }
4187
+ await this.loadCodexAuthProfiles({ silent: true });
4188
+ this.showMessage('认证文件已导入并切换', 'success');
4189
+ } catch (e) {
4190
+ this.showMessage('导入认证文件失败', 'error');
4191
+ } finally {
4192
+ this.codexAuthImportLoading = false;
4193
+ this.resetCodexAuthImportInput();
4194
+ }
4195
+ },
4196
+
4197
+ async switchCodexAuthProfile(name) {
4198
+ const key = String(name || '').trim();
4199
+ if (!key || this.codexAuthSwitching[key]) return;
4200
+ this.codexAuthSwitching[key] = true;
4201
+ try {
4202
+ const res = await api('switch-auth-profile', { name: key });
4203
+ if (res && res.error) {
4204
+ this.showMessage(res.error, 'error');
4205
+ return;
4206
+ }
4207
+ await this.loadCodexAuthProfiles({ silent: true });
4208
+ this.showMessage(`已切换认证: ${key}`, 'success');
4209
+ } catch (e) {
4210
+ this.showMessage('切换认证失败', 'error');
4211
+ } finally {
4212
+ this.codexAuthSwitching[key] = false;
4213
+ }
4214
+ },
4215
+
4216
+ async deleteCodexAuthProfile(name) {
4217
+ const key = String(name || '').trim();
4218
+ if (!key || this.codexAuthDeleting[key]) return;
4219
+ this.codexAuthDeleting[key] = true;
4220
+ try {
4221
+ const res = await api('delete-auth-profile', { name: key });
4222
+ if (res && res.error) {
4223
+ this.showMessage(res.error, 'error');
4224
+ return;
4225
+ }
4226
+ await this.loadCodexAuthProfiles({ silent: true });
4227
+ const switchedTip = res && res.switchedTo ? `,已切换到 ${res.switchedTo}` : '';
4228
+ this.showMessage(`已删除认证${switchedTip}`, 'success');
4229
+ } catch (e) {
4230
+ this.showMessage('删除认证失败', 'error');
4231
+ } finally {
4232
+ this.codexAuthDeleting[key] = false;
4233
+ }
4234
+ },
4235
+
4236
+ mergeProxySettings(nextSettings) {
4237
+ const safe = nextSettings && typeof nextSettings === 'object' ? nextSettings : {};
4238
+ const port = parseInt(String(safe.port), 10);
4239
+ const timeoutMs = parseInt(String(safe.timeoutMs), 10);
4240
+ this.proxySettings = {
4241
+ enabled: safe.enabled !== false,
4242
+ host: typeof safe.host === 'string' && safe.host.trim() ? safe.host.trim() : '127.0.0.1',
4243
+ port: Number.isFinite(port) ? port : 8318,
4244
+ provider: typeof safe.provider === 'string' ? safe.provider.trim() : '',
4245
+ authSource: safe.authSource === 'profile' || safe.authSource === 'none' ? safe.authSource : 'provider',
4246
+ timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : 30000
4247
+ };
4248
+ },
4249
+
4250
+ async loadProxyStatus(options = {}) {
4251
+ const silent = !!options.silent;
4252
+ this.proxyLoading = true;
4253
+ try {
4254
+ const res = await api('proxy-status');
4255
+ if (res && res.error) {
4256
+ if (!silent) {
4257
+ this.showMessage(res.error, 'error');
4258
+ }
4259
+ return;
4260
+ }
4261
+ this.mergeProxySettings(res && res.settings ? res.settings : {});
4262
+ this.proxyRuntime = res && res.runtime ? { running: true, ...res.runtime } : null;
4263
+ } catch (e) {
4264
+ if (!silent) {
4265
+ this.showMessage('读取代理状态失败', 'error');
4266
+ }
4267
+ } finally {
4268
+ this.proxyLoading = false;
4269
+ }
4270
+ },
4271
+
4272
+ async saveProxySettings(options = {}) {
4273
+ const silent = !!options.silent;
4274
+ this.proxySaving = true;
4275
+ try {
4276
+ const res = await api('proxy-save-config', this.proxySettings);
4277
+ if (res && res.error) {
4278
+ if (!silent) {
4279
+ this.showMessage(res.error, 'error');
4280
+ }
4281
+ return;
4282
+ }
4283
+ if (res && res.settings) {
4284
+ this.mergeProxySettings(res.settings);
4285
+ }
4286
+ if (!silent) {
4287
+ this.showMessage('代理配置已保存', 'success');
4288
+ }
4289
+ } catch (e) {
4290
+ if (!silent) {
4291
+ this.showMessage('保存代理配置失败', 'error');
4292
+ }
4293
+ } finally {
4294
+ this.proxySaving = false;
4295
+ }
4296
+ },
4297
+
4298
+ async startBuiltinProxy() {
4299
+ this.proxyStarting = true;
4300
+ try {
4301
+ const res = await api('proxy-start', {
4302
+ ...this.proxySettings,
4303
+ enabled: true
4304
+ });
4305
+ if (res && res.error) {
4306
+ this.showMessage(res.error, 'error');
4307
+ return;
4308
+ }
4309
+ if (res && res.settings) {
4310
+ this.mergeProxySettings(res.settings);
4311
+ }
4312
+ await this.loadProxyStatus({ silent: true });
4313
+ const listenTip = res && res.listenUrl ? `:${res.listenUrl}` : '';
4314
+ this.showMessage(`代理已启动${listenTip}`, 'success');
4315
+ } catch (e) {
4316
+ this.showMessage('启动代理失败', 'error');
4317
+ } finally {
4318
+ this.proxyStarting = false;
4319
+ }
4320
+ },
4321
+
4322
+ async stopBuiltinProxy() {
4323
+ this.proxyStopping = true;
4324
+ try {
4325
+ const res = await api('proxy-stop');
4326
+ if (res && res.error) {
4327
+ this.showMessage(res.error, 'error');
4328
+ return;
4329
+ }
4330
+ await this.loadProxyStatus({ silent: true });
4331
+ this.showMessage('代理已停止', 'success');
4332
+ } catch (e) {
4333
+ this.showMessage('停止代理失败', 'error');
4334
+ } finally {
4335
+ this.proxyStopping = false;
4336
+ }
4337
+ },
4338
+
4339
+ async applyBuiltinProxyProvider() {
4340
+ this.proxyApplying = true;
4341
+ try {
4342
+ const saveRes = await api('proxy-save-config', this.proxySettings);
4343
+ if (saveRes && saveRes.error) {
4344
+ this.showMessage(saveRes.error, 'error');
4345
+ return;
4346
+ }
4347
+ const res = await api('proxy-apply-provider', { switchToProxy: true });
4348
+ if (res && res.error) {
4349
+ this.showMessage(res.error, 'error');
4350
+ return;
4351
+ }
4352
+ await this.loadAll();
4353
+ this.showMessage('本地代理 provider 已写入并切换', 'success');
4354
+ } catch (e) {
4355
+ this.showMessage('应用代理 provider 失败', 'error');
4356
+ } finally {
4357
+ this.proxyApplying = false;
4358
+ }
4359
+ },
4360
+
2958
4361
  showMessage(text, type) {
2959
4362
  this.message = text;
2960
4363
  this.messageType = type || 'info';