coding-tool-x 3.3.7 → 3.3.9
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/CHANGELOG.md +20 -0
- package/README.md +253 -326
- package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-D6LzK9hk.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-BUDYuxRi.js} +1 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
- package/dist/web/assets/Home-D7KX7iF8.js +1 -0
- package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-DTgQ--vB.js} +1 -1
- package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DMCiGmCT.js} +1 -1
- package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-CRBsdVRe.js} +1 -1
- package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-DMwx2Q4k.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-DapB4ljL.js} +1 -1
- package/dist/web/assets/{icons-B29onFfZ.js → icons-B5Pl4lrD.js} +1 -1
- package/dist/web/assets/index-CL-qpoJ_.js +2 -0
- package/dist/web/assets/index-D_5dRFOL.css +1 -0
- package/dist/web/assets/{markdown-C9MYpaSi.js → markdown-DyTJGI4N.js} +1 -1
- package/dist/web/assets/{naive-ui-CxpuzdjU.js → naive-ui-Bdxp09n2.js} +1 -1
- package/dist/web/assets/{vendors-DMjSfzlv.js → vendors-CKPV1OAU.js} +2 -2
- package/dist/web/assets/{vue-vendor-DET08QYg.js → vue-vendor-3bf-fPGP.js} +1 -1
- package/dist/web/index.html +7 -7
- package/docs/home.png +0 -0
- package/package.json +14 -5
- package/src/commands/daemon.js +3 -2
- package/src/commands/security.js +1 -2
- package/src/commands/toggle-proxy.js +100 -5
- package/src/config/paths.js +718 -90
- package/src/server/api/agents.js +1 -1
- package/src/server/api/channels.js +9 -0
- package/src/server/api/claude-hooks.js +13 -8
- package/src/server/api/codex-channels.js +9 -0
- package/src/server/api/codex-proxy.js +27 -15
- package/src/server/api/gemini-proxy.js +22 -11
- package/src/server/api/hooks.js +45 -0
- package/src/server/api/oauth-credentials.js +163 -0
- package/src/server/api/opencode-proxy.js +22 -10
- package/src/server/api/plugins.js +2 -1
- package/src/server/api/proxy.js +39 -44
- package/src/server/api/skills.js +91 -13
- package/src/server/api/ui-config.js +5 -0
- package/src/server/codex-proxy-server.js +90 -70
- package/src/server/gemini-proxy-server.js +107 -88
- package/src/server/index.js +2 -0
- package/src/server/opencode-proxy-server.js +381 -225
- package/src/server/proxy-server.js +86 -60
- package/src/server/services/alias.js +3 -3
- package/src/server/services/channels.js +21 -24
- package/src/server/services/codex-channels.js +158 -255
- package/src/server/services/codex-config.js +2 -5
- package/src/server/services/codex-env-manager.js +423 -0
- package/src/server/services/codex-settings-manager.js +21 -357
- package/src/server/services/codex-statistics-service.js +3 -27
- package/src/server/services/config-export-service.js +43 -9
- package/src/server/services/config-registry-service.js +3 -2
- package/src/server/services/config-sync-manager.js +1 -1
- package/src/server/services/favorites.js +4 -3
- package/src/server/services/gemini-channels.js +14 -12
- package/src/server/services/gemini-statistics-service.js +3 -25
- package/src/server/services/mcp-service.js +35 -19
- package/src/server/services/model-detector.js +4 -3
- package/src/server/services/native-keychain.js +243 -0
- package/src/server/services/native-oauth-adapters.js +891 -0
- package/src/server/services/network-access.js +39 -1
- package/src/server/services/notification-hooks.js +951 -0
- package/src/server/services/oauth-credentials-service.js +786 -0
- package/src/server/services/oauth-utils.js +49 -0
- package/src/server/services/opencode-channels.js +19 -15
- package/src/server/services/opencode-sessions.js +2 -2
- package/src/server/services/opencode-settings-manager.js +169 -16
- package/src/server/services/opencode-statistics-service.js +3 -27
- package/src/server/services/plugins-service.js +115 -15
- package/src/server/services/prompts-service.js +2 -3
- package/src/server/services/proxy-log-helper.js +242 -0
- package/src/server/services/proxy-runtime.js +6 -4
- package/src/server/services/repo-scanner-base.js +12 -4
- package/src/server/services/request-logger.js +7 -7
- package/src/server/services/security-config.js +4 -4
- package/src/server/services/session-cache.js +2 -2
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/settings-manager.js +13 -0
- package/src/server/services/skill-service.js +867 -368
- package/src/server/services/statistics-service.js +5 -5
- package/src/server/services/ui-config.js +4 -3
- package/src/server/services/workspace-service.js +1 -1
- package/src/server/websocket-server.js +5 -4
- package/dist/web/assets/Home-BsSioaaB.css +0 -1
- package/dist/web/assets/Home-obifg_9E.js +0 -1
- package/dist/web/assets/index-C7LPdVsN.js +0 -2
- package/dist/web/assets/index-eEmjZKWP.css +0 -1
- package/docs/bannel.png +0 -0
- package/docs/model-redirection.md +0 -251
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const crypto = require('crypto');
|
|
5
4
|
const toml = require('toml');
|
|
6
5
|
const tomlStringify = require('@iarna/toml').stringify;
|
|
7
|
-
const {
|
|
6
|
+
const { PATHS } = require('../../config/paths');
|
|
8
7
|
const { getCodexDir } = require('./codex-config');
|
|
9
|
-
const {
|
|
8
|
+
const { isProxyConfig } = require('./codex-settings-manager');
|
|
9
|
+
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
10
|
+
const { syncCodexUserEnvironment } = require('./codex-env-manager');
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
+
const CODEX_PROXY_ENV_KEY = 'CC_PROXY_KEY';
|
|
13
|
+
const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Codex 渠道管理服务(多渠道架构)
|
|
15
17
|
*
|
|
16
18
|
* Codex 配置结构:
|
|
17
19
|
* - config.toml: 主配置,包含 model_provider 和各提供商配置
|
|
18
|
-
* -
|
|
20
|
+
* - 用户级环境变量: env_key 对应的 API Key 存储
|
|
19
21
|
* - 我们的 codex-channels.json: 完整渠道信息(用于管理)
|
|
20
22
|
*
|
|
21
23
|
* 多渠道模式:
|
|
@@ -31,13 +33,28 @@ function normalizeGatewaySourceType(value, fallback = 'codex') {
|
|
|
31
33
|
return fallback;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
|
|
37
|
+
const envMap = {};
|
|
38
|
+
|
|
39
|
+
for (const channel of channels) {
|
|
40
|
+
if (!channel?.envKey || !channel?.apiKey) continue;
|
|
41
|
+
envMap[channel.envKey] = channel.apiKey;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (includeProxyKey) {
|
|
45
|
+
envMap[CODEX_PROXY_ENV_KEY] = CODEX_PROXY_ENV_VALUE;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return envMap;
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
// 获取渠道存储文件路径
|
|
35
52
|
function getChannelsFilePath() {
|
|
36
|
-
const
|
|
37
|
-
if (!fs.existsSync(
|
|
38
|
-
fs.mkdirSync(
|
|
53
|
+
const channelsDir = path.dirname(PATHS.channels.codex);
|
|
54
|
+
if (!fs.existsSync(channelsDir)) {
|
|
55
|
+
fs.mkdirSync(channelsDir, { recursive: true });
|
|
39
56
|
}
|
|
40
|
-
return
|
|
57
|
+
return PATHS.channels.codex;
|
|
41
58
|
}
|
|
42
59
|
|
|
43
60
|
// 读取所有渠道(从我们的存储文件)
|
|
@@ -102,11 +119,11 @@ function initializeFromConfig() {
|
|
|
102
119
|
for (const [providerKey, providerConfig] of Object.entries(config.model_providers)) {
|
|
103
120
|
// env_key 优先级:配置的 env_key > PROVIDER_API_KEY > OPENAI_API_KEY
|
|
104
121
|
let envKey = providerConfig.env_key || `${providerKey.toUpperCase()}_API_KEY`;
|
|
105
|
-
let apiKey = auth[envKey] || '';
|
|
122
|
+
let apiKey = process.env[envKey] || auth[envKey] || '';
|
|
106
123
|
|
|
107
124
|
// 如果没找到,尝试 OPENAI_API_KEY 作为通用 fallback
|
|
108
|
-
if (!apiKey && auth['OPENAI_API_KEY']) {
|
|
109
|
-
apiKey = auth['OPENAI_API_KEY'];
|
|
125
|
+
if (!apiKey && (process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'])) {
|
|
126
|
+
apiKey = process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'];
|
|
110
127
|
envKey = 'OPENAI_API_KEY';
|
|
111
128
|
}
|
|
112
129
|
|
|
@@ -128,14 +145,6 @@ function initializeFromConfig() {
|
|
|
128
145
|
createdAt: Date.now(),
|
|
129
146
|
updatedAt: Date.now()
|
|
130
147
|
});
|
|
131
|
-
|
|
132
|
-
// 自动注入环境变量(从 Codex 迁移过来时使用)
|
|
133
|
-
if (apiKey && envKey) {
|
|
134
|
-
const injectResult = injectEnvToShell(envKey, apiKey);
|
|
135
|
-
if (injectResult.success) {
|
|
136
|
-
console.log(`[Codex Channels] Environment variable ${envKey} injected during initialization`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
148
|
}
|
|
140
149
|
}
|
|
141
150
|
|
|
@@ -158,6 +167,86 @@ function saveChannels(data) {
|
|
|
158
167
|
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
159
168
|
}
|
|
160
169
|
|
|
170
|
+
function getDefaultCodexConfig() {
|
|
171
|
+
return {
|
|
172
|
+
model: 'gpt-4',
|
|
173
|
+
model_reasoning_effort: 'high',
|
|
174
|
+
model_reasoning_summary_format: 'experimental',
|
|
175
|
+
network_access: 'enabled',
|
|
176
|
+
disable_response_storage: false,
|
|
177
|
+
show_raw_agent_reasoning: true
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cloneConfigValue(value) {
|
|
182
|
+
return JSON.parse(JSON.stringify(value));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readCodexConfigOrThrow(configPath, fallbackConfig = {}) {
|
|
186
|
+
if (!fs.existsSync(configPath)) {
|
|
187
|
+
return cloneConfigValue(fallbackConfig);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return toml.parse(content);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
throw new Error(`Failed to parse existing config.toml: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function writeTextAtomic(filePath, content) {
|
|
200
|
+
const dirPath = path.dirname(filePath);
|
|
201
|
+
if (!fs.existsSync(dirPath)) {
|
|
202
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
209
|
+
fs.renameSync(tempPath, filePath);
|
|
210
|
+
} finally {
|
|
211
|
+
if (fs.existsSync(tempPath)) {
|
|
212
|
+
try {
|
|
213
|
+
fs.unlinkSync(tempPath);
|
|
214
|
+
} catch {
|
|
215
|
+
// ignore temp cleanup errors
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function writeAnnotatedCodexConfig(configPath, config, headerLines = []) {
|
|
222
|
+
const tomlContent = tomlStringify(config);
|
|
223
|
+
const prefix = headerLines.length > 0 ? `${headerLines.join('\n')}\n\n` : '';
|
|
224
|
+
writeTextAtomic(configPath, `${prefix}${tomlContent}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getManagedProviderKeys(channels = []) {
|
|
228
|
+
const keys = new Set(['cc-proxy']);
|
|
229
|
+
for (const channel of channels) {
|
|
230
|
+
if (channel?.providerKey) {
|
|
231
|
+
keys.add(channel.providerKey);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return keys;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function pruneManagedProviders(existingProviders = {}, currentProviderKey, channels = []) {
|
|
238
|
+
const managedProviderKeys = getManagedProviderKeys(channels);
|
|
239
|
+
const preservedProviders = {};
|
|
240
|
+
|
|
241
|
+
for (const [providerKey, providerConfig] of Object.entries(existingProviders)) {
|
|
242
|
+
if (!managedProviderKeys.has(providerKey) || providerKey === currentProviderKey) {
|
|
243
|
+
preservedProviders[providerKey] = providerConfig;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return preservedProviders;
|
|
248
|
+
}
|
|
249
|
+
|
|
161
250
|
// 获取所有渠道
|
|
162
251
|
function getChannels() {
|
|
163
252
|
const data = loadChannels();
|
|
@@ -201,16 +290,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
|
|
|
201
290
|
|
|
202
291
|
data.channels.push(newChannel);
|
|
203
292
|
saveChannels(data);
|
|
204
|
-
|
|
205
|
-
// 注入该渠道的环境变量(用于直接使用 codex 命令)
|
|
206
|
-
if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
|
|
207
|
-
const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
|
|
208
|
-
if (injectResult.success) {
|
|
209
|
-
console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
|
|
210
|
-
} else {
|
|
211
|
-
console.warn(`[Codex Channels] Failed to inject ${envKey}: ${injectResult.error}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
293
|
+
syncAllChannelEnvVars(data.channels);
|
|
214
294
|
|
|
215
295
|
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
216
296
|
// writeCodexConfigForMultiChannel(data.channels);
|
|
@@ -266,14 +346,6 @@ function updateChannel(channelId, updates) {
|
|
|
266
346
|
console.log(`[Codex Single-channel mode] Enabled "${newChannel.name}", disabled all others`);
|
|
267
347
|
}
|
|
268
348
|
|
|
269
|
-
// Prevent disabling last enabled channel when proxy is OFF
|
|
270
|
-
if (!isProxyRunning && !newChannel.enabled && oldChannel.enabled) {
|
|
271
|
-
const enabledCount = data.channels.filter(ch => ch.enabled).length;
|
|
272
|
-
if (enabledCount === 0) {
|
|
273
|
-
throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
349
|
saveChannels(data);
|
|
278
350
|
|
|
279
351
|
// Sync config.toml only when proxy is OFF.
|
|
@@ -283,32 +355,7 @@ function updateChannel(channelId, updates) {
|
|
|
283
355
|
applyChannelToSettings(channelId);
|
|
284
356
|
}
|
|
285
357
|
|
|
286
|
-
|
|
287
|
-
// 如果 envKey 或 apiKey 变化,需要更新环境变量
|
|
288
|
-
const oldEnvKey = oldChannel.envKey;
|
|
289
|
-
const newEnvKey = newChannel.envKey;
|
|
290
|
-
const newApiKey = newChannel.apiKey;
|
|
291
|
-
const shouldRemoveOldEnv =
|
|
292
|
-
!!oldEnvKey && (
|
|
293
|
-
oldEnvKey !== newEnvKey ||
|
|
294
|
-
!newApiKey ||
|
|
295
|
-
newChannel.enabled === false
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
// 禁用或 key 变化时都要清理旧环境变量,避免残留
|
|
299
|
-
if (shouldRemoveOldEnv) {
|
|
300
|
-
const removeResult = removeEnvFromShell(oldEnvKey);
|
|
301
|
-
if (removeResult.success) {
|
|
302
|
-
console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (newChannel.enabled !== false && newApiKey && newEnvKey) {
|
|
307
|
-
const injectResult = injectEnvToShell(newEnvKey, newApiKey);
|
|
308
|
-
if (injectResult.success) {
|
|
309
|
-
console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
358
|
+
syncAllChannelEnvVars(data.channels);
|
|
312
359
|
|
|
313
360
|
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
314
361
|
// writeCodexConfigForMultiChannel(data.channels);
|
|
@@ -328,16 +375,7 @@ async function deleteChannel(channelId) {
|
|
|
328
375
|
const deletedChannel = data.channels[index];
|
|
329
376
|
data.channels.splice(index, 1);
|
|
330
377
|
saveChannels(data);
|
|
331
|
-
|
|
332
|
-
// 从 shell 配置文件移除该渠道的环境变量
|
|
333
|
-
if (deletedChannel.envKey) {
|
|
334
|
-
const removeResult = removeEnvFromShell(deletedChannel.envKey);
|
|
335
|
-
if (removeResult.success) {
|
|
336
|
-
console.log(`[Codex Channels] Environment variable ${deletedChannel.envKey} removed`);
|
|
337
|
-
} else {
|
|
338
|
-
console.warn(`[Codex Channels] Failed to remove ${deletedChannel.envKey}: ${removeResult.error}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
378
|
+
syncAllChannelEnvVars(data.channels);
|
|
341
379
|
|
|
342
380
|
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
343
381
|
// writeCodexConfigForMultiChannel(data.channels);
|
|
@@ -361,41 +399,20 @@ function writeCodexConfigForMultiChannel(allChannels) {
|
|
|
361
399
|
}
|
|
362
400
|
|
|
363
401
|
const configPath = path.join(codexDir, 'config.toml');
|
|
364
|
-
const authPath = path.join(codexDir, 'auth.json');
|
|
365
402
|
|
|
366
403
|
// 读取现有配置,保留所有现有字段(特别是 mcp_servers, projects 等)
|
|
404
|
+
const defaultConfig = getDefaultCodexConfig();
|
|
405
|
+
const parsedConfig = readCodexConfigOrThrow(configPath, defaultConfig);
|
|
367
406
|
let config = {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
407
|
+
...parsedConfig,
|
|
408
|
+
model: parsedConfig.model || defaultConfig.model,
|
|
409
|
+
model_reasoning_effort: parsedConfig.model_reasoning_effort || defaultConfig.model_reasoning_effort,
|
|
410
|
+
model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || defaultConfig.model_reasoning_summary_format,
|
|
411
|
+
network_access: parsedConfig.network_access || defaultConfig.network_access,
|
|
412
|
+
disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : defaultConfig.disable_response_storage,
|
|
413
|
+
show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : defaultConfig.show_raw_agent_reasoning
|
|
374
414
|
};
|
|
375
415
|
|
|
376
|
-
if (fs.existsSync(configPath)) {
|
|
377
|
-
try {
|
|
378
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
379
|
-
const parsedConfig = toml.parse(content);
|
|
380
|
-
|
|
381
|
-
// 深度合并,保留原有的所有配置
|
|
382
|
-
config = {
|
|
383
|
-
...parsedConfig,
|
|
384
|
-
// 只覆盖这些字段
|
|
385
|
-
model: parsedConfig.model || config.model,
|
|
386
|
-
model_reasoning_effort: parsedConfig.model_reasoning_effort || config.model_reasoning_effort,
|
|
387
|
-
model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || config.model_reasoning_summary_format,
|
|
388
|
-
network_access: parsedConfig.network_access || config.network_access,
|
|
389
|
-
disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : config.disable_response_storage,
|
|
390
|
-
show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : config.show_raw_agent_reasoning,
|
|
391
|
-
// mcp_servers 和 projects 会从 parsedConfig 自动继承
|
|
392
|
-
// model_provider 会根据动态切换情况决定是否更新
|
|
393
|
-
};
|
|
394
|
-
} catch (err) {
|
|
395
|
-
// ignore read error, use defaults
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
416
|
// 判断是否已启用动态切换
|
|
400
417
|
const isProxyMode = config.model_provider === 'cc-proxy';
|
|
401
418
|
const existingProviders = (config && typeof config.model_providers === 'object') ? config.model_providers : {};
|
|
@@ -441,51 +458,15 @@ function writeCodexConfigForMultiChannel(allChannels) {
|
|
|
441
458
|
}
|
|
442
459
|
}
|
|
443
460
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
fs.writeFileSync(configPath, annotatedContent, 'utf8');
|
|
455
|
-
} catch (err) {
|
|
456
|
-
console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
|
|
457
|
-
// 降级处理:如果 tomlStringify 失败,使用手工拼接(但这样会丢失注释)
|
|
458
|
-
const fallbackContent = JSON.stringify(config, null, 2);
|
|
459
|
-
fs.writeFileSync(configPath, fallbackContent, 'utf8');
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// 更新 auth.json
|
|
463
|
-
let auth = {};
|
|
464
|
-
if (fs.existsSync(authPath)) {
|
|
465
|
-
try {
|
|
466
|
-
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
467
|
-
} catch (err) {
|
|
468
|
-
console.warn('[Codex Channels] Failed to read auth.json, creating new');
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// 更新所有渠道的 API Key
|
|
473
|
-
for (const channel of allChannels) {
|
|
474
|
-
if (channel.envKey && !channel.apiKey) {
|
|
475
|
-
delete auth[channel.envKey];
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
for (const channel of allChannels) {
|
|
480
|
-
if (channel.apiKey) {
|
|
481
|
-
auth[channel.envKey] = channel.apiKey;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
486
|
-
|
|
487
|
-
// 注意:环境变量注入在 createChannel 和 updateChannel 时已经处理
|
|
488
|
-
// 这里不再重复注入,避免多次写入 shell 配置文件
|
|
461
|
+
writeAnnotatedCodexConfig(configPath, config, [
|
|
462
|
+
'# Codex Configuration',
|
|
463
|
+
'# Managed by Coding-Tool',
|
|
464
|
+
'# WARNING: MCP servers and projects are preserved automatically'
|
|
465
|
+
]);
|
|
466
|
+
syncCodexUserEnvironment(
|
|
467
|
+
buildManagedCodexEnvMap(allChannels, { includeProxyKey: isProxyMode }),
|
|
468
|
+
{ replace: true }
|
|
469
|
+
);
|
|
489
470
|
}
|
|
490
471
|
|
|
491
472
|
// 获取所有启用的渠道(供调度器使用)
|
|
@@ -523,43 +504,12 @@ function saveChannelOrder(order) {
|
|
|
523
504
|
* 确保用户可以直接使用 codex 命令而无需手动设置环境变量
|
|
524
505
|
* 这个函数会在服务启动时自动调用
|
|
525
506
|
*/
|
|
526
|
-
function syncAllChannelEnvVars() {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
return { success: true, synced: 0 };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
let syncedCount = 0;
|
|
536
|
-
const results = [];
|
|
537
|
-
|
|
538
|
-
for (const channel of channels) {
|
|
539
|
-
if (!channel.envKey) continue;
|
|
540
|
-
|
|
541
|
-
const shouldInject = channel.enabled !== false && !!channel.apiKey;
|
|
542
|
-
if (shouldInject) {
|
|
543
|
-
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
544
|
-
if (injectResult.success) {
|
|
545
|
-
syncedCount++;
|
|
546
|
-
results.push({ envKey: channel.envKey, success: true });
|
|
547
|
-
} else {
|
|
548
|
-
results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
|
|
549
|
-
}
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 清理已停用或缺失 key 的渠道环境变量,避免残留
|
|
554
|
-
removeEnvFromShell(channel.envKey);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
|
|
558
|
-
return { success: true, synced: syncedCount, results };
|
|
559
|
-
} catch (err) {
|
|
560
|
-
console.error('[Codex Channels] Failed to sync env vars:', err);
|
|
561
|
-
return { success: false, error: err.message };
|
|
562
|
-
}
|
|
507
|
+
function syncAllChannelEnvVars(channels = null) {
|
|
508
|
+
const data = channels ? { channels } : loadChannels();
|
|
509
|
+
return syncCodexUserEnvironment(
|
|
510
|
+
buildManagedCodexEnvMap(data.channels || [], { includeProxyKey: isProxyConfig() }),
|
|
511
|
+
{ replace: true }
|
|
512
|
+
);
|
|
563
513
|
}
|
|
564
514
|
|
|
565
515
|
/**
|
|
@@ -584,6 +534,7 @@ function applyChannelToSettings(channelId, options = {}) {
|
|
|
584
534
|
ch.enabled = ch.id === channelId;
|
|
585
535
|
});
|
|
586
536
|
saveChannels(data);
|
|
537
|
+
clearNativeOAuth('codex');
|
|
587
538
|
|
|
588
539
|
const codexDir = getCodexDir();
|
|
589
540
|
|
|
@@ -592,36 +543,20 @@ function applyChannelToSettings(channelId, options = {}) {
|
|
|
592
543
|
}
|
|
593
544
|
|
|
594
545
|
const configPath = path.join(codexDir, 'config.toml');
|
|
595
|
-
const authPath = path.join(codexDir, 'auth.json');
|
|
596
546
|
|
|
597
547
|
// 读取现有配置,保留 mcp_servers, projects 等
|
|
598
|
-
let config =
|
|
599
|
-
model: 'gpt-4',
|
|
600
|
-
model_reasoning_effort: 'high',
|
|
601
|
-
model_reasoning_summary_format: 'experimental',
|
|
602
|
-
network_access: 'enabled',
|
|
603
|
-
disable_response_storage: false,
|
|
604
|
-
show_raw_agent_reasoning: true
|
|
605
|
-
};
|
|
606
|
-
|
|
607
|
-
if (fs.existsSync(configPath)) {
|
|
608
|
-
try {
|
|
609
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
610
|
-
const parsedConfig = toml.parse(content);
|
|
611
|
-
// 深度合并,保留原有的所有配置
|
|
612
|
-
config = { ...parsedConfig };
|
|
613
|
-
} catch (err) {
|
|
614
|
-
console.warn('[Codex Channels] Failed to read existing config, using defaults');
|
|
615
|
-
}
|
|
616
|
-
}
|
|
548
|
+
let config = readCodexConfigOrThrow(configPath, getDefaultCodexConfig());
|
|
617
549
|
|
|
618
550
|
// 设置当前渠道为 model_provider
|
|
619
551
|
config.model_provider = channel.providerKey;
|
|
620
552
|
|
|
621
553
|
// 可选:清理 provider,关闭动态切换后只保留当前渠道配置
|
|
622
554
|
if (options.pruneProviders === true) {
|
|
623
|
-
config.model_providers
|
|
624
|
-
|
|
555
|
+
const existingProviders = (config.model_providers && typeof config.model_providers === 'object')
|
|
556
|
+
? config.model_providers
|
|
557
|
+
: {};
|
|
558
|
+
config.model_providers = pruneManagedProviders(existingProviders, channel.providerKey, data.channels);
|
|
559
|
+
} else if (!config.model_providers || typeof config.model_providers !== 'object') {
|
|
625
560
|
// 默认兼容历史行为:保留已有 provider
|
|
626
561
|
config.model_providers = {};
|
|
627
562
|
}
|
|
@@ -640,52 +575,13 @@ function applyChannelToSettings(channelId, options = {}) {
|
|
|
640
575
|
config.model_providers[channel.providerKey].query_params = channel.queryParams;
|
|
641
576
|
}
|
|
642
577
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
${tomlContent}`;
|
|
651
|
-
|
|
652
|
-
fs.writeFileSync(configPath, annotatedContent, 'utf8');
|
|
653
|
-
console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
|
|
654
|
-
} catch (err) {
|
|
655
|
-
console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
|
|
656
|
-
throw new Error('Failed to write config.toml: ' + err.message);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// 更新 auth.json
|
|
660
|
-
let auth = {};
|
|
661
|
-
if (fs.existsSync(authPath)) {
|
|
662
|
-
try {
|
|
663
|
-
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
664
|
-
} catch (err) {
|
|
665
|
-
console.warn('[Codex Channels] Failed to read auth.json, creating new');
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (channel.apiKey && channel.envKey) {
|
|
670
|
-
auth[channel.envKey] = channel.apiKey;
|
|
671
|
-
} else if (channel.envKey) {
|
|
672
|
-
delete auth[channel.envKey];
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// 清除 chatgpt token 认证字段,避免 Codex 优先用过期 token 而报 usage limit
|
|
676
|
-
delete auth.tokens;
|
|
677
|
-
delete auth.auth_mode;
|
|
678
|
-
|
|
679
|
-
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
680
|
-
|
|
681
|
-
if (channel.apiKey && channel.envKey) {
|
|
682
|
-
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
683
|
-
if (injectResult.success) {
|
|
684
|
-
console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
|
|
685
|
-
}
|
|
686
|
-
} else if (channel.envKey) {
|
|
687
|
-
removeEnvFromShell(channel.envKey);
|
|
688
|
-
}
|
|
578
|
+
writeAnnotatedCodexConfig(configPath, config, [
|
|
579
|
+
'# Codex Configuration',
|
|
580
|
+
'# Managed by Coding-Tool',
|
|
581
|
+
`# Current provider: ${channel.name}`
|
|
582
|
+
]);
|
|
583
|
+
console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
|
|
584
|
+
syncAllChannelEnvVars();
|
|
689
585
|
|
|
690
586
|
return channel;
|
|
691
587
|
}
|
|
@@ -705,6 +601,12 @@ function getEffectiveApiKey(channel) {
|
|
|
705
601
|
return channel.apiKey || null;
|
|
706
602
|
}
|
|
707
603
|
|
|
604
|
+
function disableAllChannels() {
|
|
605
|
+
const data = loadChannels();
|
|
606
|
+
data.channels.forEach(ch => { ch.enabled = false; });
|
|
607
|
+
saveChannels(data);
|
|
608
|
+
}
|
|
609
|
+
|
|
708
610
|
module.exports = {
|
|
709
611
|
getChannels,
|
|
710
612
|
createChannel,
|
|
@@ -715,5 +617,6 @@ module.exports = {
|
|
|
715
617
|
syncAllChannelEnvVars,
|
|
716
618
|
writeCodexConfigForMultiChannel,
|
|
717
619
|
applyChannelToSettings,
|
|
718
|
-
getEffectiveApiKey
|
|
620
|
+
getEffectiveApiKey,
|
|
621
|
+
disableAllChannels
|
|
719
622
|
};
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const toml = require('toml');
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
4
|
+
const { NATIVE_PATHS } = require('../../config/paths');
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
7
|
* 获取 Codex 配置目录
|
|
11
8
|
*/
|
|
12
9
|
function getCodexDir() {
|
|
13
|
-
return
|
|
10
|
+
return NATIVE_PATHS.codex.dir;
|
|
14
11
|
}
|
|
15
12
|
|
|
16
13
|
/**
|