coding-tool-x 3.3.1 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/web/assets/{Analytics-BskCbia_.js → Analytics-DtR00OYP.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-B4X3rgfY.js → ConfigTemplates-DWiSFOp5.js} +1 -1
  3. package/dist/web/assets/Home-BsSioaaB.css +1 -0
  4. package/dist/web/assets/Home-DUu2mGb6.js +1 -0
  5. package/dist/web/assets/{PluginManager-D_LoULGH.js → PluginManager-DsJ1KtNr.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DiV4Qwa1.js → ProjectList-CzTJaBJb.js} +1 -1
  7. package/dist/web/assets/{SessionList-B24o0wiX.js → SessionList-D1ovPZ0I.js} +1 -1
  8. package/dist/web/assets/{SkillManager-B9Rnuaig.js → SkillManager-DqpDTc2c.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-BkL2l5J9.js → WorkspaceManager-Dj28-3G5.js} +1 -1
  10. package/dist/web/assets/index-CaKktouI.js +2 -0
  11. package/dist/web/assets/{index-C5j22icm.css → index-DZjDFGqR.css} +1 -1
  12. package/dist/web/index.html +2 -2
  13. package/package.json +1 -1
  14. package/src/server/api/codex-proxy.js +69 -38
  15. package/src/server/api/config-export.js +4 -4
  16. package/src/server/api/gemini-proxy.js +69 -36
  17. package/src/server/api/opencode-proxy.js +48 -13
  18. package/src/server/api/proxy.js +16 -4
  19. package/src/server/services/channels.js +6 -6
  20. package/src/server/services/codex-channels.js +18 -9
  21. package/src/server/services/codex-settings-manager.js +18 -0
  22. package/src/server/services/config-export-service.js +8 -4
  23. package/src/server/services/gemini-channels.js +19 -11
  24. package/src/server/services/gemini-settings-manager.js +14 -0
  25. package/src/server/services/mcp-service.js +40 -15
  26. package/src/server/services/opencode-channels.js +18 -0
  27. package/src/server/services/opencode-sessions.js +25 -1
  28. package/src/server/services/opencode-settings-manager.js +16 -0
  29. package/dist/web/assets/Home-DHYMMKOU.js +0 -1
  30. package/dist/web/assets/Home-DuPOICVF.css +0 -1
  31. package/dist/web/assets/index-ZttxvTKw.js +0 -2
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-ZttxvTKw.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CaKktouI.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-C9MYpaSi.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-DET08QYg.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-DMjSfzlv.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-CxpuzdjU.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/icons-B29onFfZ.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-C5j22icm.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-DZjDFGqR.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.3.1",
3
+ "version": "3.3.2",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -11,7 +11,8 @@ const {
11
11
  isProxyConfig,
12
12
  getCurrentProxyPort,
13
13
  configExists,
14
- hasBackup
14
+ hasBackup,
15
+ readConfig
15
16
  } = require('../services/codex-settings-manager');
16
17
  const { getChannels, getEnabledChannels } = require('../services/codex-channels');
17
18
  const { clearAllLogs } = require('../websocket-server');
@@ -41,6 +42,21 @@ function saveActiveChannelId(channelId) {
41
42
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
42
43
  }
43
44
 
45
+ function loadActiveChannelId() {
46
+ ensureStorageDirMigrated();
47
+ const filePath = PATHS.activeChannel.codex;
48
+ try {
49
+ if (fs.existsSync(filePath)) {
50
+ const content = fs.readFileSync(filePath, 'utf8');
51
+ const data = JSON.parse(content);
52
+ return data.activeChannelId || null;
53
+ }
54
+ } catch (error) {
55
+ console.error('[Codex Proxy] Error loading active channel ID:', error);
56
+ }
57
+ return null;
58
+ }
59
+
44
60
  function removeActiveChannelFile() {
45
61
  ensureStorageDirMigrated();
46
62
  const filePath = PATHS.activeChannel.codex;
@@ -84,9 +100,20 @@ router.post('/start', async (req, res) => {
84
100
  });
85
101
  }
