coding-tool-x 3.3.0 → 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.
- package/dist/web/assets/{Analytics-C1ksHds9.js → Analytics-DtR00OYP.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DPalVYLW.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-BVmCjtCx.js → PluginManager-DsJ1KtNr.js} +1 -1
- package/dist/web/assets/{ProjectList-BkLjuHNf.js → ProjectList-CzTJaBJb.js} +1 -1
- package/dist/web/assets/{SessionList-D0TRger4.js → SessionList-D1ovPZ0I.js} +1 -1
- package/dist/web/assets/{SkillManager-CWz7fdXo.js → SkillManager-DqpDTc2c.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D0S_Mc6X.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 +69 -38
- package/src/server/api/config-export.js +4 -4
- package/src/server/api/gemini-proxy.js +69 -36
- package/src/server/api/opencode-channels.js +28 -1
- package/src/server/api/opencode-proxy.js +48 -13
- package/src/server/api/proxy.js +16 -4
- package/src/server/proxy-server.js +0 -8
- 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-DuPOICVF.css +0 -1
- package/dist/web/assets/Home-gWHWIqGE.js +0 -1
- package/dist/web/assets/index-_Wng07JI.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');
|
|
@@ -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
|
-
|
|
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
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
167
|
-
|
|
206
|
+
// 删除 active-channel.json
|
|
207
|
+
removeActiveChannelFile();
|
|
168
208
|
|
|
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
|
-
});
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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 });
|
|
@@ -131,6 +131,32 @@ module.exports = (config) => {
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
/**
|
|
135
|
+
* POST /api/opencode/channels/probe-models
|
|
136
|
+
* 用临时配置(新建渠道时)获取模型列表,无需 channelId
|
|
137
|
+
*/
|
|
138
|
+
router.post('/probe-models', async (req, res) => {
|
|
139
|
+
try {
|
|
140
|
+
const { baseUrl, apiKey, gatewaySourceType } = req.body;
|
|
141
|
+
if (!baseUrl) {
|
|
142
|
+
return res.status(400).json({ error: 'baseUrl is required' });
|
|
143
|
+
}
|
|
144
|
+
const tempChannel = { baseUrl, apiKey: apiKey || '', gatewaySourceType: gatewaySourceType || 'codex' };
|
|
145
|
+
const gst = resolveGatewaySourceType(tempChannel);
|
|
146
|
+
const listResult = await fetchModelsFromProvider(tempChannel, gst, { useV1ModelsEndpoint: true, forceRefresh: true });
|
|
147
|
+
const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
|
|
148
|
+
res.json({
|
|
149
|
+
models: listedModels,
|
|
150
|
+
supported: listedModels.length > 0,
|
|
151
|
+
error: listedModels.length > 0 ? null : (listResult.error || '未返回可用模型列表'),
|
|
152
|
+
errorHint: listedModels.length > 0 ? null : (listResult.errorHint || '请手动填写模型名称')
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[OpenCode Channels API] Error probing models:', error);
|
|
156
|
+
res.status(500).json({ error: 'Failed to probe models' });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
134
160
|
/**
|
|
135
161
|
* GET /api/opencode/channels/:channelId/models
|
|
136
162
|
* 获取渠道可用模型列表
|
|
@@ -145,9 +171,10 @@ module.exports = (config) => {
|
|
|
145
171
|
return res.status(404).json({ error: 'Channel not found' });
|
|
146
172
|
}
|
|
147
173
|
|
|
174
|
+
const forceRefresh = req.query.forceRefresh === 'true';
|
|
148
175
|
const gatewaySourceType = resolveGatewaySourceType(channel);
|
|
149
176
|
const preferredModels = collectChannelPreferredModels(channel);
|
|
150
|
-
const listResult = await fetchModelsFromProvider(channel, gatewaySourceType);
|
|
177
|
+
const listResult = await fetchModelsFromProvider(channel, gatewaySourceType, { useV1ModelsEndpoint: true, forceRefresh });
|
|
151
178
|
const listedModels = Array.isArray(listResult.models) ? uniqueModels(listResult.models) : [];
|
|
152
179
|
const shouldProbeByDefault = !!listResult.disabledByConfig;
|
|
153
180
|
let result;
|
|
@@ -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
|
|
177
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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${
|
|
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);
|
package/src/server/api/proxy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
258
|
+
// 停止动态切换后回到单渠道模式:保留激活渠道,禁用其他渠道
|
|
248
259
|
if (restoredChannel) {
|
|
249
260
|
const { applyChannelToSettings } = require('../services/channels');
|
|
250
261
|
applyChannelToSettings(restoredChannel.id);
|
|
251
|
-
console.log(`✅ Single-channel mode
|
|
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
|
-
|
|
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({
|
|
@@ -328,14 +328,6 @@ async function startProxyServer(options = {}) {
|
|
|
328
328
|
second: '2-digit'
|
|
329
329
|
});
|
|
330
330
|
const requestSnapshot = serializeFullClaudeRequest(req);
|
|
331
|
-
broadcastLog({
|
|
332
|
-
type: 'action',
|
|
333
|
-
action: 'claude_request_received',
|
|
334
|
-
time,
|
|
335
|
-
channel: channel.name,
|
|
336
|
-
source: 'claude',
|
|
337
|
-
requestSummary: buildClaudeRequestSummary(req, sessionId)
|
|
338
|
-
});
|
|
339
331
|
persistClaudeRequestSnapshot({
|
|
340
332
|
timestamp: Date.now(),
|
|
341
333
|
source: 'claude',
|
|
@@ -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
|
}
|
|
@@ -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
|
-
//
|
|
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
|
|
277
|
-
//
|
|
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
|
-
//
|
|
617
|
-
if (
|
|
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) {
|