coding-tool-x 3.3.1 → 3.3.3

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 +81 -38
  15. package/src/server/api/config-export.js +4 -4
  16. package/src/server/api/gemini-proxy.js +81 -36
  17. package/src/server/api/opencode-proxy.js +60 -13
  18. package/src/server/api/proxy.js +37 -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.3",
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');
@@ -30,6 +31,17 @@ function sanitizeChannel(channel) {
30
31
  };
31
32
  }
32
33
 
34
+ function selectLatestEnabledChannel(channels) {
35
+ if (!Array.isArray(channels) || channels.length === 0) return null;
36
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
37
+ if (enabledChannels.length === 0) return null;
38
+ return enabledChannels.reduce((latest, current) => {
39
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
40
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
41
+ return currentTs > latestTs ? current : latest;
42
+ }, enabledChannels[0]);
43
+ }
44
+
33
45
  // 保存激活渠道ID
34
46
  function saveActiveChannelId(channelId) {
35
47
  ensureStorageDirMigrated();
@@ -41,6 +53,21 @@ function saveActiveChannelId(channelId) {
41
53
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
42
54
  }
43
55
 
56
+ function loadActiveChannelId() {
57
+ ensureStorageDirMigrated();
58
+ const filePath = PATHS.activeChannel.codex;
59
+ try {
60
+ if (fs.existsSync(filePath)) {
61
+ const content = fs.readFileSync(filePath, 'utf8');
62
+ const data = JSON.parse(content);
63
+ return data.activeChannelId || null;
64
+ }
65
+ } catch (error) {
66
+ console.error('[Codex Proxy] Error loading active channel ID:', error);
67
+ }
68
+ return null;
69
+ }
70
+
44
71
  function removeActiveChannelFile() {
45
72
  ensureStorageDirMigrated();
46
73
  const filePath = PATHS.activeChannel.codex;
@@ -84,9 +111,20 @@ router.post('/start', async (req, res) => {
84
111
  });
85
112
  }
86
113
 
87
- // 2. 获取当前启用的渠道(多渠道模式)
114
+ // 2. 获取当前启用的渠道(优先使用当前配置中正在使用的 provider)
88
115
  const enabledChannels = getEnabledChannels();
89
- const currentChannel = enabledChannels[0];
116
+ let currentChannel = null;
117
+ try {
118
+ const currentProvider = readConfig()?.model_provider;
119
+ if (currentProvider && currentProvider !== 'cc-proxy') {
120
+ currentChannel = enabledChannels.find(ch => ch.providerKey === currentProvider) || null;
121
+ }
122
+ } catch (err) {
123
+ // ignore and fallback
124
+ }
125
+ if (!currentChannel) {
126
+ currentChannel = enabledChannels[0] || null;
127
+ }
90
128
  if (!currentChannel) {
91
129
  return res.status(400).json({
92
130
  error: 'No enabled Codex channel found. Please create and enable a channel first.'
@@ -141,51 +179,56 @@ router.post('/start', async (req, res) => {
141
179
  // 停止代理
142
180
  router.post('/stop', async (req, res) => {
143
181
  try {
144
- // 1. 获取当前启用的渠道(多渠道模式)
182
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
145
183
  const { channels } = getChannels();
146
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
147
- const activeChannel = enabledChannels[0];
184
+ const activeChannelId = loadActiveChannelId();
185
+ let activeChannel = selectLatestEnabledChannel(channels);
186
+ if (!activeChannel && activeChannelId) {
187
+ activeChannel = channels.find(ch => ch.id === activeChannelId);
188
+ }
189
+ if (!activeChannel) {
190
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
191
+ activeChannel = enabledChannels[0] || channels[0] || null;
192
+ }
148
193
 
149
194
  // 2. 停止代理服务器
150
195
  const proxyResult = await stopCodexProxyServer();
196
+ const hadBackup = hasBackup();
197
+
198
+ // 3. 恢复单渠道模式
199
+ // 不恢复整个 config.toml 备份,避免两个问题:
200
+ // - 备份中的旧 auth_mode/tokens 会导致 Codex 用 chatgpt token 认证 → usage limit
201
+ // - 备份中的 mcp_servers 是旧状态,会覆盖用户在动态切换期间对 MCP 的修改
202
+ // 直接丢弃备份,由 applyChannelToSettings 从当前 config.toml 写入正确渠道配置
203
+ if (hadBackup) {
204
+ const { deleteBackup } = require('../services/codex-settings-manager');
205
+ deleteBackup();
206
+ console.log('[Codex Proxy] Discarded backup (MCP changes preserved)');
207
+ }
151
208
 
152
- // 3. 恢复原始配置
153
209
  const { broadcastProxyState } = require('../websocket-server');
154
210
 
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
- }
211
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
212
+ if (activeChannel) {
213
+ const { applyChannelToSettings } = require('../services/codex-channels');
214
+ applyChannelToSettings(activeChannel.id, { pruneProviders: true });
215
+ console.log(`[Codex Proxy] Single-channel mode restored: ${activeChannel.name}`);
216
+ }
165
217
 
166
- // 删除 active-channel.json
167
- removeActiveChannelFile();
218
+ // 删除 active-channel.json
219
+ removeActiveChannelFile();
168
220
 
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
- });
221
+ res.json({
222
+ success: true,
223
+ message: `Codex proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
224
+ port: proxyResult.port,
225
+ restoredChannel: activeChannel?.name
226
+ });
185
227
 
186
- const updatedStatus = getCodexProxyStatus();
187
- broadcastProxyState('codex', updatedStatus, activeChannel, channels);
188
- }
228
+ const updatedStatus = getCodexProxyStatus();
229
+ const { channels: latestChannels } = getChannels();
230
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
231
+ broadcastProxyState('codex', updatedStatus, latestActiveChannel, latestChannels);
189
232
  } catch (error) {
190
233
  console.error('[Codex Proxy] Error stopping proxy:', error);
191
234
  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');
@@ -24,6 +26,17 @@ function sanitizeChannel(channel) {
24
26
  return rest;
25
27
  }
26
28
 
29
+ function selectLatestEnabledChannel(channels) {
30
+ if (!Array.isArray(channels) || channels.length === 0) return null;
31
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
32
+ if (enabledChannels.length === 0) return null;
33
+ return enabledChannels.reduce((latest, current) => {
34
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
35
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
36
+ return currentTs > latestTs ? current : latest;
37
+ }, enabledChannels[0]);
38
+ }
39
+
27
40
  // 保存激活渠道ID
28
41
  function saveActiveChannelId(channelId) {
29
42
  ensureStorageDirMigrated();
@@ -35,6 +48,21 @@ function saveActiveChannelId(channelId) {
35
48
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
36
49
  }
37
50
 
51
+ function loadActiveChannelId() {
52
+ ensureStorageDirMigrated();
53
+ const filePath = PATHS.activeChannel.gemini;
54
+ try {
55
+ if (fs.existsSync(filePath)) {
56
+ const content = fs.readFileSync(filePath, 'utf8');
57
+ const data = JSON.parse(content);
58
+ return data.activeChannelId || null;
59
+ }
60
+ } catch (error) {
61
+ console.error('[Gemini Proxy] Error loading active channel ID:', error);
62
+ }
63
+ return null;
64
+ }
65
+
38
66
  function removeActiveChannelFile() {
39
67
  ensureStorageDirMigrated();
40
68
  const filePath = PATHS.activeChannel.gemini;
@@ -78,9 +106,23 @@ router.post('/start', async (req, res) => {
78
106
  });
79
107
  }
80
108
 
81
- // 2. 获取当前启用的渠道(多渠道模式)
109
+ // 2. 获取当前启用的渠道(优先使用当前 .env 对应的渠道)
82
110
  const enabledChannels = getEnabledChannels();
83
- const currentChannel = enabledChannels[0];
111
+ let currentChannel = null;
112
+ try {
113
+ const env = readEnv();
114
+ const baseUrl = env?.GOOGLE_GEMINI_BASE_URL;
115
+ const apiKey = env?.GEMINI_API_KEY;
116
+ const model = env?.GEMINI_MODEL;
117
+ currentChannel = enabledChannels.find(ch =>
118
+ ch.baseUrl === baseUrl && ch.apiKey === apiKey && (!model || ch.model === model)
119
+ ) || enabledChannels.find(ch => ch.baseUrl === baseUrl && ch.apiKey === apiKey) || null;
120
+ } catch (err) {
121
+ // ignore and fallback
122
+ }
123
+ if (!currentChannel) {
124
+ currentChannel = enabledChannels[0] || null;
125
+ }
84
126
  if (!currentChannel) {
85
127
  return res.status(400).json({
86
128
  error: 'No enabled Gemini channel found. Please create and enable a channel first.'
@@ -122,48 +164,51 @@ router.post('/start', async (req, res) => {
122
164
  // 停止代理
123
165
  router.post('/stop', async (req, res) => {
124
166
  try {
125
- // 1. 获取当前启用的渠道(多渠道模式)
167
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
126
168
  const { channels } = getChannels();
127
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
128
- const activeChannel = enabledChannels[0];
169
+ const activeChannelId = loadActiveChannelId();
170
+ let activeChannel = selectLatestEnabledChannel(channels);
171
+ if (!activeChannel && activeChannelId) {
172
+ activeChannel = channels.find(ch => ch.id === activeChannelId);
173
+ }
174
+ if (!activeChannel) {
175
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
176
+ activeChannel = enabledChannels[0] || channels[0] || null;
177
+ }
129
178
 
130
179
  // 2. 停止代理服务器
131
180
  const proxyResult = await stopGeminiProxyServer();
181
+ const hadBackup = hasBackup();
132
182
 
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
- });
183
+ // 3. 恢复单渠道模式
184
+ // 不恢复整个备份,避免覆盖用户在动态切换期间对 MCP 等配置的修改
185
+ if (hadBackup) {
186
+ deleteBackup();
187
+ console.log('[Gemini Proxy] Discarded backup (MCP changes preserved)');
162
188
  }
163
189
 
190
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
191
+ if (activeChannel) {
192
+ const { applyChannelToSettings } = require('../services/gemini-channels');
193
+ applyChannelToSettings(activeChannel.id);
194
+ console.log(`[Gemini Proxy] Single-channel mode restored: ${activeChannel.name}`);
195
+ }
196
+
197
+ // 删除 gemini-active-channel.json
198
+ removeActiveChannelFile();
199
+
200
+ res.json({
201
+ success: true,
202
+ message: `Gemini proxy stopped, settings restored${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
203
+ port: proxyResult.port,
204
+ restoredChannel: activeChannel?.name
205
+ });
206
+
207
+ const { broadcastProxyState } = require('../websocket-server');
164
208
  const proxyStatus = getGeminiProxyStatus();
165
209
  const { channels: latestChannels } = getChannels();
166
- broadcastProxyState('gemini', proxyStatus, activeChannel, latestChannels);
210
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
211
+ broadcastProxyState('gemini', proxyStatus, latestActiveChannel, latestChannels);
167
212
  } catch (error) {
168
213
  console.error('[Gemini Proxy] Error stopping proxy:', error);
169
214
  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');
@@ -29,6 +31,17 @@ function sanitizeChannel(channel) {
29
31
  };
30
32
  }
31
33
 
34
+ function selectLatestEnabledChannel(channels) {
35
+ if (!Array.isArray(channels) || channels.length === 0) return null;
36
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
37
+ if (enabledChannels.length === 0) return null;
38
+ return enabledChannels.reduce((latest, current) => {
39
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
40
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
41
+ return currentTs > latestTs ? current : latest;
42
+ }, enabledChannels[0]);
43
+ }
44
+
32
45
  // 保存激活渠道ID
33
46
  function saveActiveChannelId(channelId) {
34
47
  ensureStorageDirMigrated();
@@ -40,6 +53,21 @@ function saveActiveChannelId(channelId) {
40
53
  fs.writeFileSync(filePath, JSON.stringify({ activeChannelId: channelId }, null, 2), 'utf8');
41
54
  }
42
55
 
56
+ function loadActiveChannelId() {
57
+ ensureStorageDirMigrated();
58
+ const filePath = PATHS.activeChannel.opencode;
59
+ try {
60
+ if (fs.existsSync(filePath)) {
61
+ const content = fs.readFileSync(filePath, 'utf8');
62
+ const data = JSON.parse(content);
63
+ return data.activeChannelId || null;
64
+ }
65
+ } catch (error) {
66
+ console.error('[OpenCode Proxy] Error loading active channel ID:', error);
67
+ }
68
+ return null;
69
+ }
70
+
43
71
  // 删除激活渠道文件
44
72
  function removeActiveChannelFile() {
45
73
  ensureStorageDirMigrated();
@@ -171,32 +199,51 @@ router.post('/start', async (req, res) => {
171
199
  // 停止代理
172
200
  router.post('/stop', async (req, res) => {
173
201
  try {
174
- // 1. 获取当前渠道信息
202
+ // 1. 获取当前激活渠道(优先使用启动动态切换时记录的渠道ID)
175
203
  const { channels } = getChannels();
176
- const enabledChannels = channels.filter(ch => ch.enabled !== false);
177
- const activeChannel = enabledChannels[0];
204
+ const activeChannelId = loadActiveChannelId();
205
+ let activeChannel = selectLatestEnabledChannel(channels);
206
+ if (!activeChannel && activeChannelId) {
207
+ activeChannel = channels.find(ch => ch.id === activeChannelId);
208
+ }
209
+ if (!activeChannel) {
210
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
211
+ activeChannel = enabledChannels[0] || channels[0] || null;
212
+ }
178
213
 
179
214
  // 2. 停止代理服务器
180
215
  const proxyResult = await stopOpenCodeProxyServer();
216
+ const hadBackup = hasBackup();
181
217
 
182
218
  // 3. 删除激活渠道文件
183
219
  removeActiveChannelFile();
184
220
 
185
- // 4. 恢复原始配置
186
- if (hasBackup()) {
187
- restoreSettings();
188
- console.log('[OpenCode Proxy] Restored settings from backup');
221
+ // 4. 恢复单渠道模式
222
+ // 不恢复整个备份,避免覆盖用户在动态切换期间对 MCP 等配置的修改
223
+ if (hadBackup) {
224
+ deleteBackup();
225
+ console.log('[OpenCode Proxy] Discarded backup (MCP changes preserved)');
189
226
  }
190
227
 
191
- // 5. 广播状态更新
192
- const { broadcastProxyState } = require('../websocket-server');
228
+ // 5. 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
229
+ if (activeChannel) {
230
+ applyChannelToSettings(activeChannel.id);
231
+ console.log(`[OpenCode Proxy] Single-channel mode restored: ${activeChannel.name}`);
232
+ }
233
+
234
+ // 6. 广播状态更新(使用最新渠道列表,刷新前端缓存)
235
+ const { broadcastProxyState, broadcastSchedulerState } = require('../websocket-server');
193
236
  const updatedStatus = getOpenCodeProxyStatus();
194
- broadcastProxyState('opencode', updatedStatus, activeChannel, channels);
237
+ const { channels: latestChannels } = getChannels();
238
+ const latestActiveChannel = latestChannels.find(ch => ch.enabled !== false) || null;
239
+ broadcastProxyState('opencode', updatedStatus, latestActiveChannel, latestChannels);
240
+ broadcastSchedulerState('opencode', getSchedulerState('opencode'));
195
241
 
196
242
  res.json({
197
243
  success: true,
198
- message: `OpenCode proxy stopped${activeChannel ? ' (channel: ' + activeChannel.name + ')' : ''}`,
199
- port: proxyResult.port
244
+ message: `OpenCode proxy stopped${latestActiveChannel ? ' (channel: ' + latestActiveChannel.name + ')' : ''}`,
245
+ port: proxyResult.port,
246
+ restoredChannel: latestActiveChannel?.name || null
200
247
  });
201
248
  } catch (error) {
202
249
  console.error('[OpenCode Proxy] Error stopping proxy:', error);
@@ -27,6 +27,17 @@ function sanitizeChannelForResponse(channel) {
27
27
  };
28
28
  }
29
29
 
30
+ function selectLatestEnabledChannel(channels) {
31
+ if (!Array.isArray(channels) || channels.length === 0) return null;
32
+ const enabledChannels = channels.filter(ch => ch.enabled !== false);
33
+ if (enabledChannels.length === 0) return null;
34
+ return enabledChannels.reduce((latest, current) => {
35
+ const latestTs = Number(latest?.updatedAt || latest?.createdAt || 0);
36
+ const currentTs = Number(current?.updatedAt || current?.createdAt || 0);
37
+ return currentTs > latestTs ? current : latest;
38
+ }, enabledChannels[0]);
39
+ }
40
+
30
41
  // 保存激活渠道ID
31
42
  function saveActiveChannelId(channelId) {
32
43
  ensureStorageDirMigrated();
@@ -210,8 +221,12 @@ router.post('/start', async (req, res) => {
210
221
  // 停止代理
211
222
  router.post('/stop', async (req, res) => {
212
223
  try {
224
+ const channelsBeforeStop = getAllChannels();
225
+ const latestEnabledChannel = selectLatestEnabledChannel(channelsBeforeStop);
226
+
213
227
  // 1. 停止代理服务器
214
228
  const proxyResult = await stopProxyServer();
229
+ const activeChannelId = loadActiveChannelId();
215
230
 
216
231
  // 2. 恢复配置(优先从备份,否则选择权重最高的启用渠道)
217
232
  let restoredChannel = null;
@@ -229,6 +244,14 @@ router.post('/stop', async (req, res) => {
229
244
  ch.baseUrl === currentSettings.baseUrl && ch.apiKey === currentSettings.apiKey
230
245
  );
231
246
  }
247
+ // Fallback: keep latest enabled channel when leaving dynamic switching mode
248
+ if (!restoredChannel && latestEnabledChannel) {
249
+ restoredChannel = channels.find(ch => ch.id === latestEnabledChannel.id) || latestEnabledChannel;
250
+ }
251
+ // Fallback: use previously active channel id
252
+ if (!restoredChannel && activeChannelId) {
253
+ restoredChannel = channels.find(ch => ch.id === activeChannelId);
254
+ }
232
255
  // Fallback: use first enabled channel
233
256
  if (!restoredChannel) {
234
257
  restoredChannel = channels.find(ch => ch.enabled !== false) || channels[0];
@@ -236,7 +259,16 @@ router.post('/stop', async (req, res) => {
236
259
  } else {
237
260
  // 没有备份,选择权重最高的启用渠道
238
261
  const { getBestChannelForRestore, updateClaudeSettings } = require('../services/channels');
239
- restoredChannel = getBestChannelForRestore();
262
+ const channels = getAllChannels();
263
+ restoredChannel = latestEnabledChannel
264
+ ? channels.find(ch => ch.id === latestEnabledChannel.id)
265
+ : null;
266
+ if (!restoredChannel && activeChannelId) {
267
+ restoredChannel = channels.find(ch => ch.id === activeChannelId);
268
+ }
269
+ if (!restoredChannel) {
270
+ restoredChannel = getBestChannelForRestore();
271
+ }
240
272
 
241
273
  if (restoredChannel) {
242
274
  updateClaudeSettings(restoredChannel.baseUrl, restoredChannel.apiKey);
@@ -244,11 +276,11 @@ router.post('/stop', async (req, res) => {
244
276
  }
245
277
  }
246
278
 
247
- // Enforce single-channel mode: disable all channels except the restored one
279
+ // 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
248
280
  if (restoredChannel) {
249
281
  const { applyChannelToSettings } = require('../services/channels');
250
282
  applyChannelToSettings(restoredChannel.id);
251
- console.log(`✅ Single-channel mode enforced: ${restoredChannel.name}`);
283
+ console.log(`✅ Single-channel mode restored: ${restoredChannel.name}`);
252
284
  }
253
285
 
254
286
  // 3. 删除备份文件和active-channel.json
@@ -270,7 +302,8 @@ router.post('/stop', async (req, res) => {
270
302
  const { broadcastProxyState } = require('../websocket-server');
271
303
  const updatedStatus = getProxyStatus();
272
304
  const channels = getAllChannels();
273
- broadcastProxyState('claude', updatedStatus, null, channels);
305
+ const activeChannel = channels.find(ch => ch.enabled !== false) || null;
306
+ broadcastProxyState('claude', updatedStatus, activeChannel, channels);
274
307
 
275
308
  if (restoredChannel) {
276
309
  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
  }