coding-tool-x 3.4.0 → 3.4.1
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-DEjfL5Jx.js → Analytics-CbGxotgz.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DkRL_-tf.js → ConfigTemplates-oP6nrFEb.js} +1 -1
- package/dist/web/assets/{Home-CF-L640I.js → Home-DMntmEvh.js} +1 -1
- package/dist/web/assets/{PluginManager-BzNYTdNB.js → PluginManager-BUC_c7nH.js} +1 -1
- package/dist/web/assets/{ProjectList-C0-JgHMM.js → ProjectList-CW8J49n7.js} +1 -1
- package/dist/web/assets/{SessionList-CkZUdX5N.js → SessionList-7lYnF92v.js} +1 -1
- package/dist/web/assets/{SkillManager-Cak0-4d4.js → SkillManager-Cs08216i.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CGDJzwEr.js → WorkspaceManager-CY-oGtyB.js} +1 -1
- package/dist/web/assets/{index-Dz7v9OM0.css → index-5qy5NMIP.css} +1 -1
- package/dist/web/assets/index-ClCqKpvX.js +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -2
- package/src/server/codex-proxy-server.js +4 -92
- package/src/server/gemini-proxy-server.js +5 -28
- package/src/server/opencode-proxy-server.js +3 -93
- package/src/server/proxy-server.js +2 -57
- package/src/server/services/base/base-channel-service.js +247 -0
- package/src/server/services/base/proxy-utils.js +152 -0
- package/src/server/services/channel-health.js +30 -19
- package/src/server/services/channels.js +125 -293
- package/src/server/services/codex-channels.js +148 -513
- package/src/server/services/codex-env-manager.js +49 -19
- package/src/server/services/gemini-channels.js +2 -7
- package/src/server/services/oauth-credentials-service.js +12 -2
- package/src/server/services/opencode-channels.js +7 -9
- package/src/server/services/repo-scanner-base.js +1 -0
- package/dist/web/assets/index-D_WItvHE.js +0 -2
|
@@ -8,602 +8,237 @@ const { getCodexDir } = require('./codex-config');
|
|
|
8
8
|
const { isProxyConfig } = require('./codex-settings-manager');
|
|
9
9
|
const { clearNativeOAuth } = require('./native-oauth-adapters');
|
|
10
10
|
const { syncCodexUserEnvironment } = require('./codex-env-manager');
|
|
11
|
+
const BaseChannelService = require('./base/base-channel-service');
|
|
11
12
|
|
|
12
13
|
const CODEX_PROXY_ENV_KEY = 'CC_PROXY_KEY';
|
|
13
14
|
const CODEX_PROXY_ENV_VALUE = 'PROXY_KEY';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
* Codex 渠道管理服务(多渠道架构)
|
|
17
|
-
*
|
|
18
|
-
* Codex 配置结构:
|
|
19
|
-
* - config.toml: 主配置,包含 model_provider 和各提供商配置
|
|
20
|
-
* - 用户级环境变量: env_key 对应的 API Key 存储
|
|
21
|
-
* - 我们的 codex-channels.json: 完整渠道信息(用于管理)
|
|
22
|
-
*
|
|
23
|
-
* 多渠道模式:
|
|
24
|
-
* - 使用 enabled 字段标记渠道是否启用
|
|
25
|
-
* - 使用 weight 和 maxConcurrency 控制负载均衡
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
function normalizeGatewaySourceType(value, fallback = 'codex') {
|
|
29
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
30
|
-
if (normalized === 'claude') return 'claude';
|
|
31
|
-
if (normalized === 'codex') return 'codex';
|
|
32
|
-
if (normalized === 'gemini') return 'gemini';
|
|
33
|
-
return fallback;
|
|
34
|
-
}
|
|
16
|
+
// ── Codex 特有工具函数 ──
|
|
35
17
|
|
|
36
18
|
function buildManagedCodexEnvMap(channels = [], { includeProxyKey = false } = {}) {
|
|
37
|
-
// 代理模式:只写代理 Key,不暴露真实 API Key
|
|
38
19
|
if (includeProxyKey) {
|
|
39
20
|
return { [CODEX_PROXY_ENV_KEY]: CODEX_PROXY_ENV_VALUE };
|
|
40
21
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// 获取渠道存储文件路径
|
|
49
|
-
function getChannelsFilePath() {
|
|
50
|
-
const channelsDir = path.dirname(PATHS.channels.codex);
|
|
51
|
-
if (!fs.existsSync(channelsDir)) {
|
|
52
|
-
fs.mkdirSync(channelsDir, { recursive: true });
|
|
22
|
+
const envMap = {};
|
|
23
|
+
for (const ch of channels) {
|
|
24
|
+
if (ch.enabled !== false && ch.envKey && ch.apiKey) {
|
|
25
|
+
envMap[ch.envKey] = ch.apiKey;
|
|
26
|
+
}
|
|
53
27
|
}
|
|
54
|
-
return
|
|
28
|
+
return envMap;
|
|
55
29
|
}
|
|
56
30
|
|
|
57
|
-
|
|
58
|
-
function loadChannels() {
|
|
59
|
-
const filePath = getChannelsFilePath();
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(filePath)) {
|
|
62
|
-
// 尝试从 config.toml 初始化
|
|
63
|
-
return initializeFromConfig();
|
|
64
|
-
}
|
|
65
|
-
|
|
31
|
+
function syncAllChannelEnvVars() {
|
|
66
32
|
try {
|
|
67
|
-
const
|
|
68
|
-
const data =
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
enabled: ch.enabled !== false, // 默认启用
|
|
75
|
-
weight: ch.weight || 1,
|
|
76
|
-
maxConcurrency: ch.maxConcurrency || null,
|
|
77
|
-
modelRedirects: ch.modelRedirects || [],
|
|
78
|
-
speedTestModel: ch.speedTestModel || null,
|
|
79
|
-
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex')
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
return data;
|
|
33
|
+
const svc = getServiceInstance();
|
|
34
|
+
const data = svc.loadChannels();
|
|
35
|
+
const proxyRunning = isProxyConfig();
|
|
36
|
+
const envMap = buildManagedCodexEnvMap(data.channels, {
|
|
37
|
+
includeProxyKey: proxyRunning
|
|
38
|
+
});
|
|
39
|
+
syncCodexUserEnvironment(envMap, { replace: true });
|
|
84
40
|
} catch (err) {
|
|
85
|
-
console.
|
|
86
|
-
return { channels: [] };
|
|
41
|
+
console.warn('[Codex Channels] syncAllChannelEnvVars failed:', err.message);
|
|
87
42
|
}
|
|
88
43
|
}
|
|
89
44
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const defaultData = { channels: [] };
|
|
96
|
-
|
|
97
|
-
if (!fs.existsSync(configPath)) {
|
|
98
|
-
saveChannels(defaultData);
|
|
99
|
-
return defaultData;
|
|
45
|
+
function writeAnnotatedCodexConfig(configPath, config, comments = []) {
|
|
46
|
+
let tomlContent = tomlStringify(config);
|
|
47
|
+
if (comments.length > 0) {
|
|
48
|
+
tomlContent = comments.join('\n') + '\n\n' + tomlContent;
|
|
100
49
|
}
|
|
50
|
+
fs.writeFileSync(configPath, tomlContent, 'utf8');
|
|
51
|
+
}
|
|
101
52
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// 读取 auth.json
|
|
108
|
-
let auth = {};
|
|
109
|
-
if (fs.existsSync(authPath)) {
|
|
110
|
-
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 从 model_providers 提取渠道
|
|
114
|
-
const channels = [];
|
|
115
|
-
if (config.model_providers) {
|
|
116
|
-
for (const [providerKey, providerConfig] of Object.entries(config.model_providers)) {
|
|
117
|
-
// env_key 优先级:配置的 env_key > PROVIDER_API_KEY > OPENAI_API_KEY
|
|
118
|
-
let envKey = providerConfig.env_key || `${providerKey.toUpperCase()}_API_KEY`;
|
|
119
|
-
let apiKey = process.env[envKey] || auth[envKey] || '';
|
|
120
|
-
|
|
121
|
-
// 如果没找到,尝试 OPENAI_API_KEY 作为通用 fallback
|
|
122
|
-
if (!apiKey && (process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'])) {
|
|
123
|
-
apiKey = process.env.OPENAI_API_KEY || auth['OPENAI_API_KEY'];
|
|
124
|
-
envKey = 'OPENAI_API_KEY';
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
channels.push({
|
|
128
|
-
id: crypto.randomUUID(),
|
|
129
|
-
name: providerConfig.name || providerKey,
|
|
130
|
-
providerKey,
|
|
131
|
-
baseUrl: providerConfig.base_url || '',
|
|
132
|
-
wireApi: providerConfig.wire_api || 'responses',
|
|
133
|
-
envKey,
|
|
134
|
-
apiKey,
|
|
135
|
-
websiteUrl: providerConfig.website_url || '',
|
|
136
|
-
requiresOpenaiAuth: providerConfig.requires_openai_auth !== false,
|
|
137
|
-
queryParams: providerConfig.query_params || null,
|
|
138
|
-
enabled: config.model_provider === providerKey, // 当前激活的渠道启用
|
|
139
|
-
weight: 1,
|
|
140
|
-
maxConcurrency: null,
|
|
141
|
-
gatewaySourceType: 'codex',
|
|
142
|
-
createdAt: Date.now(),
|
|
143
|
-
updatedAt: Date.now()
|
|
144
|
-
});
|
|
145
|
-
}
|
|
53
|
+
function pruneManagedProviders(existingProviders, currentProviderKey, allChannels) {
|
|
54
|
+
const knownKeys = new Set(allChannels.map(ch => ch.providerKey).filter(Boolean));
|
|
55
|
+
for (const key of Object.keys(existingProviders)) {
|
|
56
|
+
if (key !== currentProviderKey && !knownKeys.has(key)) {
|
|
57
|
+
delete existingProviders[key];
|
|
146
58
|
}
|
|
147
|
-
|
|
148
|
-
const data = {
|
|
149
|
-
channels
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
saveChannels(data);
|
|
153
|
-
return data;
|
|
154
|
-
} catch (err) {
|
|
155
|
-
console.error('[Codex Channels] Failed to initialize from config:', err);
|
|
156
|
-
saveChannels(defaultData);
|
|
157
|
-
return defaultData;
|
|
158
59
|
}
|
|
159
60
|
}
|
|
160
61
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function getDefaultCodexConfig() {
|
|
168
|
-
return {
|
|
169
|
-
model: 'gpt-4',
|
|
170
|
-
model_reasoning_effort: 'high',
|
|
171
|
-
model_reasoning_summary_format: 'experimental',
|
|
172
|
-
network_access: 'enabled',
|
|
173
|
-
disable_response_storage: false,
|
|
174
|
-
show_raw_agent_reasoning: true
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function cloneConfigValue(value) {
|
|
179
|
-
return JSON.parse(JSON.stringify(value));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function readCodexConfigOrThrow(configPath, fallbackConfig = {}) {
|
|
183
|
-
if (!fs.existsSync(configPath)) {
|
|
184
|
-
return cloneConfigValue(fallbackConfig);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const content = fs.readFileSync(configPath, 'utf8');
|
|
62
|
+
function writeCodexConfigForMultiChannel(channels) {
|
|
63
|
+
const codexDir = getCodexDir();
|
|
64
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
188
65
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
66
|
+
let config = {};
|
|
67
|
+
if (fs.existsSync(configPath)) {
|
|
68
|
+
try {
|
|
69
|
+
config = toml.parse(fs.readFileSync(configPath, 'utf8'));
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn('[Codex Channels] Failed to parse existing config.toml:', err.message);
|
|
72
|
+
config = {};
|
|
73
|
+
}
|
|
193
74
|
}
|
|
194
|
-
}
|
|
195
75
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (!fs.existsSync(dirPath)) {
|
|
199
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
76
|
+
if (!config.model_providers || typeof config.model_providers !== 'object') {
|
|
77
|
+
config.model_providers = {};
|
|
200
78
|
}
|
|
201
79
|
|
|
202
|
-
const
|
|
80
|
+
const enabledChannels = channels.filter(ch => ch.enabled !== false);
|
|
81
|
+
if (enabledChannels.length > 0) {
|
|
82
|
+
const primary = enabledChannels[0];
|
|
83
|
+
config.model_provider = primary.providerKey;
|
|
203
84
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
85
|
+
for (const ch of enabledChannels) {
|
|
86
|
+
config.model_providers[ch.providerKey] = {
|
|
87
|
+
name: ch.name,
|
|
88
|
+
base_url: ch.baseUrl,
|
|
89
|
+
wire_api: ch.wireApi || 'responses',
|
|
90
|
+
env_key: ch.envKey,
|
|
91
|
+
requires_openai_auth: ch.requiresOpenaiAuth !== false
|
|
92
|
+
};
|
|
93
|
+
if (ch.queryParams && Object.keys(ch.queryParams).length > 0) {
|
|
94
|
+
config.model_providers[ch.providerKey].query_params = ch.queryParams;
|
|
213
95
|
}
|
|
214
96
|
}
|
|
215
97
|
}
|
|
216
|
-
}
|
|
217
98
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function getManagedProviderKeys(channels = []) {
|
|
225
|
-
const keys = new Set(['cc-proxy']);
|
|
226
|
-
for (const channel of channels) {
|
|
227
|
-
if (channel?.providerKey) {
|
|
228
|
-
keys.add(channel.providerKey);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return keys;
|
|
99
|
+
writeAnnotatedCodexConfig(configPath, config, [
|
|
100
|
+
'# Codex Configuration',
|
|
101
|
+
'# Managed by Coding-Tool (multi-channel)'
|
|
102
|
+
]);
|
|
232
103
|
}
|
|
233
104
|
|
|
234
|
-
|
|
235
|
-
const managedProviderKeys = getManagedProviderKeys(channels);
|
|
236
|
-
const preservedProviders = {};
|
|
105
|
+
// ── CodexChannelService ──
|
|
237
106
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
107
|
+
class CodexChannelService extends BaseChannelService {
|
|
108
|
+
constructor() {
|
|
109
|
+
super({
|
|
110
|
+
platform: 'codex',
|
|
111
|
+
channelsFilePath: PATHS.channels.codex,
|
|
112
|
+
defaultGatewaySource: 'codex',
|
|
113
|
+
isProxyRunning: () => isProxyConfig(),
|
|
114
|
+
});
|
|
242
115
|
}
|
|
243
116
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// 获取所有渠道
|
|
248
|
-
function getChannels() {
|
|
249
|
-
const data = loadChannels();
|
|
250
|
-
return {
|
|
251
|
-
channels: data.channels || []
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// 添加渠道
|
|
256
|
-
function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses', extraConfig = {}) {
|
|
257
|
-
const data = loadChannels();
|
|
258
|
-
|
|
259
|
-
// 检查 providerKey 是否已存在
|
|
260
|
-
const existing = data.channels.find(c => c.providerKey === providerKey);
|
|
261
|
-
if (existing) {
|
|
262
|
-
throw new Error(`Provider key "${providerKey}" already exists`);
|
|
117
|
+
_generateId() {
|
|
118
|
+
return crypto.randomUUID();
|
|
263
119
|
}
|
|
264
120
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
queryParams: extraConfig.queryParams || null,
|
|
278
|
-
enabled: extraConfig.enabled !== false, // 默认启用
|
|
279
|
-
weight: extraConfig.weight || 1,
|
|
280
|
-
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
281
|
-
modelRedirects: extraConfig.modelRedirects || [],
|
|
282
|
-
speedTestModel: extraConfig.speedTestModel || null,
|
|
283
|
-
gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
|
|
284
|
-
createdAt: Date.now(),
|
|
285
|
-
updatedAt: Date.now()
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
data.channels.push(newChannel);
|
|
289
|
-
saveChannels(data);
|
|
290
|
-
syncAllChannelEnvVars(data.channels);
|
|
291
|
-
|
|
292
|
-
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
293
|
-
// writeCodexConfigForMultiChannel(data.channels);
|
|
294
|
-
|
|
295
|
-
return newChannel;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 更新渠道
|
|
299
|
-
function updateChannel(channelId, updates) {
|
|
300
|
-
const data = loadChannels();
|
|
301
|
-
const index = data.channels.findIndex(c => c.id === channelId);
|
|
302
|
-
|
|
303
|
-
if (index === -1) {
|
|
304
|
-
throw new Error('Channel not found');
|
|
121
|
+
_applyDefaults(channel) {
|
|
122
|
+
const ch = super._applyDefaults(channel);
|
|
123
|
+
ch.providerKey = ch.providerKey || '';
|
|
124
|
+
ch.envKey = ch.envKey || '';
|
|
125
|
+
ch.wireApi = ch.wireApi || 'responses';
|
|
126
|
+
ch.model = ch.model || '';
|
|
127
|
+
ch.speedTestModel = ch.speedTestModel || null;
|
|
128
|
+
ch.modelRedirects = Array.isArray(ch.modelRedirects) ? ch.modelRedirects : [];
|
|
129
|
+
ch.gatewaySourceType = ch.gatewaySourceType || 'codex';
|
|
130
|
+
ch.requiresOpenaiAuth = ch.requiresOpenaiAuth !== false;
|
|
131
|
+
ch.queryParams = ch.queryParams || {};
|
|
132
|
+
return ch;
|
|
305
133
|
}
|
|
306
134
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
throw new Error(`Provider key "${
|
|
135
|
+
_validateUniqueness(channels, fields, excludeId) {
|
|
136
|
+
if (!fields.providerKey) return;
|
|
137
|
+
const dup = channels.find(ch =>
|
|
138
|
+
ch.providerKey === fields.providerKey && ch.id !== excludeId
|
|
139
|
+
);
|
|
140
|
+
if (dup) {
|
|
141
|
+
throw new Error(`Provider key "${fields.providerKey}" already exists`);
|
|
314
142
|
}
|
|
315
143
|
}
|
|
316
144
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
id: channelId, // 保持 ID 不变
|
|
321
|
-
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
322
|
-
modelRedirects: merged.modelRedirects || [],
|
|
323
|
-
speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
|
|
324
|
-
gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
|
|
325
|
-
updatedAt: Date.now()
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
data.channels[index] = newChannel;
|
|
329
|
-
|
|
330
|
-
// Get proxy status
|
|
331
|
-
const { getCodexProxyStatus } = require('../codex-proxy-server');
|
|
332
|
-
const proxyStatus = getCodexProxyStatus();
|
|
333
|
-
const isProxyRunning = proxyStatus.running;
|
|
334
|
-
|
|
335
|
-
// Single-channel enforcement: enabling a channel disables all others ONLY when proxy is OFF
|
|
336
|
-
// When proxy is ON (dynamic switching), multiple channels can be enabled simultaneously
|
|
337
|
-
if (!isProxyRunning && newChannel.enabled && !oldChannel.enabled) {
|
|
338
|
-
data.channels.forEach((ch, i) => {
|
|
339
|
-
if (i !== index && ch.enabled) {
|
|
340
|
-
ch.enabled = false;
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
console.log(`[Codex Single-channel mode] Enabled "${newChannel.name}", disabled all others`);
|
|
145
|
+
_onAfterCreate(_channel, _allChannels) {
|
|
146
|
+
syncAllChannelEnvVars();
|
|
147
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
344
148
|
}
|
|
345
149
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// In dynamic switching mode, defer local config writes until proxy stop.
|
|
350
|
-
if (!isProxyRunning && newChannel.enabled) {
|
|
351
|
-
console.log(`[Codex Settings-sync] Channel "${newChannel.name}" enabled, syncing config.toml...`);
|
|
352
|
-
applyChannelToSettings(channelId);
|
|
150
|
+
_onAfterUpdate(_old, _next, _allChannels) {
|
|
151
|
+
syncAllChannelEnvVars();
|
|
152
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
353
153
|
}
|
|
354
154
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return data.channels[index];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// 删除渠道
|
|
364
|
-
async function deleteChannel(channelId) {
|
|
365
|
-
const data = loadChannels();
|
|
366
|
-
|
|
367
|
-
const index = data.channels.findIndex(c => c.id === channelId);
|
|
368
|
-
if (index === -1) {
|
|
369
|
-
throw new Error('Channel not found');
|
|
155
|
+
_onAfterDelete(_channel, _allChannels) {
|
|
156
|
+
clearNativeOAuth('codex');
|
|
157
|
+
syncAllChannelEnvVars();
|
|
158
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
370
159
|
}
|
|
371
160
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
161
|
+
_applyToNativeSettings(channel) {
|
|
162
|
+
clearNativeOAuth('codex');
|
|
163
|
+
const codexDir = getCodexDir();
|
|
164
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
376
165
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
*
|
|
386
|
-
* 关键改进:
|
|
387
|
-
* 1. 完整保留现有配置(mcp_servers, projects 等)
|
|
388
|
-
* 2. 如果已启用动态切换(cc-proxy),不覆盖 model_provider
|
|
389
|
-
* 3. 使用 TOML 序列化而不是字符串拼接,确保配置完整性
|
|
390
|
-
*/
|
|
391
|
-
function writeCodexConfigForMultiChannel(allChannels) {
|
|
392
|
-
const codexDir = getCodexDir();
|
|
393
|
-
|
|
394
|
-
if (!fs.existsSync(codexDir)) {
|
|
395
|
-
fs.mkdirSync(codexDir, { recursive: true });
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const configPath = path.join(codexDir, 'config.toml');
|
|
166
|
+
let config = {};
|
|
167
|
+
if (fs.existsSync(configPath)) {
|
|
168
|
+
try {
|
|
169
|
+
config = toml.parse(fs.readFileSync(configPath, 'utf8'));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
config = {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
399
174
|
|
|
400
|
-
|
|
401
|
-
const defaultConfig = getDefaultCodexConfig();
|
|
402
|
-
const parsedConfig = readCodexConfigOrThrow(configPath, defaultConfig);
|
|
403
|
-
let config = {
|
|
404
|
-
...parsedConfig,
|
|
405
|
-
model: parsedConfig.model || defaultConfig.model,
|
|
406
|
-
model_reasoning_effort: parsedConfig.model_reasoning_effort || defaultConfig.model_reasoning_effort,
|
|
407
|
-
model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || defaultConfig.model_reasoning_summary_format,
|
|
408
|
-
network_access: parsedConfig.network_access || defaultConfig.network_access,
|
|
409
|
-
disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : defaultConfig.disable_response_storage,
|
|
410
|
-
show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : defaultConfig.show_raw_agent_reasoning
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
// 判断是否已启用动态切换
|
|
414
|
-
const isProxyMode = config.model_provider === 'cc-proxy';
|
|
415
|
-
const existingProviders = (config && typeof config.model_providers === 'object') ? config.model_providers : {};
|
|
416
|
-
const existingProxyProvider = existingProviders['cc-proxy'];
|
|
417
|
-
|
|
418
|
-
// 只有当未启用动态切换时,才更新 model_provider
|
|
419
|
-
if (!isProxyMode) {
|
|
420
|
-
const enabledChannels = allChannels.filter(c => c.enabled !== false);
|
|
421
|
-
const defaultProvider = enabledChannels[0]?.providerKey || allChannels[0]?.providerKey || 'openai';
|
|
422
|
-
config.model_provider = defaultProvider;
|
|
423
|
-
}
|
|
175
|
+
config.model_provider = channel.providerKey;
|
|
424
176
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// 在代理模式下,先保留 cc-proxy provider,避免被覆盖导致缺少 provider
|
|
429
|
-
if (isProxyMode) {
|
|
430
|
-
if (existingProxyProvider) {
|
|
431
|
-
config.model_providers['cc-proxy'] = existingProxyProvider;
|
|
432
|
-
} else {
|
|
433
|
-
// 回退默认的代理配置(使用默认端口),确保 provider 存在
|
|
434
|
-
config.model_providers['cc-proxy'] = {
|
|
435
|
-
name: 'cc-proxy',
|
|
436
|
-
base_url: 'http://127.0.0.1:20089/v1',
|
|
437
|
-
wire_api: 'responses',
|
|
438
|
-
env_key: 'CC_PROXY_KEY'
|
|
439
|
-
};
|
|
177
|
+
if (!config.model_providers || typeof config.model_providers !== 'object') {
|
|
178
|
+
config.model_providers = {};
|
|
440
179
|
}
|
|
441
|
-
|
|
180
|
+
const data = this.loadChannels();
|
|
181
|
+
pruneManagedProviders(config.model_providers, channel.providerKey, data.channels);
|
|
442
182
|
|
|
443
|
-
for (const channel of allChannels) {
|
|
444
183
|
config.model_providers[channel.providerKey] = {
|
|
445
184
|
name: channel.name,
|
|
446
185
|
base_url: channel.baseUrl,
|
|
447
|
-
wire_api: channel.wireApi,
|
|
186
|
+
wire_api: channel.wireApi || 'responses',
|
|
448
187
|
env_key: channel.envKey,
|
|
449
188
|
requires_openai_auth: channel.requiresOpenaiAuth !== false
|
|
450
189
|
};
|
|
451
190
|
|
|
452
|
-
// 添加额外查询参数(如 Azure 的 api-version)
|
|
453
191
|
if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
|
|
454
192
|
config.model_providers[channel.providerKey].query_params = channel.queryParams;
|
|
455
193
|
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
writeAnnotatedCodexConfig(configPath, config, [
|
|
459
|
-
'# Codex Configuration',
|
|
460
|
-
'# Managed by Coding-Tool',
|
|
461
|
-
'# WARNING: MCP servers and projects are preserved automatically'
|
|
462
|
-
]);
|
|
463
|
-
syncCodexUserEnvironment(
|
|
464
|
-
buildManagedCodexEnvMap(allChannels, { includeProxyKey: isProxyMode }),
|
|
465
|
-
{ replace: true }
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
194
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
function saveChannelOrder(order) {
|
|
477
|
-
const data = loadChannels();
|
|
478
|
-
|
|
479
|
-
// 按照给定的顺序重新排列
|
|
480
|
-
const orderedChannels = [];
|
|
481
|
-
for (const id of order) {
|
|
482
|
-
const channel = data.channels.find(c => c.id === id);
|
|
483
|
-
if (channel) {
|
|
484
|
-
orderedChannels.push(channel);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 添加不在顺序中的渠道(新添加的)
|
|
489
|
-
for (const channel of data.channels) {
|
|
490
|
-
if (!orderedChannels.find(c => c.id === channel.id)) {
|
|
491
|
-
orderedChannels.push(channel);
|
|
492
|
-
}
|
|
195
|
+
writeAnnotatedCodexConfig(configPath, config, [
|
|
196
|
+
'# Codex Configuration',
|
|
197
|
+
'# Managed by Coding-Tool',
|
|
198
|
+
`# Current provider: ${channel.name}`
|
|
199
|
+
]);
|
|
200
|
+
console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
|
|
201
|
+
syncAllChannelEnvVars();
|
|
493
202
|
}
|
|
494
|
-
|
|
495
|
-
data.channels = orderedChannels;
|
|
496
|
-
saveChannels(data);
|
|
497
203
|
}
|
|
498
204
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const data = channels ? { channels } : loadChannels();
|
|
506
|
-
return syncCodexUserEnvironment(
|
|
507
|
-
buildManagedCodexEnvMap(data.channels || [], { includeProxyKey: isProxyConfig() }),
|
|
508
|
-
{ replace: true }
|
|
509
|
-
);
|
|
205
|
+
// ── 单例 + 兼容导出 ──
|
|
206
|
+
|
|
207
|
+
let _instance = null;
|
|
208
|
+
function getServiceInstance() {
|
|
209
|
+
if (!_instance) _instance = new CodexChannelService();
|
|
210
|
+
return _instance;
|
|
510
211
|
}
|
|
511
212
|
|
|
512
|
-
|
|
513
|
-
* 将指定渠道应用到 Codex 配置文件
|
|
514
|
-
* 类似 Claude 的"写入配置"功能,将渠道设置为当前激活的 provider
|
|
515
|
-
*
|
|
516
|
-
* @param {string} channelId - 渠道 ID
|
|
517
|
-
* @param {Object} options - 可选参数
|
|
518
|
-
* @param {boolean} options.pruneProviders - 是否清理 model_providers 仅保留当前渠道
|
|
519
|
-
* @returns {Object} 应用结果
|
|
520
|
-
*/
|
|
521
|
-
function applyChannelToSettings(channelId, options = {}) {
|
|
522
|
-
const data = loadChannels();
|
|
523
|
-
const channel = data.channels.find(c => c.id === channelId);
|
|
524
|
-
|
|
525
|
-
if (!channel) {
|
|
526
|
-
throw new Error('Channel not found');
|
|
527
|
-
}
|
|
213
|
+
const service = getServiceInstance();
|
|
528
214
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
215
|
+
function getChannels() { return service.getChannels(); }
|
|
216
|
+
function getEnabledChannels() { return service.getEnabledChannels(); }
|
|
217
|
+
function createChannel(name, providerKey, baseUrl, apiKey, wireApi, extraConfig = {}) {
|
|
218
|
+
const envKey = extraConfig.envKey || `${providerKey.toUpperCase()}_API_KEY`;
|
|
219
|
+
return service.createChannel({
|
|
220
|
+
name, providerKey, baseUrl, apiKey, wireApi,
|
|
221
|
+
envKey,
|
|
222
|
+
...extraConfig,
|
|
532
223
|
});
|
|
533
|
-
saveChannels(data);
|
|
534
|
-
clearNativeOAuth('codex');
|
|
535
|
-
|
|
536
|
-
const codexDir = getCodexDir();
|
|
537
|
-
|
|
538
|
-
if (!fs.existsSync(codexDir)) {
|
|
539
|
-
fs.mkdirSync(codexDir, { recursive: true });
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const configPath = path.join(codexDir, 'config.toml');
|
|
543
|
-
|
|
544
|
-
// 读取现有配置,保留 mcp_servers, projects 等
|
|
545
|
-
let config = readCodexConfigOrThrow(configPath, getDefaultCodexConfig());
|
|
546
|
-
|
|
547
|
-
// 设置当前渠道为 model_provider
|
|
548
|
-
config.model_provider = channel.providerKey;
|
|
549
|
-
|
|
550
|
-
// 可选:清理 provider,关闭动态切换后只保留当前渠道配置
|
|
551
|
-
if (options.pruneProviders === true) {
|
|
552
|
-
const existingProviders = (config.model_providers && typeof config.model_providers === 'object')
|
|
553
|
-
? config.model_providers
|
|
554
|
-
: {};
|
|
555
|
-
config.model_providers = pruneManagedProviders(existingProviders, channel.providerKey, data.channels);
|
|
556
|
-
} else if (!config.model_providers || typeof config.model_providers !== 'object') {
|
|
557
|
-
// 默认兼容历史行为:保留已有 provider
|
|
558
|
-
config.model_providers = {};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// 添加/更新当前渠道的 provider 配置
|
|
562
|
-
config.model_providers[channel.providerKey] = {
|
|
563
|
-
name: channel.name,
|
|
564
|
-
base_url: channel.baseUrl,
|
|
565
|
-
wire_api: channel.wireApi || 'responses',
|
|
566
|
-
env_key: channel.envKey,
|
|
567
|
-
requires_openai_auth: channel.requiresOpenaiAuth !== false
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
// 添加额外查询参数(如 Azure 的 api-version)
|
|
571
|
-
if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
|
|
572
|
-
config.model_providers[channel.providerKey].query_params = channel.queryParams;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
writeAnnotatedCodexConfig(configPath, config, [
|
|
576
|
-
'# Codex Configuration',
|
|
577
|
-
'# Managed by Coding-Tool',
|
|
578
|
-
`# Current provider: ${channel.name}`
|
|
579
|
-
]);
|
|
580
|
-
console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
|
|
581
|
-
syncAllChannelEnvVars();
|
|
582
|
-
|
|
583
|
-
return channel;
|
|
584
224
|
}
|
|
225
|
+
function updateChannel(id, updates) { return service.updateChannel(id, updates); }
|
|
226
|
+
function deleteChannel(id) { return service.deleteChannel(id); }
|
|
227
|
+
function saveChannelOrder(order) { return service.saveChannelOrder(order); }
|
|
228
|
+
function applyChannelToSettings(id) { return service.applyChannelToSettings(id); }
|
|
229
|
+
function getEffectiveApiKey(channel) { return service.getEffectiveApiKey(channel); }
|
|
230
|
+
function disableAllChannels() { return service.disableAllChannels(); }
|
|
585
231
|
|
|
586
|
-
//
|
|
232
|
+
// 服务启动时自动同步环境变量
|
|
587
233
|
try {
|
|
588
|
-
const data = loadChannels();
|
|
234
|
+
const data = service.loadChannels();
|
|
589
235
|
if (data.channels && data.channels.length > 0) {
|
|
590
236
|
syncAllChannelEnvVars();
|
|
591
237
|
}
|
|
592
238
|
} catch (err) {
|
|
593
|
-
// 静默失败,不影响模块加载
|
|
594
239
|
console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
|
|
595
240
|
}
|
|
596
241
|
|
|
597
|
-
function getEffectiveApiKey(channel) {
|
|
598
|
-
return channel.apiKey || null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function disableAllChannels() {
|
|
602
|
-
const data = loadChannels();
|
|
603
|
-
data.channels.forEach(ch => { ch.enabled = false; });
|
|
604
|
-
saveChannels(data);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
242
|
module.exports = {
|
|
608
243
|
getChannels,
|
|
609
244
|
createChannel,
|
|
@@ -615,5 +250,5 @@ module.exports = {
|
|
|
615
250
|
writeCodexConfigForMultiChannel,
|
|
616
251
|
applyChannelToSettings,
|
|
617
252
|
getEffectiveApiKey,
|
|
618
|
-
disableAllChannels
|
|
253
|
+
disableAllChannels,
|
|
619
254
|
};
|