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