codexmate 0.0.4 → 0.0.5

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.html CHANGED
@@ -523,6 +523,101 @@
523
523
  gap: var(--spacing-xs);
524
524
  }
525
525
 
526
+ .recent-list {
527
+ display: flex;
528
+ flex-wrap: wrap;
529
+ gap: 8px;
530
+ margin-top: 8px;
531
+ }
532
+
533
+ .recent-item {
534
+ border: 1px solid var(--color-border-soft);
535
+ background: var(--color-surface-elevated);
536
+ border-radius: var(--radius-md);
537
+ padding: 10px 12px;
538
+ min-width: 170px;
539
+ text-align: left;
540
+ cursor: pointer;
541
+ transition: all var(--transition-fast) var(--ease-spring);
542
+ box-shadow: 0 2px 6px rgba(27, 23, 20, 0.06);
543
+ }
544
+
545
+ .recent-item:hover {
546
+ transform: translateY(-1px);
547
+ box-shadow: 0 4px 12px rgba(27, 23, 20, 0.12);
548
+ border-color: var(--color-border);
549
+ }
550
+
551
+ .recent-item:disabled {
552
+ opacity: 0.6;
553
+ cursor: not-allowed;
554
+ }
555
+
556
+ .recent-provider {
557
+ font-size: var(--font-size-body);
558
+ font-weight: var(--font-weight-secondary);
559
+ color: var(--color-text-primary);
560
+ margin-bottom: 4px;
561
+ }
562
+
563
+ .recent-model {
564
+ font-size: var(--font-size-caption);
565
+ color: var(--color-text-tertiary);
566
+ line-height: 1.4;
567
+ }
568
+
569
+ .recent-empty {
570
+ font-size: var(--font-size-caption);
571
+ color: var(--color-text-tertiary);
572
+ }
573
+
574
+ .health-report {
575
+ margin-top: 10px;
576
+ padding: 10px 12px;
577
+ border-radius: var(--radius-md);
578
+ border: 1px solid var(--color-border-soft);
579
+ background: var(--color-surface-alt);
580
+ display: grid;
581
+ gap: 8px;
582
+ }
583
+
584
+ .health-remote-toggle {
585
+ display: inline-flex;
586
+ align-items: center;
587
+ gap: 8px;
588
+ font-size: var(--font-size-caption);
589
+ color: var(--color-text-secondary);
590
+ }
591
+
592
+ .health-remote-toggle input {
593
+ accent-color: var(--color-brand);
594
+ }
595
+
596
+ .health-ok {
597
+ color: var(--color-success);
598
+ font-weight: var(--font-weight-secondary);
599
+ }
600
+
601
+ .health-issue {
602
+ background: #fff6f5;
603
+ border-left: 3px solid var(--color-error);
604
+ padding: 8px 10px;
605
+ border-radius: 10px;
606
+ }
607
+
608
+ .health-issue-title {
609
+ font-size: var(--font-size-caption);
610
+ font-weight: var(--font-weight-secondary);
611
+ color: var(--color-text-primary);
612
+ margin-bottom: 4px;
613
+ }
614
+
615
+ .health-issue-suggestion {
616
+ font-size: var(--font-size-caption);
617
+ color: var(--color-text-secondary);
618
+ line-height: 1.4;
619
+ }
620
+
526
621
  .btn-icon {
527
622
  width: 28px;
528
623
  height: 28px;
@@ -580,6 +675,31 @@
580
675
  box-shadow: var(--shadow-input-focus);
581
676
  }
582
677
 
678
+ .model-input {
679
+ width: 100%;
680
+ padding: 12px var(--spacing-sm);
681
+ border: 1px solid var(--color-border-soft);
682
+ border-radius: var(--radius-sm);
683
+ font-size: var(--font-size-body);
684
+ font-weight: var(--font-weight-body);
685
+ background-color: var(--color-surface-alt);
686
+ color: var(--color-text-primary);
687
+ outline: none;
688
+ transition: all var(--transition-fast) var(--ease-smooth);
689
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
690
+ }
691
+
692
+ .model-input:hover {
693
+ border-color: var(--color-border-strong);
694
+ background-color: var(--color-surface);
695
+ }
696
+
697
+ .model-input:focus {
698
+ background-color: var(--color-surface);
699
+ border-color: var(--color-brand);
700
+ box-shadow: var(--shadow-input-focus);
701
+ }
702
+
583
703
  .config-template-hint {
584
704
  margin-top: 8px;
585
705
  margin-bottom: 10px;
@@ -1280,6 +1400,37 @@
1280
1400
  }
1281
1401
  }
1282
1402
 
