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.
- package/dist/web/assets/{Analytics-BskCbia_.js → Analytics-DtR00OYP.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-B4X3rgfY.js → ConfigTemplates-DWiSFOp5.js} +1 -1
- package/dist/web/assets/Home-BsSioaaB.css +1 -0
- package/dist/web/assets/Home-DUu2mGb6.js +1 -0
- package/dist/web/assets/{PluginManager-D_LoULGH.js → PluginManager-DsJ1KtNr.js} +1 -1
- package/dist/web/assets/{ProjectList-DiV4Qwa1.js → ProjectList-CzTJaBJb.js} +1 -1
- package/dist/web/assets/{SessionList-B24o0wiX.js → SessionList-D1ovPZ0I.js} +1 -1
- package/dist/web/assets/{SkillManager-B9Rnuaig.js → SkillManager-DqpDTc2c.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-BkL2l5J9.js → WorkspaceManager-Dj28-3G5.js} +1 -1
- package/dist/web/assets/index-CaKktouI.js +2 -0
- package/dist/web/assets/{index-C5j22icm.css → index-DZjDFGqR.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/codex-proxy.js +81 -38
- package/src/server/api/config-export.js +4 -4
- package/src/server/api/gemini-proxy.js +81 -36
- package/src/server/api/opencode-proxy.js +60 -13
- package/src/server/api/proxy.js +37 -4
- package/src/server/services/channels.js +6 -6
- package/src/server/services/codex-channels.js +18 -9
- package/src/server/services/codex-settings-manager.js +18 -0
- package/src/server/services/config-export-service.js +8 -4
- package/src/server/services/gemini-channels.js +19 -11
- package/src/server/services/gemini-settings-manager.js +14 -0
- package/src/server/services/mcp-service.js +40 -15
- package/src/server/services/opencode-channels.js +18 -0
- package/src/server/services/opencode-sessions.js +25 -1
- package/src/server/services/opencode-settings-manager.js +16 -0
- package/dist/web/assets/Home-DHYMMKOU.js +0 -1
- package/dist/web/assets/Home-DuPOICVF.css +0 -1
- package/dist/web/assets/index-ZttxvTKw.js +0 -2
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
@@ -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
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
167
|
-
|
|
218
|
+
// 删除 active-channel.json
|
|
219
|
+
removeActiveChannelFile();
|
|
168
220
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
console.log('[Gemini Proxy]
|
|
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
|
-
|
|
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
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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${
|
|
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);
|
package/src/server/api/proxy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
279
|
+
// 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
|
|
248
280
|
if (restoredChannel) {
|
|
249
281
|
const { applyChannelToSettings } = require('../services/channels');
|
|
250
282
|
applyChannelToSettings(restoredChannel.id);
|
|
251
|
-
console.log(`✅ Single-channel mode
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
303
|
-
//
|
|
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
|
}
|