86
102
 
87
- // 2. 获取当前启用的渠道(多渠道模式)
103
+ // 2. 获取当前启用的渠道(优先使用当前配置中正在使用的 provider)
88
104
  const enabledChannels = getEnabledChannels();
89
- const currentChannel = enabledChannels[0];
105
+ let currentChannel = null;
106
+ try {
107
+ const currentProvider = readConfig()?.model_provider;
108
+ if (currentProvider && currentProvider !== 'cc-proxy') {
109
+ currentChannel = enabledChannels.find(ch => ch.providerKey === currentProvider) || null;
110
+ }
111
+ } catch (err) {
112
+ // ignore and fallback
113
+ }
114
+ if (!currentChannel) {
115
+ currentChannel = enabledChannels[0] || null;
116
+ }
90
117
  if (!currentChannel) {
91
118
  return res.status(400).json({
92
119
  error: 'No enabled Codex channel found. Please create and enable a channel first.'
@@ -141,51 +168,55 @@ router.post('/start', async (req, res) => {
141
168
  // 停止代理
142
169
  router.post('/stop', async (req, res) => {
143
170
  try {
144
- // 1. 获取当前启用的渠道(多渠道模式)
171
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
145
172
  const { channels } = getChannels();
146
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
147
- const activeChannel = enabledChannels[0];
173
+ const activeChannelId = loadActiveChannelId();
174
+ let activeChannel = activeChannelId
175
+ ? channels.find(ch => ch.id === activeChannelId)
176
+ : null;
177
+ if (!activeChannel) {
178
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
179
+ activeChannel = enabledChannels[0] || channels[0] || null;
180
+ }
148
181
 
149
182
  // 2. 停止代理服务器
150
183
  const proxyResult = await stopCodexProxyServer();
184
+ const hadBackup = hasBackup();
185
+
186
+ // 3. 恢复单渠道模式
187
+ // 不恢复整个 config.toml 备份,避免两个问题:
188
+ // - 备份中的旧 auth_mode/tokens 会导致 Codex 用 chatgpt token 认证 → usage limit
189
+ // - 备份中的 mcp_servers 是旧状态,会覆盖用户在动态切换期间对 MCP 的修改
190
+ // 直接丢弃备份,由 applyChannelToSettings 从当前 config.toml 写入正确渠道配置
191
+ if (hadBackup) {
192
+ const { deleteBackup } = require('../services/codex-settings-manager');
193
+ deleteBackup();
194
+ console.log('[Codex Proxy] Discarded backup (MCP changes preserved)');
195
+ }
151
196
 
152
- // 3. 恢复原始配置
153
197
  const { broadcastProxyState } = require('../websocket-server');
154
198
 
155
- if (hasBackup()) {
156
- restoreSettings();
157
- console.log('[Codex Proxy] Restored settings from backup');
158
-
159
- // Enforce single-channel mode: apply the active channel and disable all others
160
- if (activeChannel) {
161
- const { applyChannelToSettings } = require('../services/codex-channels');
162
- applyChannelToSettings(activeChannel.id);
163
- console.log(`[Codex Proxy] Single-channel mode enforced: ${activeChannel.name}`);
164
- }
199
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
200
+ if (activeChannel) {
201
+ const { applyChannelToSettings } = require('../services/codex-channels');
202
+ applyChannelToSettings(activeChannel.id, { pruneProviders: true });
203
+ console.log(`[Codex Proxy] Single-channel mode restored: ${activeChannel.name}`);
204
+ }
165
205
 
166
- // 删除 active-channel.json
167
- removeActiveChannelFile();
206
+ // 删除 active-channel.json
207
+ removeActiveChannelFile();
168
208
 
169
- const response = {
170
- success: true,
171
- message: `Codex proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
172
- port: proxyResult.port,
173
- restoredChannel: activeChannel?.name
174
- };
175
- res.json(response);
176
-
177
- const updatedStatus = getCodexProxyStatus();
178
- broadcastProxyState('codex', updatedStatus, activeChannel, channels);
179
- } else {
180
- res.json({
181
- success: true,
182
- message: 'Codex proxy stopped (no backup to restore)',
183
- port: proxyResult.port
184
- });
209
+ res.json({
210
+ success: true,
211
+ message: `Codex proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
212
+ port: proxyResult.port,
213
+ restoredChannel: activeChannel?.name
214
+ });
185
215
 
186
- const updatedStatus = getCodexProxyStatus();
187
- broadcastProxyState('codex', updatedStatus, activeChannel, channels);
188
- }
216
+ const updatedStatus = getCodexProxyStatus();
217
+ const { channels: latestChannels } = getChannels();
218
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
219
+ broadcastProxyState('codex', updatedStatus, latestActiveChannel, latestChannels);
189
220
  } catch (error) {
190
221
  console.error('[Codex Proxy] Error stopping proxy:', error);
191
222
  res.status(500).json({ error: error.message });
@@ -92,7 +92,7 @@ router.get('/', (req, res) => {
92
92
  * POST /api/config-export/import
93
93
  * Body: { data: {...}, overwrite: boolean }
94
94
  */
95
- router.post('/import', (req, res) => {
95
+ router.post('/import', async (req, res) => {
96
96
  try {
97
97
  const { data, overwrite = false } = req.body;
98
98
 
@@ -103,7 +103,7 @@ router.post('/import', (req, res) => {
103
103
  });
104
104
  }
105
105
 
106
- const result = configExportService.importConfigs(data, { overwrite });
106
+ const result = await configExportService.importConfigs(data, { overwrite });
107
107
 
108
108
  res.json(result);
109
109
  } catch (err) {
@@ -119,7 +119,7 @@ router.post('/import', (req, res) => {
119
119
  * 导入 ZIP 配置
120
120
  * POST /api/config-export/import-zip
121
121
  */
122
- router.post('/import-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
122
+ router.post('/import-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), async (req, res) => {
123
123
  try {
124
124
  const overwrite = req.query.overwrite === 'true';
125
125
  const buffer = req.body;
@@ -132,7 +132,7 @@ router.post('/import-zip', express.raw({ type: ['application/zip', 'application/
132
132
  }
133
133
 
134
134
  const data = parseConfigZip(buffer);
135
- const result = configExportService.importConfigs(data, { overwrite });
135
+ const result = await configExportService.importConfigs(data, { overwrite });
136
136
  res.json(result);
137
137
  } catch (err) {
138
138
  console.error('[ConfigExport API] 导入 ZIP 失败:', err);
@@ -8,10 +8,12 @@ const {
8
8
  const {
9
9
  setProxyConfig,
10
10
  restoreSettings,
11
+ deleteBackup,
11
12
  isProxyConfig,
12
13
  getCurrentProxyPort,
13
14
  configExists,
14
- hasBackup
15
+ hasBackup,
16
+ readEnv
15
17
  } = require('../services/gemini-settings-manager');
16
18
  const { getChannels, getEnabledChannels } = require('../services/gemini-channels');
17
19
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
@@ -35,6 +37,21 @@ function saveActiveChannelId(channelId) {
35
37
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
36
38
  }
37
39
 
40
+ function loadActiveChannelId() {
41
+ ensureStorageDirMigrated();
42
+ const filePath = PATHS.activeChannel.gemini;
43
+ try {
44
+ if (fs.existsSync(filePath)) {
45
+ const content = fs.readFileSync(filePath, 'utf8');
46
+ const data = JSON.parse(content);
47
+ return data.activeChannelId || null;
48
+ }
49
+ } catch (error) {
50
+ console.error('[Gemini Proxy] Error loading active channel ID:', error);
51
+ }
52
+ return null;
53
+ }
54
+
38
55
  function removeActiveChannelFile() {
39
56
  ensureStorageDirMigrated();
40
57
  const filePath = PATHS.activeChannel.gemini;
@@ -78,9 +95,23 @@ router.post('/start', async (req, res) => {
78
95
  });
79
96
  }
80
97
 
81
- // 2. 获取当前启用的渠道(多渠道模式)
98
+ // 2. 获取当前启用的渠道(优先使用当前 .env 对应的渠道)
82
99
  const enabledChannels = getEnabledChannels();
83
- const currentChannel = enabledChannels[0];
100
+ let currentChannel = null;
101
+ try {
102
+ const env = readEnv();
103
+ const baseUrl = env?.GOOGLE_GEMINI_BASE_URL;
104
+ const apiKey = env?.GEMINI_API_KEY;
105
+ const model = env?.GEMINI_MODEL;
106
+ currentChannel = enabledChannels.find(ch =>
107
+ ch.baseUrl === baseUrl && ch.apiKey === apiKey && (!model || ch.model === model)
108
+ ) || enabledChannels.find(ch => ch.baseUrl === baseUrl && ch.apiKey === apiKey) || null;
109
+ } catch (err) {
110
+ // ignore and fallback
111
+ }
112
+ if (!currentChannel) {
113
+ currentChannel = enabledChannels[0] || null;
114
+ }
84
115
  if (!currentChannel) {
85
116
  return res.status(400).json({
86
117
  error: 'No enabled Gemini channel found. Please create and enable a channel first.'
@@ -122,48 +153,50 @@ router.post('/start', async (req, res) => {
122
153
  // 停止代理
123
154
  router.post('/stop', async (req, res) => {
124
155
  try {
125
- // 1. 获取当前启用的渠道(多渠道模式)
156
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
126
157
  const { channels } = getChannels();
127
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
128
- const activeChannel = enabledChannels[0];
158
+ const activeChannelId = loadActiveChannelId();
159
+ let activeChannel = activeChannelId
160
+ ? channels.find(ch => ch.id === activeChannelId)
161
+ : null;
162
+ if (!activeChannel) {
163
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
164
+ activeChannel = enabledChannels[0] || channels[0] || null;
165
+ }
129
166
 
130
167
  // 2. 停止代理服务器
131
168
  const proxyResult = await stopGeminiProxyServer();
169
+ const hadBackup = hasBackup();
132
170
 
133
- // 3. 恢复原始配置
134
- const { broadcastProxyState } = require('../websocket-server');
135
- if (hasBackup()) {
136
- restoreSettings();
137
- console.log('[Gemini Proxy] Restored settings from backup');
138
-
139
- // Enforce single-channel mode: apply the active channel and disable all others
140
- if (activeChannel) {
141
- const { applyChannelToSettings } = require('../services/gemini-channels');
142
- applyChannelToSettings(activeChannel.id);
143
- console.log(`[Gemini Proxy] Single-channel mode enforced: ${activeChannel.name}`);
144
- }
145
-
146
- // 删除 gemini-active-channel.json
147
- removeActiveChannelFile();
148
-
149
- const response = {
150
- success: true,
151
- message: `Gemini proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
152
- port: proxyResult.port,
153
- restoredChannel: activeChannel?.name
154
- };
155
- res.json(response);
156
- } else {
157
- res.json({
158
- success: true,
159
- message: 'Gemini proxy stopped (no backup to restore)',
160
- port: proxyResult.port
161
- });
171
+ // 3. 恢复单渠道模式
172
+ // 不恢复整个备份,避免覆盖用户在动态切换期间对 MCP 等配置的修改
173
+ if (hadBackup) {
174
+ deleteBackup();
175
+ console.log('[Gemini Proxy] Discarded backup (MCP changes preserved)');
162
176
  }
163
177
 
178
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
179
+ if (activeChannel) {
180
+ const { applyChannelToSettings } = require('../services/gemini-channels');
181
+ applyChannelToSettings(activeChannel.id);
182
+ console.log(`[Gemini Proxy] Single-channel mode restored: ${activeChannel.name}`);
183
+ }
184
+
185
+ // 删除 gemini-active-channel.json
186
+ removeActiveChannelFile();
187
+
188
+ res.json({
189
+ success: true,
190
+ message: `Gemini proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
191
+ port: proxyResult.port,
192
+ restoredChannel: activeChannel?.name
193
+ });
194
+
195
+ const { broadcastProxyState } = require('../websocket-server');
164
196
  const proxyStatus = getGeminiProxyStatus();
165
197
  const { channels: latestChannels } = getChannels();
166
- broadcastProxyState('gemini', proxyStatus, activeChannel, latestChannels);
198
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
199
+ broadcastProxyState('gemini', proxyStatus, latestActiveChannel, latestChannels);
167
200
  } catch (error) {
168
201
  console.error('[Gemini Proxy] Error stopping proxy:', error);
169
202
  res.status(500).json({ error: error.message });
@@ -11,10 +11,12 @@ const {
11
11
  hasBackup,
12
12
  setProxyConfig,
13
13
  restoreSettings,
14
+ deleteBackup,
14
15
  isProxyConfig,
15
16
  getCurrentProxyPort
16
17
  } = require('../services/opencode-settings-manager');
17
- const { getChannels, getEnabledChannels } = require('../services/opencode-channels');
18
+ const { getChannels, getEnabledChannels, applyChannelToSettings } = require('../services/opencode-channels');
19
+ const { getSchedulerState } = require('../services/channel-scheduler');
18
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
19
21
  const fs = require('fs');
20
22
  const path = require('path');
@@ -40,6 +42,21 @@ function saveActiveChannelId(channelId) {
40
42
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
41
43
  }
42
44
 
45
+ function loadActiveChannelId() {
46
+ ensureStorageDirMigrated();
47
+ const filePath = PATHS.activeChannel.opencode;
48
+ try {
49
+ if (fs.existsSync(filePath)) {
50
+ const content = fs.readFileSync(filePath, 'utf8');
51
+ const data = JSON.parse(content);
52
+ return data.activeChannelId || null;
53
+ }
54
+ } catch (error) {
55
+ console.error('[OpenCode Proxy] Error loading active channel ID:', error);
56
+ }
57
+ return null;
58
+ }
59
+
43
60
  // 删除激活渠道文件
44
61
  function removeActiveChannelFile() {
45
62
  ensureStorageDirMigrated();
@@ -171,32 +188,50 @@ router.post('/start', async (req, res) => {
171
188
  // 停止代理
172
189
  router.post('/stop', async (req, res) => {
173
190
  try {
174
- // 1. 获取当前渠道信息
191
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
175
192
  const { channels } = getChannels();
176
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
177
- const activeChannel = enabledChannels[0];
193
+ const activeChannelId = loadActiveChannelId();
194
+ let activeChannel = activeChannelId
195
+ ? channels.find(ch => ch.id === activeChannelId)
196
+ : null;
197
+ if (!activeChannel) {
198
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
199
+ activeChannel = enabledChannels[0] || channels[0] || null;
200
+ }
178
201
 
179
202
  // 2. 停止代理服务器
180
203
  const proxyResult = await stopOpenCodeProxyServer();
204
+ const hadBackup = hasBackup();
181
205
 
182
206
  // 3. 删除激活渠道文件
183
207
  removeActiveChannelFile();
184
208
 
185
- // 4. 恢复原始配置
186
- if (hasBackup()) {
187
- restoreSettings();
188
- console.log('[OpenCode Proxy] Restored settings from backup');
209
+ // 4. 恢复单渠道模式
210
+ // 不恢复整个备份,避免覆盖用户在动态切换期间对 MCP 等配置的修改
211
+ if (hadBackup) {
212
+ deleteBackup();
213
+ console.log('[OpenCode Proxy] Discarded backup (MCP changes preserved)');
189
214
  }
190
215
 
191
- // 5. 广播状态更新
192
- const { broadcastProxyState } = require('../websocket-server');
216
+ // 5. 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
217
+ if (activeChannel) {
218
+ applyChannelToSettings(activeChannel.id);
219
+ console.log(`[OpenCode Proxy] Single-channel mode restored: ${activeChannel.name}`);
220
+ }
221
+
222
+ // 6. 广播状态更新(使用最新渠道列表,刷新前端缓存)
223
+ const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
193
224
  const updatedStatus = getOpenCodeProxyStatus();
194
- broadcastProxyState('opencode', updatedStatus, activeChannel, channels);
225
+ const { channels: latestChannels } = getChannels();
226
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
227
+ broadcastProxyState('opencode', updatedStatus, latestActiveChannel, latestChannels);
228
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
195
229
 
196
230
  res.json({
197
231
  success: true,
198
- message: `OpenCode proxy stopped${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
199
- port: proxyResult.port
232
+ message: `OpenCode proxy stopped${latestActiveChannel ? ' (channel: ' + latestActiveChannel.name + ')' : ''}`,
233
+ port: proxyResult.port,
234
+ restoredChannel: latestActiveChannel?.name || null
200
235
  });
201
236
  } catch (error) {
202
237
  console.error('[OpenCode Proxy] Error stopping proxy:', error);
@@ -212,6 +212,7 @@ router.post('/stop', async (req, res) => {
212
212
  try {
213
213
  // 1. 停止代理服务器
214
214
  const proxyResult = await stopProxyServer();
215
+ const activeChannelId = loadActiveChannelId();
215
216
 
216
217
  // 2. 恢复配置(优先从备份,否则选择权重最高的启用渠道)
217
218
  let restoredChannel = null;
@@ -229,6 +230,10 @@ router.post('/stop', async (req, res) => {
229
230
  ch.baseUrl === currentSettings.baseUrl && ch.apiKey === currentSettings.apiKey
230
231
  );
231
232
  }
233
+ // Fallback: use previously active channel id
234
+ if (!restoredChannel && activeChannelId) {
235
+ restoredChannel = channels.find(ch => ch.id === activeChannelId);
236
+ }
232
237
  // Fallback: use first enabled channel
233
238
  if (!restoredChannel) {
234
239
  restoredChannel = channels.find(ch => ch.enabled !== false) || channels[0];
@@ -236,7 +241,13 @@ router.post('/stop', async (req, res) => {
236
241
  } else {
237
242
  // 没有备份,选择权重最高的启用渠道
238
243
  const { getBestChannelForRestore, updateClaudeSettings } = require('../services/channels');
239
- restoredChannel = getBestChannelForRestore();
244
+ const channels = getAllChannels();
245
+ restoredChannel = activeChannelId
246
+ ? channels.find(ch => ch.id === activeChannelId)
247
+ : null;
248
+ if (!restoredChannel) {
249
+ restoredChannel = getBestChannelForRestore();
250
+ }
240
251
 
241
252
  if (restoredChannel) {
242
253
  updateClaudeSettings(restoredChannel.baseUrl, restoredChannel.apiKey);
@@ -244,11 +255,11 @@ router.post('/stop', async (req, res) => {
244
255
  }
245
256
  }
246
257
 
247
- // Enforce single-channel mode: disable all channels except the restored one
258
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
248
259
  if (restoredChannel) {
249
260
  const { applyChannelToSettings } = require('../services/channels');
250
261
  applyChannelToSettings(restoredChannel.id);
251
- console.log(`✅ Single-channel mode enforced: ${restoredChannel.name}`);
262
+ console.log(`✅ Single-channel mode restored: ${restoredChannel.name}`);
252
263
  }
253
264
 
254
265
  // 3. 删除备份文件和active-channel.json
@@ -270,7 +281,8 @@ router.post('/stop', async (req, res) => {
270
281
  const { broadcastProxyState } = require('../websocket-server');
271
282
  const updatedStatus = getProxyStatus();
272
283
  const channels = getAllChannels();
273
- broadcastProxyState('claude', updatedStatus, null, channels);
284
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
285
+ broadcastProxyState('claude', updatedStatus, activeChannel, channels);
274
286
 
275
287
  if (restoredChannel) {
276
288
  res.json({
@@ -278,9 +278,9 @@ function updateChannel(id, updates) {
278
278
  const proxyStatus = getProxyStatus();
279
279
  const isProxyRunning = proxyStatus.running;
280
280
 
281
- // Single-channel enforcement: enabling a channel disables all others
282
- // (applies regardless of proxy state user intent is to switch to this channel)
283
- if (nextChannel.enabled && !oldChannel.enabled) {
281
+ // Single-channel enforcement: enabling a channel disables all others ONLY when proxy is OFF
282
+ // When proxy is ON (dynamic switching), multiple channels can be enabled simultaneously
283
+ if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
284
284
  data.channels.forEach((ch, i) => {
285
285
  if (i !== index && ch.enabled) {
286
286
  ch.enabled = false;
@@ -299,9 +299,9 @@ function updateChannel(id, updates) {
299
299
 
300
300
  saveChannels(data);
301
301
 
302
- // Sync settings.json whenever a channel becomes enabled (proxy OFF: immediate switch;
303
- // proxy ON: pre-configures for when proxy stops)
304
- if (nextChannel.enabled) {
302
+ // Sync settings.json only when proxy is OFF.
303
+ // In dynamic switching mode, defer local config writes until proxy stop.
304
+ if (!isProxyRunning && nextChannel.enabled) {
305
305
  console.log(`[Settings-sync] Channel "${nextChannel.name}" enabled, syncing settings.json...`);
306
306
  updateClaudeSettingsWithModelConfig(nextChannel);
307
307
  }
@@ -252,9 +252,9 @@ function updateChannel(channelId, updates) {
252
252
  const proxyStatus = getCodexProxyStatus();
253
253
  const isProxyRunning = proxyStatus.running;
254
254
 
255
- // Single-channel enforcement: enabling a channel disables all others
256
- // (applies regardless of proxy state user intent is to switch to this channel)
257
- if (newChannel.enabled && !oldChannel.enabled) {
255
+ // Single-channel enforcement: enabling a channel disables all others ONLY when proxy is OFF
256
+ // When proxy is ON (dynamic switching), multiple channels can be enabled simultaneously
257
+ if (!isProxyRunning && newChannel.enabled && !oldChannel.enabled) {
258
258
  data.channels.forEach((ch, i) => {
259
259
  if (i !== index && ch.enabled) {
260
260
  ch.enabled = false;
@@ -273,9 +273,9 @@ function updateChannel(channelId, updates) {
273
273
 
274
274
  saveChannels(data);
275
275
 
276
- // Sync config.toml whenever a channel becomes enabled (proxy OFF: immediate switch;
277
- // proxy ON: pre-configures for when proxy stops)
278
- if (newChannel.enabled) {
276
+ // Sync config.toml only when proxy is OFF.
277
+ // In dynamic switching mode, defer local config writes until proxy stop.
278
+ if (!isProxyRunning && newChannel.enabled) {
279
279
  console.log(`[Codex Settings-sync] Channel "${newChannel.name}" enabled, syncing config.toml...`);
280
280
  applyChannelToSettings(channelId);
281
281
  }
@@ -564,9 +564,11 @@ function syncAllChannelEnvVars() {
564
564
  * 类似 Claude 的"写入配置"功能,将渠道设置为当前激活的 provider
565
565
  *
566
566
  * @param {string} channelId - 渠道 ID
567
+ * @param {Object} options - 可选参数
568
+ * @param {boolean} options.pruneProviders - 是否清理 model_providers 仅保留当前渠道
567
569
  * @returns {Object} 应用结果
568
570
  */
569
- function applyChannelToSettings(channelId) {
571
+ function applyChannelToSettings(channelId, options = {}) {
570
572
  const data = loadChannels();
571
573
  const channel = data.channels.find(c => c.id === channelId);
572
574
 
@@ -613,8 +615,11 @@ function applyChannelToSettings(channelId) {
613
615
  // 设置当前渠道为 model_provider
614
616
  config.model_provider = channel.providerKey;
615
617
 
616
- // 确保 model_providers 对象存在
617
- if (!config.model_providers) {
618
+ // 可选:清理 provider,关闭动态切换后只保留当前渠道配置
619
+ if (options.pruneProviders === true) {
620
+ config.model_providers = {};
621
+ } else if (!config.model_providers) {
622
+ // 默认兼容历史行为:保留已有 provider
618
623
  config.model_providers = {};
619
624
  }
620
625
 
@@ -664,6 +669,10 @@ ${tomlContent}`;
664
669
  delete auth[channel.envKey];
665
670
  }
666
671
 
672
+ // 清除 chatgpt token 认证字段,避免 Codex 优先用过期 token 而报 usage limit
673
+ delete auth.tokens;
674
+ delete auth.auth_mode;
675
+
667
676
  fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
668
677
 
669
678
  if (channel.apiKey && channel.envKey) {
@@ -312,6 +312,23 @@ function backupSettings() {
312
312
  }
313
313
  }
314
314
 
315
+ // 只删除备份文件,不恢复(保留当前配置)
316
+ function deleteBackup() {
317
+ try {
318
+ if (fs.existsSync(getConfigBackupPath())) {
319
+ fs.unlinkSync(getConfigBackupPath());
320
+ }
321
+ if (fs.existsSync(getAuthBackupPath())) {
322
+ fs.unlinkSync(getAuthBackupPath());
323
+ }
324
+ console.log('Codex backup files deleted');
325
+ return { success: true };
326
+ } catch (err) {
327
+ console.warn('Failed to delete backup files:', err.message);
328
+ return { success: false, error: err.message };
329
+ }
330
+ }
331
+
315
332
  // 恢复配置
316
333
  function restoreSettings() {
317
334
  try {
@@ -622,6 +639,7 @@ module.exports = {
622
639
  writeAuth,
623
640
  backupSettings,
624
641
  restoreSettings,
642
+ deleteBackup,
625
643
  setProxyConfig,
626
644
  isProxyConfig,
627
645
  getCurrentProxyPort,
@@ -572,7 +572,7 @@ function exportAllConfigsZip() {
572
572
  * @param {Object} options - 导入选项 { overwrite: boolean }
573
573
  * @returns {Object} 导入结果
574
574
  */
575
- function importConfigs(importData, options = {}) {
575
+ async function importConfigs(importData, options = {}) {
576
576
  const { overwrite = true } = options; // 默认覆盖模式
577
577
  const results = {
578
578
  configTemplates: { success: 0, failed: 0, skipped: 0 },
@@ -941,12 +941,16 @@ function importConfigs(importData, options = {}) {
941
941
  }
942
942
 
943
943
  // 导入 MCP Servers
944
- if (mcpServers && mcpServers.length > 0 && overwrite) {
944
+ const mcpServerList = Array.isArray(mcpServers)
945
+ ? mcpServers
946
+ : Object.values(mcpServers || {});
947
+
948
+ if (mcpServerList.length > 0 && overwrite) {
945
949
  try {
946
950
  const mcpService = require('./mcp-service');
947
- for (const server of mcpServers) {
951
+ for (const server of mcpServerList) {
948
952
  try {
949
- mcpService.saveServer(server);
953
+ await mcpService.saveServer(server, { syncPlatforms: false });
950
954
  results.mcpServers.success++;
951
955
  } catch (err) {
952
956
  console.error(`[ConfigImport] 导入 MCP Server 失败: ${server.name}`, err);