1403
+ @media (max-width: 520px) {
1404
+ .session-item-header {
1405
+ flex-direction: column;
1406
+ align-items: stretch;
1407
+ }
1408
+
1409
+ .session-item-actions {
1410
+ justify-content: flex-end;
1411
+ }
1412
+
1413
+ .session-actions {
1414
+ width: 100%;
1415
+ flex-direction: column;
1416
+ align-items: stretch;
1417
+ }
1418
+
1419
+ .btn-session-refresh,
1420
+ .btn-session-export {
1421
+ width: 100%;
1422
+ }
1423
+
1424
+ .session-toolbar-group.session-toolbar-actions {
1425
+ flex-direction: column;
1426
+ align-items: stretch;
1427
+ }
1428
+
1429
+ .session-toolbar-group.session-toolbar-actions .btn-tool {
1430
+ width: 100%;
1431
+ }
1432
+ }
1433
+
1283
1434
  .btn[disabled] {
1284
1435
  opacity: 0.5;
1285
1436
  cursor: not-allowed;
@@ -1666,23 +1817,23 @@
1666
1817
  Codex<br>
1667
1818
  <span class="accent">Mate.</span>
1668
1819
  </h1>
1669
- <p class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
1820
+ <p class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
1670
1821
 
1671
1822
  <!-- 模式切换器 -->
1672
- <div class="segmented-control">
1673
- <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
1674
- Codex 配置
1675
- </button>
1676
- <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
1677
- Claude Code 配置
1678
- </button>
1679
- <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
1680
- OpenClaw 配置
1681
- </button>
1682
- <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
1683
- 会话浏览
1684
- </button>
1685
- </div>
1823
+ <div class="segmented-control">
1824
+ <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
1825
+ Codex 配置
1826
+ </button>
1827
+ <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
1828
+ Claude Code 配置
1829
+ </button>
1830
+ <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
1831
+ OpenClaw 配置
1832
+ </button>
1833
+ <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
1834
+ 会话浏览
1835
+ </button>
1836
+ </div>
1686
1837
 
1687
1838
  <!-- 内容包裹器 - 稳定布局 -->
1688
1839
  <div class="content-wrapper">
@@ -1701,33 +1852,107 @@
1701
1852
  <div class="selector-header">
1702
1853
  <span class="selector-title">模型</span>
1703
1854
  <div class="selector-actions">
1704
- <button class="btn-icon" @click="showModelModal = true" title="添加模型">+</button>
1705
- <button class="btn-icon" @click="showModelListModal = true" title="管理模型">≡</button>
1855
+ <button class="btn-icon" @click="showModelModal = true" title="添加模型" v-if="modelsSource === 'legacy'">+</button>
1856
+ <button class="btn-icon" @click="showModelListModal = true" title="管理模型" v-if="modelsSource === 'legacy'">≡</button>
1706
1857
  </div>
1707
1858
  </div>
1708
- <select class="model-select" v-model="currentModel" @change="onModelChange">
1709
- <option v-for="model in models" :key="model" :value="model">{{ model }}</option>
1859
+ <select
1860
+ v-if="codexModelsLoading || modelsSource === 'remote'"
1861
+ class="model-select"
1862
+ v-model="currentModel"
1863
+ @change="onModelChange"
1864
+ :disabled="codexModelsLoading"
1865
+ >
1866
+ <option v-if="codexModelsLoading" value="">加载中...</option>
1867
+ <option v-else v-for="model in models" :key="model" :value="model">{{ model }}</option>
1710
1868
  </select>
1869
+ <input
1870
+ v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
1871
+ class="model-input"
1872
+ v-model="currentModel"
1873
+ @blur="onModelChange"
1874
+ placeholder="例如: gpt-5.3-codex"
1875
+ >
1876
+ <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
1877
+ 当前提供商未提供模型列表,视为不限;模型可手动输入。
1878
+ </div>
1879
+ <div class="config-template-hint" v-if="modelsSource === 'error'">
1880
+ 模型列表获取失败,请检查接口或手动输入。
1881
+ </div>
1882
+ <div class="config-template-hint" v-if="modelsSource === 'remote' && !modelsHasCurrent">
1883
+ 当前模型不在接口列表中,请手动输入或在模板中调整。
1884
+ </div>
1711
1885
  <div class="config-template-hint">
1712
- Codex 配置改动采用模板确认模式:请先编辑模板,再手动确认应用。
1886
+ Codex 配置需先改模板,再手动应用。
1713
1887
  </div>
1714
1888
  <button class="btn-tool btn-template-editor" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
1715
1889
  打开 Config 模板编辑器
1716
1890
  </button>
1717
1891
  </div>
1718
1892
 
1893
+ <div class="selector-section">
1894
+ <div class="selector-header">
1895
+ <span class="selector-title">最近使用</span>
1896
+ <span v-if="recentLoading" class="selector-title">加载中...</span>
1897
+ </div>
1898
+ <div v-if="recentConfigs.length === 0" class="recent-empty">
1899
+ 暂无记录
1900
+ </div>
1901
+ <div v-else class="recent-list">
1902
+ <button
1903
+ v-for="item in recentConfigs"
1904
+ :key="item.provider + '::' + item.model + '::' + (item.usedAt || '')"
1905
+ class="recent-item"
1906
+ @click="applyRecentConfig(item)"
1907
+ :disabled="loading || !!initError">
1908
+ <div class="recent-provider">{{ item.provider }}</div>
1909
+ <div class="recent-model">{{ item.model }}</div>
1910
+ </button>
1911
+ </div>
1912
+ </div>
1913
+
1719
1914
  <div class="selector-section">
1720
1915
  <div class="selector-header">
1721
1916
  <span class="selector-title">AGENTS.md</span>
1722
1917
  </div>
1723
1918
  <div class="config-template-hint">
1724
- 管理 Codex 指令文件,默认读写 <code>~/.codex/AGENTS.md</code>(与 <code>config.toml</code> 同级)。
1919
+ Codex 指令:<code>~/.codex/AGENTS.md</code>(同级 <code>config.toml</code>)。
1725
1920
  </div>
1726
1921
  <button class="btn-tool" @click="openAgentsEditor" :disabled="loading || !!initError || agentsLoading">
1727
1922
  {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
1728
1923
  </button>
1729
1924
  </div>
1730
1925
 
1926
+ <div class="selector-section">
1927
+ <div class="selector-header">
1928
+ <span class="selector-title">配置健康检查</span>
1929
+ </div>
1930
+ <div class="config-template-hint">
1931
+ 检测 base_url、API Key 与模型可用性(可选远程探测会发起真实请求,可能产生费用)。
1932
+ </div>
1933
+ <div class="config-template-hint">
1934
+ <label class="health-remote-toggle">
1935
+ <input type="checkbox" v-model="healthCheckRemote">
1936
+ 启用远程探测(请求 base_url 与 /v1/models)
1937
+ </label>
1938
+ </div>
1939
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
1940
+ {{ healthCheckLoading ? '检查中...' : '运行检查' }}
1941
+ </button>
1942
+ <div v-if="healthCheckResult" class="health-report">
1943
+ <div v-if="healthCheckResult.ok" class="health-ok">未发现问题</div>
1944
+ <div v-else>
1945
+ <div
1946
+ v-for="(issue, index) in healthCheckResult.issues"
1947
+ :key="(issue.code || 'issue') + index"
1948
+ class="health-issue">
1949
+ <div class="health-issue-title">{{ issue.message }}</div>
1950
+ <div v-if="issue.suggestion" class="health-issue-suggestion">{{ issue.suggestion }}</div>
1951
+ </div>
1952
+ </div>
1953
+ </div>
1954
+ </div>
1955
+
1731
1956
  <div v-if="!loading && !initError" class="card-list">
1732
1957
  <div v-for="provider in providersList" :key="provider.name"
1733
1958
  :class="['card', { active: currentProvider === provider.name }]"
@@ -1770,17 +1995,33 @@
1770
1995
  </div>
1771
1996
  </div>
1772
1997
 
1773
- <!-- Claude Code 配置模式 -->
1774
- <div v-show="configMode === 'claude'" class="mode-content">
1775
- <!-- 添加提供商按钮 -->
1776
- <button class="btn-add" @click="showClaudeConfigModal = true" v-if="!loading && !initError">
1998
+ <!-- Claude Code 配置模式 -->
1999
+ <div v-show="configMode === 'claude'" class="mode-content">
2000
+ <!-- 添加提供商按钮 -->
2001
+ <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
1777
2002
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1778
2003
  <path d="M10 4v12M4 10h12"/>
1779
2004
  </svg>
1780
2005
  添加提供商
1781
2006
  </button>
1782
2007
  <div class="config-template-hint">
1783
- 默认应用到 <code>~/.claude/settings.json</code>;编辑弹窗中可使用兼容模式写入系统环境变量。
2008
+ 默认应用到 <code>~/.claude/settings.json</code>。
2009
+ </div>
2010
+
2011
+ <div class="selector-section">
2012
+ <div class="selector-header">
2013
+ <span class="selector-title">模型</span>
2014
+ </div>
2015
+ <input
2016
+ class="model-input"
2017
+ v-model="currentClaudeModel"
2018
+ @blur="onClaudeModelChange"
2019
+ @keyup.enter="onClaudeModelChange"
2020
+ placeholder="例如: claude-3-7-sonnet"
2021
+ >
2022
+ <div class="config-template-hint">
2023
+ 模型修改后会自动保存并应用到当前配置。
2024
+ </div>
1784
2025
  </div>
1785
2026
 
1786
2027
  <div class="card-list">
@@ -1814,69 +2055,69 @@
1814
2055
  </div>
1815
2056
  </div>
1816
2057
  </div>
1817
- </div>
1818
- </div>
1819
-
1820
- <!-- OpenClaw 配置模式 -->
1821
- <div v-show="configMode === 'openclaw'" class="mode-content">
1822
- <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
1823
- <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1824
- <path d="M10 4v12M4 10h12"/>
1825
- </svg>
1826
- 添加 OpenClaw 配置
1827
- </button>
1828
- <div class="config-template-hint">
1829
- 默认应用到 <code>~/.openclaw/openclaw.json</code>;支持 JSON5(注释/尾逗号)。
1830
- </div>
1831
-
1832
- <div class="selector-section">
1833
- <div class="selector-header">
1834
- <span class="selector-title">AGENTS.md</span>
1835
- </div>
1836
- <div class="config-template-hint">
1837
- 管理 OpenClaw Workspace 指令文件,默认读写 <code>~/.openclaw/workspace/AGENTS.md</code>。
1838
- </div>
1839
- <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
1840
- {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
1841
- </button>
1842
- </div>
1843
-
1844
- <div class="card-list">
1845
- <div v-for="(config, name) in openclawConfigs" :key="name"
1846
- :class="['card', { active: currentOpenclawConfig === name }]"
1847
- @click="applyOpenclawConfig(name)">
1848
- <div class="card-leading">
1849
- <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
1850
- <div class="card-content">
1851
- <div class="card-title">{{ name }}</div>
1852
- <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
1853
- </div>
1854
- </div>
1855
- <div class="card-trailing">
1856
- <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
1857
- {{ openclawHasContent(config) ? '已配置' : '未配置' }}
1858
- </span>
1859
- <div class="card-actions" @click.stop>
1860
- <button class="card-action-btn" @click="openOpenclawEditModal(name)" title="编辑">
1861
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1862
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
1863
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
1864
- </svg>
1865
- </button>
1866
- <button class="card-action-btn delete" @click="deleteOpenclawConfig(name)" title="删除">
1867
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1868
- <path d="M3 6h18"/>
1869
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
1870
- </svg>
1871
- </button>
1872
- </div>
1873
- </div>
1874
- </div>
1875
- </div>
1876
- </div>
1877
-
1878
- <!-- 会话浏览模式 -->
1879
- <div v-show="configMode === 'sessions'" class="mode-content">
2058
+ </div>
2059
+ </div>
2060
+
2061
+ <!-- OpenClaw 配置模式 -->
2062
+ <div v-show="configMode === 'openclaw'" class="mode-content">
2063
+ <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
2064
+ <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
2065
+ <path d="M10 4v12M4 10h12"/>
2066
+ </svg>
2067
+ 添加 OpenClaw 配置
2068
+ </button>
2069
+ <div class="config-template-hint">
2070
+ 默认应用到 <code>~/.openclaw/openclaw.json</code>;支持 JSON5(注释/尾逗号)。
2071
+ </div>
2072
+
2073
+ <div class="selector-section">
2074
+ <div class="selector-header">
2075
+ <span class="selector-title">AGENTS.md</span>
2076
+ </div>
2077
+ <div class="config-template-hint">
2078
+ 管理 OpenClaw Workspace 指令文件,默认读写 <code>~/.openclaw/workspace/AGENTS.md</code>。
2079
+ </div>
2080
+ <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
2081
+ {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
2082
+ </button>
2083
+ </div>
2084
+
2085
+ <div class="card-list">
2086
+ <div v-for="(config, name) in openclawConfigs" :key="name"
2087
+ :class="['card', { active: currentOpenclawConfig === name }]"
2088
+ @click="applyOpenclawConfig(name)">
2089
+ <div class="card-leading">
2090
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
2091
+ <div class="card-content">
2092
+ <div class="card-title">{{ name }}</div>
2093
+ <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
2094
+ </div>
2095
+ </div>
2096
+ <div class="card-trailing">
2097
+ <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
2098
+ {{ openclawHasContent(config) ? '已配置' : '未配置' }}
2099
+ </span>
2100
+ <div class="card-actions" @click.stop>
2101
+ <button class="card-action-btn" @click="openOpenclawEditModal(name)" title="编辑">
2102
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2103
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
2104
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
2105
+ </svg>
2106
+ </button>
2107
+ <button class="card-action-btn delete" @click="deleteOpenclawConfig(name)" title="删除">
2108
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2109
+ <path d="M3 6h18"/>
2110
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
2111
+ </svg>
2112
+ </button>
2113
+ </div>
2114
+ </div>
2115
+ </div>
2116
+ </div>
2117
+ </div>
2118
+
2119
+ <!-- 会话浏览模式 -->
2120
+ <div v-show="configMode === 'sessions'" class="mode-content">
1880
2121
  <div class="selector-section">
1881
2122
  <div class="selector-header">
1882
2123
  <span class="selector-title">会话来源</span>
@@ -1937,7 +2178,8 @@
1937
2178
  </div>
1938
2179
  </div>
1939
2180
  <div class="session-hint">
1940
- 关键词检索与角色/时间筛选暂时停用;当前仅支持来源与路径筛选会话列表。右侧预览区仅支持查看与导出。
2181
+ 关键词/角色/时间筛选暂不可用;<br>
2182
+ 仅支持来源与路径筛选,右侧仅查看/导出。
1941
2183
  </div>
1942
2184
  </div>
1943
2185
 
@@ -2172,10 +2414,6 @@
2172
2414
  <label class="form-label">Base URL</label>
2173
2415
  <input v-model="newClaudeConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2174
2416
  </div>
2175
- <div class="form-group">
2176
- <label class="form-label">模型</label>
2177
- <input v-model="newClaudeConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
2178
- </div>
2179
2417
 
2180
2418
  <div class="btn-group">
2181
2419
  <button class="btn btn-cancel" @click="closeClaudeConfigModal">取消</button>
@@ -2185,9 +2423,9 @@
2185
2423
  </div>
2186
2424
 
2187
2425
  <!-- 编辑Claude配置模态框 -->
2188
- <div v-if="showEditConfigModal" class="modal-overlay" @click.self="closeEditConfigModal">
2189
- <div class="modal">
2190
- <div class="modal-title">编辑 Claude Code 配置</div>
2426
+ <div v-if="showEditConfigModal" class="modal-overlay" @click.self="closeEditConfigModal">
2427
+ <div class="modal">
2428
+ <div class="modal-title">编辑 Claude Code 配置</div>
2191
2429
 
2192
2430
  <div class="form-group">
2193
2431
  <label class="form-label">配置名称</label>
@@ -2201,71 +2439,65 @@
2201
2439
  <label class="form-label">Base URL</label>
2202
2440
  <input v-model="editingConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2203
2441
  </div>
2442
+
2443
+ <div class="btn-group">
2444
+ <button class="btn btn-cancel" @click="closeEditConfigModal">取消</button>
2445
+ <button class="btn btn-confirm" @click="saveAndApplyConfig">保存并应用</button>
2446
+ </div>
2447
+ </div>
2448
+ </div>
2449
+
2450
+ <div v-if="showOpenclawConfigModal" class="modal-overlay" @click.self="closeOpenclawConfigModal">
2451
+ <div class="modal modal-wide">
2452
+ <div class="modal-title">{{ openclawEditorTitle }}</div>
2453
+
2204
2454
  <div class="form-group">
2205
- <label class="form-label">模型</label>
2206
- <input v-model="editingConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
2455
+ <label class="form-label">配置名称</label>
2456
+ <input v-model="openclawEditing.name" class="form-input" :readonly="openclawEditing.lockName" placeholder="例如: 默认配置">
2457
+ </div>
2458
+
2459
+ <div class="form-group">
2460
+ <label class="form-label">目标文件</label>
2461
+ <div class="form-hint">
2462
+ {{ openclawConfigPath || '未加载' }}
2463
+ <span v-if="openclawConfigPath">
2464
+ ({{ openclawConfigExists ? '已存在' : '不存在,将在应用时创建' }})
2465
+ </span>
2466
+ </div>
2467
+ <div class="btn-group" style="justify-content:flex-start;">
2468
+ <button class="btn btn-confirm secondary" @click="loadOpenclawConfigFromFile" :disabled="openclawFileLoading">
2469
+ {{ openclawFileLoading ? '加载中...' : '加载当前配置' }}
2470
+ </button>
2471
+ </div>
2472
+ </div>
2473
+
2474
+ <div class="form-group">
2475
+ <label class="form-label">OpenClaw 配置(JSON5)</label>
2476
+ <textarea
2477
+ v-model="openclawEditing.content"
2478
+ class="form-input template-editor"
2479
+ spellcheck="false"
2480
+ placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
2481
+ <div class="template-editor-warning">
2482
+ 保存仅写入本地配置库;点击“保存并应用”后会写入 openclaw.json。
2483
+ </div>
2207
2484
  </div>
2208
2485
 
2209
2486
  <div class="btn-group">
2210
- <button class="btn btn-cancel" @click="closeEditConfigModal">取消</button>
2211
- <button class="btn btn-confirm" @click="updateConfig">保存</button>
2212
- <button class="btn btn-confirm secondary" @click="saveAndApplyConfig">保存并应用到 Claude 配置</button>
2213
- <button class="btn btn-confirm secondary" @click="saveAndApplyEnvCompat">兼容模式应用到环境变量</button>
2214
- </div>
2215
- </div>
2216
- </div>
2217
-
2218
- <div v-if="showOpenclawConfigModal" class="modal-overlay" @click.self="closeOpenclawConfigModal">
2219
- <div class="modal modal-wide">
2220
- <div class="modal-title">{{ openclawEditorTitle }}</div>
2221
-
2222
- <div class="form-group">
2223
- <label class="form-label">配置名称</label>
2224
- <input v-model="openclawEditing.name" class="form-input" :readonly="openclawEditing.lockName" placeholder="例如: 默认配置">
2225
- </div>
2226
-
2227
- <div class="form-group">
2228
- <label class="form-label">目标文件</label>
2229
- <div class="form-hint">
2230
- {{ openclawConfigPath || '未加载' }}
2231
- <span v-if="openclawConfigPath">
2232
- ({{ openclawConfigExists ? '已存在' : '不存在,将在应用时创建' }})
2233
- </span>
2234
- </div>
2235
- <div class="btn-group" style="justify-content:flex-start;">
2236
- <button class="btn btn-confirm secondary" @click="loadOpenclawConfigFromFile" :disabled="openclawFileLoading">
2237
- {{ openclawFileLoading ? '加载中...' : '加载当前配置' }}
2238
- </button>
2239
- </div>
2240
- </div>
2241
-
2242
- <div class="form-group">
2243
- <label class="form-label">OpenClaw 配置(JSON5)</label>
2244
- <textarea
2245
- v-model="openclawEditing.content"
2246
- class="form-input template-editor"
2247
- spellcheck="false"
2248
- placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
2249
- <div class="template-editor-warning">
2250
- 保存仅写入本地配置库;点击“保存并应用”后会写入 openclaw.json。
2251
- </div>
2252
- </div>
2253
-
2254
- <div class="btn-group">
2255
- <button class="btn btn-cancel" @click="closeOpenclawConfigModal">取消</button>
2256
- <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving">
2257
- {{ openclawSaving ? '保存中...' : '保存' }}
2258
- </button>
2259
- <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawApplying">
2260
- {{ openclawApplying ? '应用中...' : '保存并应用' }}
2261
- </button>
2262
- </div>
2263
- </div>
2264
- </div>
2265
-
2266
- <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="closeConfigTemplateModal">
2267
- <div class="modal modal-wide">
2268
- <div class="modal-title">Config 模板编辑器(手动确认应用)</div>
2487
+ <button class="btn btn-cancel" @click="closeOpenclawConfigModal">取消</button>
2488
+ <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving">
2489
+ {{ openclawSaving ? '保存中...' : '保存' }}
2490
+ </button>
2491
+ <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawApplying">
2492
+ {{ openclawApplying ? '应用中...' : '保存并应用' }}
2493
+ </button>
2494
+ </div>
2495
+ </div>
2496
+ </div>
2497
+
2498
+ <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="closeConfigTemplateModal">
2499
+ <div class="modal modal-wide">
2500
+ <div class="modal-title">Config 模板编辑器(手动确认应用)</div>
2269
2501
 
2270
2502
  <div class="form-group">
2271
2503
  <label class="form-label">config.toml 模板</label>
@@ -2288,9 +2520,9 @@
2288
2520
  </div>
2289
2521
  </div>
2290
2522
 
2291
- <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
2292
- <div class="modal modal-wide">
2293
- <div class="modal-title">{{ agentsModalTitle }}</div>
2523
+ <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
2524
+ <div class="modal modal-wide">
2525
+ <div class="modal-title">{{ agentsModalTitle }}</div>
2294
2526
 
2295
2527
  <div class="form-group">
2296
2528
  <label class="form-label">目标文件</label>
@@ -2304,16 +2536,16 @@
2304
2536
 
2305
2537
  <div class="form-group">
2306
2538
  <label class="form-label">AGENTS.md 内容</label>
2307
- <textarea
2308
- v-model="agentsContent"
2309
- class="form-input template-editor"
2310
- spellcheck="false"
2311
- :readonly="agentsLoading"
2312
- placeholder="在这里编辑 AGENTS.md 内容"></textarea>
2313
- <div class="template-editor-warning">
2314
- {{ agentsModalHint }}
2315
- </div>
2316
- </div>
2539
+ <textarea
2540
+ v-model="agentsContent"
2541
+ class="form-input template-editor"
2542
+ spellcheck="false"
2543
+ :readonly="agentsLoading"
2544
+ placeholder="在这里编辑 AGENTS.md 内容"></textarea>
2545
+ <div class="template-editor-warning">
2546
+ {{ agentsModalHint }}
2547
+ </div>
2548
+ </div>
2317
2549
 
2318
2550
  <div class="btn-group">
2319
2551
  <button class="btn btn-cancel" @click="closeAgentsModal">取消</button>
@@ -2330,22 +2562,22 @@
2330
2562
  <div v-if="message" :class="['toast', messageType]">{{ message }}</div>
2331
2563
  </div>
2332
2564
 
2333
- <script>
2334
- const { createApp } = Vue;
2335
- const API_BASE = 'http://localhost:3737';
2336
- const DEFAULT_OPENCLAW_TEMPLATE = `{
2337
- // OpenClaw config (JSON5)
2338
- agent: {
2339
- model: "gpt-4.1"
2340
- },
2341
- agents: {
2342
- defaults: {
2343
- workspace: "~/.openclaw/workspace"
2344
- }
2345
- }
2346
- }`;
2347
-
2348
- async function api(action, params = {}) {
2565
+ <script>
2566
+ const { createApp } = Vue;
2567
+ const API_BASE = 'http://localhost:3737';
2568
+ const DEFAULT_OPENCLAW_TEMPLATE = `{
2569
+ // OpenClaw config (JSON5)
2570
+ agent: {
2571
+ model: "gpt-4.1"
2572
+ },
2573
+ agents: {
2574
+ defaults: {
2575
+ workspace: "~/.openclaw/workspace"
2576
+ }
2577
+ }
2578
+ }`;
2579
+
2580
+ async function api(action, params = {}) {
2349
2581
  const res = await fetch(`${API_BASE}/api`, {
2350
2582
  method: 'POST',
2351
2583
  headers: { 'Content-Type': 'application/json' },
@@ -2362,6 +2594,13 @@
2362
2594
  currentModel: '',
2363
2595
  providersList: [],
2364
2596
  models: [],
2597
+ codexModelsLoading: false,
2598
+ modelsSource: 'remote',
2599
+ modelsHasCurrent: true,
2600
+ claudeModels: [],
2601
+ claudeModelsSource: 'idle',
2602
+ claudeModelsHasCurrent: true,
2603
+ claudeModelsLoading: false,
2365
2604
  loading: true,
2366
2605
  initError: '',
2367
2606
  message: '',
@@ -2370,23 +2609,23 @@
2370
2609
  showEditModal: false,
2371
2610
  showModelModal: false,
2372
2611
  showModelListModal: false,
2373
- showClaudeConfigModal: false,
2374
- showEditConfigModal: false,
2375
- showOpenclawConfigModal: false,
2376
- showConfigTemplateModal: false,
2377
- showAgentsModal: false,
2378
- configTemplateContent: '',
2379
- configTemplateApplying: false,
2380
- agentsContent: '',
2381
- agentsPath: '',
2382
- agentsExists: false,
2383
- agentsLineEnding: '\n',
2384
- agentsLoading: false,
2385
- agentsSaving: false,
2386
- agentsContext: 'codex',
2387
- agentsModalTitle: 'AGENTS.md 编辑器',
2388
- agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
2389
- sessionsList: [],
2612
+ showClaudeConfigModal: false,
2613
+ showEditConfigModal: false,
2614
+ showOpenclawConfigModal: false,
2615
+ showConfigTemplateModal: false,
2616
+ showAgentsModal: false,
2617
+ configTemplateContent: '',
2618
+ configTemplateApplying: false,
2619
+ agentsContent: '',
2620
+ agentsPath: '',
2621
+ agentsExists: false,
2622
+ agentsLineEnding: '\n',
2623
+ agentsLoading: false,
2624
+ agentsSaving: false,
2625
+ agentsContext: 'codex',
2626
+ agentsModalTitle: 'AGENTS.md 编辑器',
2627
+ agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
2628
+ sessionsList: [],
2390
2629
  sessionsLoading: false,
2391
2630
  sessionFilterSource: 'all',
2392
2631
  sessionPathFilter: '',
@@ -2417,10 +2656,11 @@
2417
2656
  speedLoading: {},
2418
2657
  newProvider: { name: '', url: '', key: '' },
2419
2658
  editingProvider: { name: '', url: '', key: '' },
2420
- newModelName: '',
2421
- currentClaudeConfig: '',
2422
- editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
2423
- claudeConfigs: {
2659
+ newModelName: '',
2660
+ currentClaudeConfig: '',
2661
+ currentClaudeModel: '',
2662
+ editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
2663
+ claudeConfigs: {
2424
2664
  '智谱GLM': {
2425
2665
  apiKey: '',
2426
2666
  baseUrl: 'https://open.bigmodel.cn/api/anthropic',
@@ -2428,33 +2668,38 @@
2428
2668
  hasKey: false
2429
2669
  }
2430
2670
  },
2431
- newClaudeConfig: {
2432
- name: '',
2433
- apiKey: '',
2434
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
2435
- model: 'glm-4.7'
2436
- },
2437
- currentOpenclawConfig: '',
2438
- openclawConfigs: {
2439
- '默认配置': {
2440
- content: DEFAULT_OPENCLAW_TEMPLATE
2441
- }
2442
- },
2443
- openclawEditing: { name: '', content: '', lockName: false },
2444
- openclawEditorTitle: '添加 OpenClaw 配置',
2445
- openclawConfigPath: '',
2446
- openclawConfigExists: false,
2447
- openclawLineEnding: '\n',
2448
- openclawFileLoading: false,
2449
- openclawSaving: false,
2450
- openclawApplying: false
2451
- }
2452
- },
2453
- mounted() {
2454
- const savedConfigs = localStorage.getItem('claudeConfigs');
2455
- if (savedConfigs) {
2456
- try {
2457
- this.claudeConfigs = JSON.parse(savedConfigs);
2671
+ newClaudeConfig: {
2672
+ name: '',
2673
+ apiKey: '',
2674
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
2675
+ model: 'glm-4.7'
2676
+ },
2677
+ currentOpenclawConfig: '',
2678
+ openclawConfigs: {
2679
+ '默认配置': {
2680
+ content: DEFAULT_OPENCLAW_TEMPLATE
2681
+ }
2682
+ },
2683
+ openclawEditing: { name: '', content: '', lockName: false },
2684
+ openclawEditorTitle: '添加 OpenClaw 配置',
2685
+ openclawConfigPath: '',
2686
+ openclawConfigExists: false,
2687
+ openclawLineEnding: '\n',
2688
+ openclawFileLoading: false,
2689
+ openclawSaving: false,
2690
+ openclawApplying: false,
2691
+ recentConfigs: [],
2692
+ recentLoading: false,
2693
+ healthCheckLoading: false,
2694
+ healthCheckResult: null,
2695
+ healthCheckRemote: false
2696
+ }
2697
+ },
2698
+ mounted() {
2699
+ const savedConfigs = localStorage.getItem('claudeConfigs');
2700
+ if (savedConfigs) {
2701
+ try {
2702
+ this.claudeConfigs = JSON.parse(savedConfigs);
2458
2703
  for (const [name, config] of Object.entries(this.claudeConfigs)) {
2459
2704
  if (config.apiKey && config.apiKey.includes('****')) {
2460
2705
  config.apiKey = '';
@@ -2469,37 +2714,41 @@
2469
2714
  }
2470
2715
  } catch (e) {
2471
2716
  console.error('加载 Claude 配置失败:', e);
2472
- }
2473
- }
2474
- const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
2475
- if (savedOpenclawConfigs) {
2476
- try {
2477
- this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
2478
- const configNames = Object.keys(this.openclawConfigs);
2479
- if (configNames.length > 0) {
2480
- this.currentOpenclawConfig = configNames[0];
2481
- }
2482
- } catch (e) {
2483
- console.error('加载 OpenClaw 配置失败:', e);
2484
- }
2485
- } else {
2486
- const configNames = Object.keys(this.openclawConfigs);
2487
- if (configNames.length > 0) {
2488
- this.currentOpenclawConfig = configNames[0];
2489
- }
2490
- }
2491
- this.loadAll();
2492
- },
2717
+ }
2718
+ }
2719
+ if (!this.currentClaudeConfig) {
2720
+ const configNames = Object.keys(this.claudeConfigs);
2721
+ if (configNames.length > 0) {
2722
+ this.currentClaudeConfig = configNames[0];
2723
+ }
2724
+ }
2725
+ this.syncClaudeModelFromConfig();
2726
+ const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
2727
+ if (savedOpenclawConfigs) {
2728
+ try {
2729
+ this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
2730
+ const configNames = Object.keys(this.openclawConfigs);
2731
+ if (configNames.length > 0) {
2732
+ this.currentOpenclawConfig = configNames[0];
2733
+ }
2734
+ } catch (e) {
2735
+ console.error('加载 OpenClaw 配置失败:', e);
2736
+ }
2737
+ } else {
2738
+ const configNames = Object.keys(this.openclawConfigs);
2739
+ if (configNames.length > 0) {
2740
+ this.currentOpenclawConfig = configNames[0];
2741
+ }
2742
+ }
2743
+ this.loadAll();
2744
+ },
2493
2745
  methods: {
2494
2746
  async loadAll() {
2495
2747
  this.loading = true;
2496
2748
  this.initError = '';
2497
2749
  try {
2498
- const [statusRes, listRes, modelsRes] = await Promise.all([
2499
- api('status'),
2500
- api('list'),
2501
- api('models')
2502
- ]);
2750
+ const statusRes = await api('status');
2751
+ const listRes = await api('list');
2503
2752
 
2504
2753
  if (statusRes.error) {
2505
2754
  this.initError = statusRes.error;
@@ -2507,14 +2756,16 @@
2507
2756
  this.currentProvider = statusRes.provider;
2508
2757
  this.currentModel = statusRes.model;
2509
2758
  this.providersList = listRes.providers;
2510
- this.models = modelsRes.models;
2759
+ await this.loadModelsForProvider(this.currentProvider);
2511
2760
  if (statusRes.configReady === false) {
2512
2761
  this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板;请在模板编辑器确认后创建。', 'info');
2513
2762
  }
2514
2763
  if (statusRes.initNotice) {
2515
2764
  this.showMessage(statusRes.initNotice, 'info');
2516
2765
  }
2766
+ this.maybeShowStarPrompt();
2517
2767
  }
2768
+ await this.loadRecentConfigs();
2518
2769
  } catch (e) {
2519
2770
  this.initError = '连接失败: ' + e.message;
2520
2771
  } finally {
@@ -2522,8 +2773,129 @@
2522
2773
  }
2523
2774
  },
2524
2775
 
2776
+ async loadModelsForProvider(providerName) {
2777
+ this.codexModelsLoading = true;
2778
+ if (!providerName) {
2779
+ this.models = [];
2780
+ this.modelsSource = 'unlimited';
2781
+ this.modelsHasCurrent = true;
2782
+ this.codexModelsLoading = false;
2783
+ return;
2784
+ }
2785
+ try {
2786
+ const res = await api('models', { provider: providerName });
2787
+ if (res.unlimited) {
2788
+ this.models = [];
2789
+ this.modelsSource = 'unlimited';
2790
+ this.modelsHasCurrent = true;
2791
+ return;
2792
+ }
2793
+ if (res.error) {
2794
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
2795
+ this.models = [];
2796
+ this.modelsSource = 'error';
2797
+ this.modelsHasCurrent = true;
2798
+ return;
2799
+ }
2800
+ const list = Array.isArray(res.models) ? res.models : [];
2801
+ this.models = list;
2802
+ this.modelsSource = res.source || 'remote';
2803
+ this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
2804
+ } catch (e) {
2805
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
2806
+ this.models = [];
2807
+ this.modelsSource = 'error';
2808
+ this.modelsHasCurrent = true;
2809
+ } finally {
2810
+ this.codexModelsLoading = false;
2811
+ }
2812
+ },
2813
+
2814
+ getCurrentClaudeConfig() {
2815
+ if (!this.currentClaudeConfig) return null;
2816
+ return this.claudeConfigs[this.currentClaudeConfig] || null;
2817
+ },
2818
+
2819
+ syncClaudeModelFromConfig() {
2820
+ const config = this.getCurrentClaudeConfig();
2821
+ this.currentClaudeModel = config && config.model ? config.model : '';
2822
+ },
2823
+
2824
+ refreshClaudeModelContext() {
2825
+ this.syncClaudeModelFromConfig();
2826
+ this.loadClaudeModels();
2827
+ },
2828
+
2829
+ resetClaudeModelsState() {
2830
+ this.claudeModels = [];
2831
+ this.claudeModelsSource = 'idle';
2832
+ this.claudeModelsHasCurrent = true;
2833
+ this.claudeModelsLoading = false;
2834
+ },
2835
+
2836
+ updateClaudeModelsCurrent() {
2837
+ const currentModel = (this.currentClaudeModel || '').trim();
2838
+ this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
2839
+ },
2840
+
2841
+ async loadClaudeModels() {
2842
+ const config = this.getCurrentClaudeConfig();
2843
+ const baseUrl = (config.baseUrl || '').trim();
2844
+ const apiKey = (config.apiKey || '').trim();
2845
+
2846
+ if (!baseUrl) {
2847
+ this.resetClaudeModelsState();
2848
+ return;
2849
+ }
2850
+
2851
+ this.claudeModelsLoading = true;
2852
+ try {
2853
+ const res = await api('models-by-url', { baseUrl, apiKey });
2854
+ if (res.unlimited) {
2855
+ this.claudeModels = [];
2856
+ this.claudeModelsSource = 'unlimited';
2857
+ this.claudeModelsHasCurrent = true;
2858
+ return;
2859
+ }
2860
+ if (res.error) {
2861
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
2862
+ this.claudeModels = [];
2863
+ this.claudeModelsSource = 'error';
2864
+ this.claudeModelsHasCurrent = true;
2865
+ return;
2866
+ }
2867
+ const list = Array.isArray(res.models) ? res.models : [];
2868
+ this.claudeModels = list;
2869
+ this.claudeModelsSource = res.source || 'remote';
2870
+ this.updateClaudeModelsCurrent();
2871
+ } catch (e) {
2872
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
2873
+ this.claudeModels = [];
2874
+ this.claudeModelsSource = 'error';
2875
+ this.claudeModelsHasCurrent = true;
2876
+ } finally {
2877
+ this.claudeModelsLoading = false;
2878
+ }
2879
+ },
2880
+
2881
+ openClaudeConfigModal() {
2882
+ this.showClaudeConfigModal = true;
2883
+ },
2884
+
2885
+ maybeShowStarPrompt() {
2886
+ const storageKey = 'codexmateStarPrompted';
2887
+ if (localStorage.getItem(storageKey)) {
2888
+ return;
2889
+ }
2890
+ this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star。', 'info');
2891
+ localStorage.setItem(storageKey, '1');
2892
+ },
2893
+
2525
2894
  switchConfigMode(mode) {
2526
2895
  this.configMode = mode;
2896
+ if (mode === 'claude') {
2897
+ this.refreshClaudeModelContext();
2898
+ }
2527
2899
  if (mode === 'sessions' && this.sessionsList.length === 0) {
2528
2900
  this.loadSessions();
2529
2901
  }
@@ -2915,6 +3287,7 @@
2915
3287
 
2916
3288
  async switchProvider(name) {
2917
3289
  this.currentProvider = name;
3290
+ await this.loadModelsForProvider(name);
2918
3291
  await this.openConfigTemplateEditor();
2919
3292
  },
2920
3293
 
@@ -2922,6 +3295,60 @@
2922
3295
  await this.openConfigTemplateEditor();
2923
3296
  },
2924
3297
 
3298
+ async loadRecentConfigs() {
3299
+ this.recentLoading = true;
3300
+ try {
3301
+ const res = await api('get-recent-configs');
3302
+ if (res && Array.isArray(res.items)) {
3303
+ this.recentConfigs = res.items;
3304
+ } else {
3305
+ this.recentConfigs = [];
3306
+ }
3307
+ } catch (e) {
3308
+ this.recentConfigs = [];
3309
+ } finally {
3310
+ this.recentLoading = false;
3311
+ }
3312
+ },
3313
+
3314
+ async applyRecentConfig(item) {
3315
+ if (!item || !item.provider || !item.model) {
3316
+ this.showMessage('最近配置无效,无法应用', 'error');
3317
+ return;
3318
+ }
3319
+ this.currentProvider = item.provider;
3320
+ this.currentModel = item.model;
3321
+ await this.openConfigTemplateEditor({
3322
+ appendHint: '最近使用配置,确认后将写入 config.toml'
3323
+ });
3324
+ },
3325
+
3326
+ async runHealthCheck() {
3327
+ this.healthCheckLoading = true;
3328
+ this.healthCheckResult = null;
3329
+ try {
3330
+ const res = await api('config-health-check', {
3331
+ remote: this.healthCheckRemote
3332
+ });
3333
+ if (res && typeof res === 'object') {
3334
+ this.healthCheckResult = res;
3335
+ if (res.ok) {
3336
+ this.showMessage('健康检查通过', 'success');
3337
+ } else {
3338
+ this.showMessage('发现配置问题,请查看详情', 'error');
3339
+ }
3340
+ } else {
3341
+ this.healthCheckResult = null;
3342
+ this.showMessage('健康检查失败:返回数据异常', 'error');
3343
+ }
3344
+ } catch (e) {
3345
+ this.healthCheckResult = null;
3346
+ this.showMessage('健康检查失败: ' + e.message, 'error');
3347
+ } finally {
3348
+ this.healthCheckLoading = false;
3349
+ }
3350
+ },
3351
+
2925
3352
  escapeTomlString(value) {
2926
3353
  return String(value || '')
2927
3354
  .replace(/\\/g, '\\\\')
@@ -2984,84 +3411,84 @@
2984
3411
  }
2985
3412
  },
2986
3413
 
2987
- async openAgentsEditor() {
2988
- this.setAgentsModalContext('codex');
2989
- this.agentsLoading = true;
2990
- try {
2991
- const res = await api('get-agents-file');
2992
- if (res.error) {
2993
- this.showMessage(res.error, 'error');
3414
+ async openAgentsEditor() {
3415
+ this.setAgentsModalContext('codex');
3416
+ this.agentsLoading = true;
3417
+ try {
3418
+ const res = await api('get-agents-file');
3419
+ if (res.error) {
3420
+ this.showMessage(res.error, 'error');
3421
+ return;
3422
+ }
3423
+ this.agentsContent = res.content || '';
3424
+ this.agentsPath = res.path || '';
3425
+ this.agentsExists = !!res.exists;
3426
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3427
+ this.showAgentsModal = true;
3428
+ } catch (e) {
3429
+ this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
3430
+ } finally {
3431
+ this.agentsLoading = false;
3432
+ }
3433
+ },
3434
+
3435
+ async openOpenclawAgentsEditor() {
3436
+ this.setAgentsModalContext('openclaw');
3437
+ this.agentsLoading = true;
3438
+ try {
3439
+ const res = await api('get-openclaw-agents-file');
3440
+ if (res.error) {
3441
+ this.showMessage(res.error, 'error');
2994
3442
  return;
2995
3443
  }
3444
+ if (res.configError) {
3445
+ this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
3446
+ }
2996
3447
  this.agentsContent = res.content || '';
2997
3448
  this.agentsPath = res.path || '';
2998
3449
  this.agentsExists = !!res.exists;
2999
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3000
- this.showAgentsModal = true;
3001
- } catch (e) {
3002
- this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
3003
- } finally {
3004
- this.agentsLoading = false;
3005
- }
3006
- },
3007
-
3008
- async openOpenclawAgentsEditor() {
3009
- this.setAgentsModalContext('openclaw');
3010
- this.agentsLoading = true;
3011
- try {
3012
- const res = await api('get-openclaw-agents-file');
3013
- if (res.error) {
3014
- this.showMessage(res.error, 'error');
3015
- return;
3016
- }
3017
- if (res.configError) {
3018
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
3019
- }
3020
- this.agentsContent = res.content || '';
3021
- this.agentsPath = res.path || '';
3022
- this.agentsExists = !!res.exists;
3023
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3024
- this.showAgentsModal = true;
3025
- } catch (e) {
3026
- this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
3027
- } finally {
3028
- this.agentsLoading = false;
3029
- }
3030
- },
3031
-
3032
- setAgentsModalContext(context) {
3033
- this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
3034
- if (this.agentsContext === 'openclaw') {
3035
- this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
3036
- this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
3037
- } else {
3038
- this.agentsModalTitle = 'AGENTS.md 编辑器';
3039
- this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
3040
- }
3041
- },
3042
-
3043
- closeAgentsModal() {
3044
- this.showAgentsModal = false;
3045
- this.agentsContent = '';
3046
- this.agentsPath = '';
3047
- this.agentsExists = false;
3048
- this.agentsLineEnding = '\n';
3049
- this.agentsSaving = false;
3050
- this.setAgentsModalContext('codex');
3051
- },
3052
-
3053
- async applyAgentsContent() {
3054
- this.agentsSaving = true;
3055
- try {
3056
- const action = this.agentsContext === 'openclaw'
3057
- ? 'apply-openclaw-agents-file'
3058
- : 'apply-agents-file';
3059
- const res = await api(action, {
3060
- content: this.agentsContent,
3061
- lineEnding: this.agentsLineEnding
3062
- });
3063
- if (res.error) {
3064
- this.showMessage(res.error, 'error');
3450
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3451
+ this.showAgentsModal = true;
3452
+ } catch (e) {
3453
+ this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
3454
+ } finally {
3455
+ this.agentsLoading = false;
3456
+ }
3457
+ },
3458
+
3459
+ setAgentsModalContext(context) {
3460
+ this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
3461
+ if (this.agentsContext === 'openclaw') {
3462
+ this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
3463
+ this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
3464
+ } else {
3465
+ this.agentsModalTitle = 'AGENTS.md 编辑器';
3466
+ this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
3467
+ }
3468
+ },
3469
+
3470
+ closeAgentsModal() {
3471
+ this.showAgentsModal = false;
3472
+ this.agentsContent = '';
3473
+ this.agentsPath = '';
3474
+ this.agentsExists = false;
3475
+ this.agentsLineEnding = '\n';
3476
+ this.agentsSaving = false;
3477
+ this.setAgentsModalContext('codex');
3478
+ },
3479
+
3480
+ async applyAgentsContent() {
3481
+ this.agentsSaving = true;
3482
+ try {
3483
+ const action = this.agentsContext === 'openclaw'
3484
+ ? 'apply-openclaw-agents-file'
3485
+ : 'apply-agents-file';
3486
+ const res = await api(action, {
3487
+ content: this.agentsContent,
3488
+ lineEnding: this.agentsLineEnding
3489
+ });
3490
+ if (res.error) {
3491
+ this.showMessage(res.error, 'error');
3065
3492
  return;
3066
3493
  }
3067
3494
  this.showMessage('AGENTS.md 已保存', 'success');
@@ -3186,6 +3613,35 @@
3186
3613
 
3187
3614
  switchClaudeConfig(name) {
3188
3615
  this.currentClaudeConfig = name;
3616
+ this.refreshClaudeModelContext();
3617
+ },
3618
+
3619
+ onClaudeModelChange() {
3620
+ const name = this.currentClaudeConfig;
3621
+ if (!name) {
3622
+ this.showMessage('请先选择配置', 'error');
3623
+ return;
3624
+ }
3625
+ const model = (this.currentClaudeModel || '').trim();
3626
+ if (!model) {
3627
+ this.showMessage('请输入模型', 'error');
3628
+ return;
3629
+ }
3630
+ const existing = this.claudeConfigs[name] || {};
3631
+ this.currentClaudeModel = model;
3632
+ this.claudeConfigs[name] = {
3633
+ apiKey: existing.apiKey || '',
3634
+ baseUrl: existing.baseUrl || '',
3635
+ model: model,
3636
+ hasKey: !!existing.apiKey
3637
+ };
3638
+ this.saveClaudeConfigs();
3639
+ this.updateClaudeModelsCurrent();
3640
+ if (!this.claudeConfigs[name].apiKey) {
3641
+ this.showMessage('该配置未设置 API Key,请先编辑', 'error');
3642
+ return;
3643
+ }
3644
+ this.applyClaudeConfig(name);
3189
3645
  },
3190
3646
 
3191
3647
  saveClaudeConfigs() {
@@ -3214,6 +3670,9 @@
3214
3670
  this.saveClaudeConfigs();
3215
3671
  this.showMessage('配置已更新', 'success');
3216
3672
  this.closeEditConfigModal();
3673
+ if (name === this.currentClaudeConfig) {
3674
+ this.refreshClaudeModelContext();
3675
+ }
3217
3676
  },
3218
3677
 
3219
3678
  closeEditConfigModal() {
@@ -3233,7 +3692,12 @@
3233
3692
 
3234
3693
  const config = this.claudeConfigs[name];
3235
3694
  if (!config.apiKey) {
3236
- return this.showMessage('请先输入 API Key', 'error');
3695
+ this.showMessage('已保存,未应用:请先输入 API Key', 'info');
3696
+ this.closeEditConfigModal();
3697
+ if (name === this.currentClaudeConfig) {
3698
+ this.refreshClaudeModelContext();
3699
+ }
3700
+ return;
3237
3701
  }
3238
3702
 
3239
3703
  const res = await api('apply-claude-config', { config });
@@ -3243,30 +3707,9 @@
3243
3707
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3244
3708
  this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
3245
3709
  this.closeEditConfigModal();
3246
- }
3247
- },
3248
-
3249
- async saveAndApplyEnvCompat() {
3250
- const name = this.editingConfig.name;
3251
- this.claudeConfigs[name] = {
3252
- apiKey: this.editingConfig.apiKey,
3253
- baseUrl: this.editingConfig.baseUrl,
3254
- model: this.editingConfig.model,
3255
- hasKey: !!this.editingConfig.apiKey
3256
- };
3257
- this.saveClaudeConfigs();
3258
-
3259
- const config = this.claudeConfigs[name];
3260
- if (!config.apiKey) {
3261
- return this.showMessage('请先输入 API Key', 'error');
3262
- }
3263
-
3264
- const res = await api('apply-env', { config });
3265
- if (res.error || res.success === false) {
3266
- this.showMessage(res.error || '应用环境变量失败', 'error');
3267
- } else {
3268
- this.showMessage('已保存并应用到系统环境变量(兼容模式)', 'success');
3269
- this.closeEditConfigModal();
3710
+ if (name === this.currentClaudeConfig) {
3711
+ this.refreshClaudeModelContext();
3712
+ }
3270
3713
  }
3271
3714
  },
3272
3715
 
@@ -3290,6 +3733,7 @@
3290
3733
  this.saveClaudeConfigs();
3291
3734
  this.showMessage('配置已添加', 'success');
3292
3735
  this.closeClaudeConfigModal();
3736
+ this.refreshClaudeModelContext();
3293
3737
  },
3294
3738
 
3295
3739
  deleteClaudeConfig(name) {
@@ -3305,10 +3749,12 @@
3305
3749
  }
3306
3750
  this.saveClaudeConfigs();
3307
3751
  this.showMessage('配置已删除', 'success');
3752
+ this.refreshClaudeModelContext();
3308
3753
  },
3309
3754
 
3310
3755
  async applyClaudeConfig(name) {
3311
3756
  this.currentClaudeConfig = name;
3757
+ this.refreshClaudeModelContext();
3312
3758
  const config = this.claudeConfigs[name];
3313
3759
 
3314
3760
  if (!config.apiKey) {
@@ -3324,187 +3770,187 @@
3324
3770
  }
3325
3771
  },
3326
3772
 
3327
- closeClaudeConfigModal() {
3328
- this.showClaudeConfigModal = false;
3329
- this.newClaudeConfig = {
3330
- name: '',
3331
- apiKey: '',
3332
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3333
- model: 'glm-4.7'
3334
- };
3335
- },
3336
-
3337
- openclawHasContent(config) {
3338
- return !!(config && typeof config.content === 'string' && config.content.trim());
3339
- },
3340
-
3341
- openclawSubtitle(config) {
3342
- if (!this.openclawHasContent(config)) {
3343
- return '未设置配置';
3344
- }
3345
- const length = config.content.trim().length;
3346
- return `已保存 ${length} 字符`;
3347
- },
3348
-
3349
- saveOpenclawConfigs() {
3350
- localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
3351
- },
3352
-
3353
- openOpenclawAddModal() {
3354
- this.openclawEditorTitle = '添加 OpenClaw 配置';
3355
- this.openclawEditing = {
3356
- name: '',
3357
- content: DEFAULT_OPENCLAW_TEMPLATE,
3358
- lockName: false
3359
- };
3360
- this.openclawConfigPath = '';
3361
- this.openclawConfigExists = false;
3362
- this.openclawLineEnding = '\n';
3363
- this.showOpenclawConfigModal = true;
3364
- },
3365
-
3366
- openOpenclawEditModal(name) {
3367
- const config = this.openclawConfigs[name];
3368
- this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
3369
- this.openclawEditing = {
3370
- name,
3371
- content: (config && config.content) ? config.content : '',
3372
- lockName: true
3373
- };
3374
- this.showOpenclawConfigModal = true;
3375
- },
3376
-
3377
- closeOpenclawConfigModal() {
3378
- this.showOpenclawConfigModal = false;
3379
- this.openclawEditing = { name: '', content: '', lockName: false };
3380
- this.openclawSaving = false;
3381
- this.openclawApplying = false;
3382
- },
3383
-
3384
- async loadOpenclawConfigFromFile() {
3385
- this.openclawFileLoading = true;
3386
- try {
3387
- const res = await api('get-openclaw-config');
3388
- if (res.error) {
3389
- this.showMessage(res.error, 'error');
3390
- return;
3391
- }
3392
- this.openclawConfigPath = res.path || '';
3393
- this.openclawConfigExists = !!res.exists;
3394
- this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3395
- if (res.content && res.content.trim()) {
3396
- this.openclawEditing.content = res.content;
3397
- } else if (!this.openclawEditing.content) {
3398
- this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
3399
- }
3400
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
3401
- } catch (e) {
3402
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
3403
- } finally {
3404
- this.openclawFileLoading = false;
3405
- }
3406
- },
3407
-
3408
- persistOpenclawConfig({ closeModal = true } = {}) {
3409
- if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
3410
- this.showMessage('请输入配置名称', 'error');
3411
- return '';
3412
- }
3413
- const name = this.openclawEditing.name.trim();
3414
- if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
3415
- this.showMessage('配置名称已存在', 'error');
3416
- return '';
3417
- }
3418
- if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
3419
- this.showMessage('配置内容不能为空', 'error');
3420
- return '';
3421
- }
3422
-
3423
- this.openclawConfigs[name] = {
3424
- content: this.openclawEditing.content
3425
- };
3426
- this.currentOpenclawConfig = name;
3427
- this.saveOpenclawConfigs();
3428
- if (closeModal) {
3429
- this.closeOpenclawConfigModal();
3430
- }
3431
- return name;
3432
- },
3433
-
3434
- async saveOpenclawConfig() {
3435
- this.openclawSaving = true;
3436
- try {
3437
- const name = this.persistOpenclawConfig();
3438
- if (!name) return;
3439
- this.showMessage('OpenClaw 配置已保存', 'success');
3440
- } finally {
3441
- this.openclawSaving = false;
3442
- }
3443
- },
3444
-
3445
- async saveAndApplyOpenclawConfig() {
3446
- this.openclawApplying = true;
3447
- try {
3448
- const name = this.persistOpenclawConfig({ closeModal: false });
3449
- if (!name) return;
3450
- const config = this.openclawConfigs[name];
3451
- const res = await api('apply-openclaw-config', {
3452
- content: config.content,
3453
- lineEnding: this.openclawLineEnding
3454
- });
3455
- if (res.error || res.success === false) {
3456
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3457
- return;
3458
- }
3459
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3460
- this.openclawConfigExists = true;
3461
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3462
- this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
3463
- this.closeOpenclawConfigModal();
3464
- } catch (e) {
3465
- this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
3466
- } finally {
3467
- this.openclawApplying = false;
3468
- }
3469
- },
3470
-
3471
- deleteOpenclawConfig(name) {
3472
- if (Object.keys(this.openclawConfigs).length <= 1) {
3473
- return this.showMessage('至少保留一个配置', 'error');
3474
- }
3475
- if (!confirm(`确定删除配置 "${name}"?`)) return;
3476
- delete this.openclawConfigs[name];
3477
- if (this.currentOpenclawConfig === name) {
3478
- this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
3479
- }
3480
- this.saveOpenclawConfigs();
3481
- this.showMessage('OpenClaw 配置已删除', 'success');
3482
- },
3483
-
3484
- async applyOpenclawConfig(name) {
3485
- this.currentOpenclawConfig = name;
3486
- const config = this.openclawConfigs[name];
3487
- if (!this.openclawHasContent(config)) {
3488
- return this.showMessage('该配置为空,请先编辑', 'error');
3489
- }
3490
- const res = await api('apply-openclaw-config', {
3491
- content: config.content,
3492
- lineEnding: this.openclawLineEnding
3493
- });
3494
- if (res.error || res.success === false) {
3495
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3496
- } else {
3497
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3498
- this.openclawConfigExists = true;
3499
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3500
- this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
3501
- }
3502
- },
3503
-
3504
- formatLatency(result) {
3505
- if (!result) return '';
3506
- if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
3507
- const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
3773
+ closeClaudeConfigModal() {
3774
+ this.showClaudeConfigModal = false;
3775
+ this.newClaudeConfig = {
3776
+ name: '',
3777
+ apiKey: '',
3778
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3779
+ model: 'glm-4.7'
3780
+ };
3781
+ },
3782
+
3783
+ openclawHasContent(config) {
3784
+ return !!(config && typeof config.content === 'string' && config.content.trim());
3785
+ },
3786
+
3787
+ openclawSubtitle(config) {
3788
+ if (!this.openclawHasContent(config)) {
3789
+ return '未设置配置';
3790
+ }
3791
+ const length = config.content.trim().length;
3792
+ return `已保存 ${length} 字符`;
3793
+ },
3794
+
3795
+ saveOpenclawConfigs() {
3796
+ localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
3797
+ },
3798
+
3799
+ openOpenclawAddModal() {
3800
+ this.openclawEditorTitle = '添加 OpenClaw 配置';
3801
+ this.openclawEditing = {
3802
+ name: '',
3803
+ content: DEFAULT_OPENCLAW_TEMPLATE,
3804
+ lockName: false
3805
+ };
3806
+ this.openclawConfigPath = '';
3807
+ this.openclawConfigExists = false;
3808
+ this.openclawLineEnding = '\n';
3809
+ this.showOpenclawConfigModal = true;
3810
+ },
3811
+
3812
+ openOpenclawEditModal(name) {
3813
+ const config = this.openclawConfigs[name];
3814
+ this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
3815
+ this.openclawEditing = {
3816
+ name,
3817
+ content: (config && config.content) ? config.content : '',
3818
+ lockName: true
3819
+ };
3820
+ this.showOpenclawConfigModal = true;
3821
+ },
3822
+
3823
+ closeOpenclawConfigModal() {
3824
+ this.showOpenclawConfigModal = false;
3825
+ this.openclawEditing = { name: '', content: '', lockName: false };
3826
+ this.openclawSaving = false;
3827
+ this.openclawApplying = false;
3828
+ },
3829
+
3830
+ async loadOpenclawConfigFromFile() {
3831
+ this.openclawFileLoading = true;
3832
+ try {
3833
+ const res = await api('get-openclaw-config');
3834
+ if (res.error) {
3835
+ this.showMessage(res.error, 'error');
3836
+ return;
3837
+ }
3838
+ this.openclawConfigPath = res.path || '';
3839
+ this.openclawConfigExists = !!res.exists;
3840
+ this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3841
+ if (res.content && res.content.trim()) {
3842
+ this.openclawEditing.content = res.content;
3843
+ } else if (!this.openclawEditing.content) {
3844
+ this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
3845
+ }
3846
+ this.showMessage('已加载当前 OpenClaw 配置', 'success');
3847
+ } catch (e) {
3848
+ this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
3849
+ } finally {
3850
+ this.openclawFileLoading = false;
3851
+ }
3852
+ },
3853
+
3854
+ persistOpenclawConfig({ closeModal = true } = {}) {
3855
+ if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
3856
+ this.showMessage('请输入配置名称', 'error');
3857
+ return '';
3858
+ }
3859
+ const name = this.openclawEditing.name.trim();
3860
+ if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
3861
+ this.showMessage('配置名称已存在', 'error');
3862
+ return '';
3863
+ }
3864
+ if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
3865
+ this.showMessage('配置内容不能为空', 'error');
3866
+ return '';
3867
+ }
3868
+
3869
+ this.openclawConfigs[name] = {
3870
+ content: this.openclawEditing.content
3871
+ };
3872
+ this.currentOpenclawConfig = name;
3873
+ this.saveOpenclawConfigs();
3874
+ if (closeModal) {
3875
+ this.closeOpenclawConfigModal();
3876
+ }
3877
+ return name;
3878
+ },
3879
+
3880
+ async saveOpenclawConfig() {
3881
+ this.openclawSaving = true;
3882
+ try {
3883
+ const name = this.persistOpenclawConfig();
3884
+ if (!name) return;
3885
+ this.showMessage('OpenClaw 配置已保存', 'success');
3886
+ } finally {
3887
+ this.openclawSaving = false;
3888
+ }
3889
+ },
3890
+
3891
+ async saveAndApplyOpenclawConfig() {
3892
+ this.openclawApplying = true;
3893
+ try {
3894
+ const name = this.persistOpenclawConfig({ closeModal: false });
3895
+ if (!name) return;
3896
+ const config = this.openclawConfigs[name];
3897
+ const res = await api('apply-openclaw-config', {
3898
+ content: config.content,
3899
+ lineEnding: this.openclawLineEnding
3900
+ });
3901
+ if (res.error || res.success === false) {
3902
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3903
+ return;
3904
+ }
3905
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3906
+ this.openclawConfigExists = true;
3907
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3908
+ this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
3909
+ this.closeOpenclawConfigModal();
3910
+ } catch (e) {
3911
+ this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
3912
+ } finally {
3913
+ this.openclawApplying = false;
3914
+ }
3915
+ },
3916
+
3917
+ deleteOpenclawConfig(name) {
3918
+ if (Object.keys(this.openclawConfigs).length <= 1) {
3919
+ return this.showMessage('至少保留一个配置', 'error');
3920
+ }
3921
+ if (!confirm(`确定删除配置 "${name}"?`)) return;
3922
+ delete this.openclawConfigs[name];
3923
+ if (this.currentOpenclawConfig === name) {
3924
+ this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
3925
+ }
3926
+ this.saveOpenclawConfigs();
3927
+ this.showMessage('OpenClaw 配置已删除', 'success');
3928
+ },
3929
+
3930
+ async applyOpenclawConfig(name) {
3931
+ this.currentOpenclawConfig = name;
3932
+ const config = this.openclawConfigs[name];
3933
+ if (!this.openclawHasContent(config)) {
3934
+ return this.showMessage('该配置为空,请先编辑', 'error');
3935
+ }
3936
+ const res = await api('apply-openclaw-config', {
3937
+ content: config.content,
3938
+ lineEnding: this.openclawLineEnding
3939
+ });
3940
+ if (res.error || res.success === false) {
3941
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3942
+ } else {
3943
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3944
+ this.openclawConfigExists = true;
3945
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3946
+ this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
3947
+ }
3948
+ },
3949
+
3950
+ formatLatency(result) {
3951
+ if (!result) return '';
3952
+ if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
3953
+ const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
3508
3954
  return `${ms}ms`;
3509
3955
  },
3510
3956
 
@@ -3537,10 +3983,3 @@
3537
3983
  </script>
3538
3984
  </body>
3539
3985
  </html>
3540
-
3541
-
3542
-
3543
-
3544
-
3545
-
3546
-