@adversity/coding-tool-x 2.2.0
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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const toml = require('toml');
|
|
6
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
7
|
+
const { getCodexDir } = require('./codex-config');
|
|
8
|
+
const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex-settings-manager');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Codex 渠道管理服务(多渠道架构)
|
|
12
|
+
*
|
|
13
|
+
* Codex 配置结构:
|
|
14
|
+
* - config.toml: 主配置,包含 model_provider 和各提供商配置
|
|
15
|
+
* - auth.json: API Key 存储
|
|
16
|
+
* - 我们的 codex-channels.json: 完整渠道信息(用于管理)
|
|
17
|
+
*
|
|
18
|
+
* 多渠道模式:
|
|
19
|
+
* - 使用 enabled 字段标记渠道是否启用
|
|
20
|
+
* - 使用 weight 和 maxConcurrency 控制负载均衡
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// 获取渠道存储文件路径
|
|
24
|
+
function getChannelsFilePath() {
|
|
25
|
+
const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
|
|
26
|
+
if (!fs.existsSync(ccToolDir)) {
|
|
27
|
+
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
return path.join(ccToolDir, 'codex-channels.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 读取所有渠道(从我们的存储文件)
|
|
33
|
+
function loadChannels() {
|
|
34
|
+
const filePath = getChannelsFilePath();
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
// 尝试从 config.toml 初始化
|
|
38
|
+
return initializeFromConfig();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
43
|
+
const data = JSON.parse(content);
|
|
44
|
+
// 确保渠道有 enabled 字段(兼容旧数据)
|
|
45
|
+
if (data.channels) {
|
|
46
|
+
data.channels = data.channels.map(ch => ({
|
|
47
|
+
...ch,
|
|
48
|
+
enabled: ch.enabled !== false, // 默认启用
|
|
49
|
+
weight: ch.weight || 1,
|
|
50
|
+
maxConcurrency: ch.maxConcurrency || null
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
return data;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error('[Codex Channels] Failed to parse channels file:', err);
|
|
56
|
+
return { channels: [] };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 从现有 config.toml 初始化渠道
|
|
61
|
+
function initializeFromConfig() {
|
|
62
|
+
const configPath = path.join(getCodexDir(), 'config.toml');
|
|
63
|
+
const authPath = path.join(getCodexDir(), 'auth.json');
|
|
64
|
+
|
|
65
|
+
const defaultData = { channels: [] };
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(configPath)) {
|
|
68
|
+
saveChannels(defaultData);
|
|
69
|
+
return defaultData;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// 读取 config.toml
|
|
74
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
75
|
+
const config = toml.parse(configContent);
|
|
76
|
+
|
|
77
|
+
// 读取 auth.json
|
|
78
|
+
let auth = {};
|
|
79
|
+
if (fs.existsSync(authPath)) {
|
|
80
|
+
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 从 model_providers 提取渠道
|
|
84
|
+
const channels = [];
|
|
85
|
+
if (config.model_providers) {
|
|
86
|
+
for (const [providerKey, providerConfig] of Object.entries(config.model_providers)) {
|
|
87
|
+
// env_key 优先级:配置的 env_key > PROVIDER_API_KEY > OPENAI_API_KEY
|
|
88
|
+
let envKey = providerConfig.env_key || `${providerKey.toUpperCase()}_API_KEY`;
|
|
89
|
+
let apiKey = auth[envKey] || '';
|
|
90
|
+
|
|
91
|
+
// 如果没找到,尝试 OPENAI_API_KEY 作为通用 fallback
|
|
92
|
+
if (!apiKey && auth['OPENAI_API_KEY']) {
|
|
93
|
+
apiKey = auth['OPENAI_API_KEY'];
|
|
94
|
+
envKey = 'OPENAI_API_KEY';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
channels.push({
|
|
98
|
+
id: crypto.randomUUID(),
|
|
99
|
+
name: providerConfig.name || providerKey,
|
|
100
|
+
providerKey,
|
|
101
|
+
baseUrl: providerConfig.base_url || '',
|
|
102
|
+
wireApi: providerConfig.wire_api || 'responses',
|
|
103
|
+
envKey,
|
|
104
|
+
apiKey,
|
|
105
|
+
websiteUrl: providerConfig.website_url || '',
|
|
106
|
+
requiresOpenaiAuth: providerConfig.requires_openai_auth !== false,
|
|
107
|
+
queryParams: providerConfig.query_params || null,
|
|
108
|
+
enabled: config.model_provider === providerKey, // 当前激活的渠道启用
|
|
109
|
+
weight: 1,
|
|
110
|
+
maxConcurrency: null,
|
|
111
|
+
createdAt: Date.now(),
|
|
112
|
+
updatedAt: Date.now()
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 自动注入环境变量(从 Codex 迁移过来时使用)
|
|
116
|
+
if (apiKey && envKey) {
|
|
117
|
+
const injectResult = injectEnvToShell(envKey, apiKey);
|
|
118
|
+
if (injectResult.success) {
|
|
119
|
+
console.log(`[Codex Channels] Environment variable ${envKey} injected during initialization`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = {
|
|
126
|
+
channels
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
saveChannels(data);
|
|
130
|
+
return data;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[Codex Channels] Failed to initialize from config:', err);
|
|
133
|
+
saveChannels(defaultData);
|
|
134
|
+
return defaultData;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 保存渠道数据
|
|
139
|
+
function saveChannels(data) {
|
|
140
|
+
const filePath = getChannelsFilePath();
|
|
141
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 获取所有渠道
|
|
145
|
+
function getChannels() {
|
|
146
|
+
const data = loadChannels();
|
|
147
|
+
return {
|
|
148
|
+
channels: data.channels || []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 添加渠道
|
|
153
|
+
function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses', extraConfig = {}) {
|
|
154
|
+
const data = loadChannels();
|
|
155
|
+
|
|
156
|
+
// 检查 providerKey 是否已存在
|
|
157
|
+
const existing = data.channels.find(c => c.providerKey === providerKey);
|
|
158
|
+
if (existing) {
|
|
159
|
+
throw new Error(`Provider key "${providerKey}" already exists`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const envKey = extraConfig.envKey || `${providerKey.toUpperCase()}_API_KEY`;
|
|
163
|
+
|
|
164
|
+
const newChannel = {
|
|
165
|
+
id: crypto.randomUUID(),
|
|
166
|
+
name,
|
|
167
|
+
providerKey,
|
|
168
|
+
baseUrl,
|
|
169
|
+
wireApi,
|
|
170
|
+
envKey,
|
|
171
|
+
apiKey,
|
|
172
|
+
websiteUrl: extraConfig.websiteUrl || '',
|
|
173
|
+
requiresOpenaiAuth: extraConfig.requiresOpenaiAuth !== false,
|
|
174
|
+
queryParams: extraConfig.queryParams || null,
|
|
175
|
+
enabled: extraConfig.enabled !== false, // 默认启用
|
|
176
|
+
weight: extraConfig.weight || 1,
|
|
177
|
+
maxConcurrency: extraConfig.maxConcurrency || null,
|
|
178
|
+
createdAt: Date.now(),
|
|
179
|
+
updatedAt: Date.now()
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
data.channels.push(newChannel);
|
|
183
|
+
saveChannels(data);
|
|
184
|
+
|
|
185
|
+
// 注入该渠道的环境变量(用于直接使用 codex 命令)
|
|
186
|
+
if (apiKey && envKey) {
|
|
187
|
+
const injectResult = injectEnvToShell(envKey, apiKey);
|
|
188
|
+
if (injectResult.success) {
|
|
189
|
+
console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
|
|
190
|
+
} else {
|
|
191
|
+
console.warn(`[Codex Channels] Failed to inject ${envKey}: ${injectResult.error}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
196
|
+
// writeCodexConfigForMultiChannel(data.channels);
|
|
197
|
+
|
|
198
|
+
return newChannel;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 更新渠道
|
|
202
|
+
function updateChannel(channelId, updates) {
|
|
203
|
+
const data = loadChannels();
|
|
204
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
205
|
+
|
|
206
|
+
if (index === -1) {
|
|
207
|
+
throw new Error('Channel not found');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const oldChannel = data.channels[index];
|
|
211
|
+
|
|
212
|
+
// 检查 providerKey 冲突
|
|
213
|
+
if (updates.providerKey && updates.providerKey !== oldChannel.providerKey) {
|
|
214
|
+
const existing = data.channels.find(c => c.providerKey === updates.providerKey && c.id !== channelId);
|
|
215
|
+
if (existing) {
|
|
216
|
+
throw new Error(`Provider key "${updates.providerKey}" already exists`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newChannel = {
|
|
221
|
+
...oldChannel,
|
|
222
|
+
...updates,
|
|
223
|
+
id: channelId, // 保持 ID 不变
|
|
224
|
+
createdAt: oldChannel.createdAt, // 保持创建时间
|
|
225
|
+
updatedAt: Date.now()
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
data.channels[index] = newChannel;
|
|
229
|
+
saveChannels(data);
|
|
230
|
+
|
|
231
|
+
// 处理环境变量更新
|
|
232
|
+
// 如果 envKey 或 apiKey 变化,需要更新环境变量
|
|
233
|
+
const oldEnvKey = oldChannel.envKey;
|
|
234
|
+
const newEnvKey = newChannel.envKey;
|
|
235
|
+
const oldApiKey = oldChannel.apiKey;
|
|
236
|
+
const newApiKey = newChannel.apiKey;
|
|
237
|
+
|
|
238
|
+
// 如果 envKey 改变,删除旧的,注入新的
|
|
239
|
+
if (oldEnvKey !== newEnvKey && oldEnvKey) {
|
|
240
|
+
const removeResult = removeEnvFromShell(oldEnvKey);
|
|
241
|
+
if (removeResult.success) {
|
|
242
|
+
console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 如果有新的 API Key,注入到环境变量
|
|
247
|
+
if (newApiKey && newEnvKey) {
|
|
248
|
+
const injectResult = injectEnvToShell(newEnvKey, newApiKey);
|
|
249
|
+
if (injectResult.success) {
|
|
250
|
+
console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
255
|
+
// writeCodexConfigForMultiChannel(data.channels);
|
|
256
|
+
|
|
257
|
+
return data.channels[index];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 删除渠道
|
|
261
|
+
function deleteChannel(channelId) {
|
|
262
|
+
const data = loadChannels();
|
|
263
|
+
|
|
264
|
+
const index = data.channels.findIndex(c => c.id === channelId);
|
|
265
|
+
if (index === -1) {
|
|
266
|
+
throw new Error('Channel not found');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const deletedChannel = data.channels[index];
|
|
270
|
+
data.channels.splice(index, 1);
|
|
271
|
+
saveChannels(data);
|
|
272
|
+
|
|
273
|
+
// 从 shell 配置文件移除该渠道的环境变量
|
|
274
|
+
if (deletedChannel.envKey) {
|
|
275
|
+
const removeResult = removeEnvFromShell(deletedChannel.envKey);
|
|
276
|
+
if (removeResult.success) {
|
|
277
|
+
console.log(`[Codex Channels] Environment variable ${deletedChannel.envKey} removed`);
|
|
278
|
+
} else {
|
|
279
|
+
console.warn(`[Codex Channels] Failed to remove ${deletedChannel.envKey}: ${removeResult.error}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 注意:不再自动写入 config.toml,只在开启代理控制时才同步
|
|
284
|
+
// writeCodexConfigForMultiChannel(data.channels);
|
|
285
|
+
|
|
286
|
+
return { success: true };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* 写入 Codex 配置文件(多渠道模式)
|
|
291
|
+
*
|
|
292
|
+
* 关键改进:
|
|
293
|
+
* 1. 完整保留现有配置(mcp_servers, projects 等)
|
|
294
|
+
* 2. 如果已启用动态切换(cc-proxy),不覆盖 model_provider
|
|
295
|
+
* 3. 使用 TOML 序列化而不是字符串拼接,确保配置完整性
|
|
296
|
+
*/
|
|
297
|
+
function writeCodexConfigForMultiChannel(allChannels) {
|
|
298
|
+
const codexDir = getCodexDir();
|
|
299
|
+
|
|
300
|
+
if (!fs.existsSync(codexDir)) {
|
|
301
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
305
|
+
const authPath = path.join(codexDir, 'auth.json');
|
|
306
|
+
|
|
307
|
+
// 读取现有配置,保留所有现有字段(特别是 mcp_servers, projects 等)
|
|
308
|
+
let config = {
|
|
309
|
+
model: 'gpt-4',
|
|
310
|
+
model_reasoning_effort: 'high',
|
|
311
|
+
model_reasoning_summary_format: 'experimental',
|
|
312
|
+
network_access: 'enabled',
|
|
313
|
+
disable_response_storage: false,
|
|
314
|
+
show_raw_agent_reasoning: true
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (fs.existsSync(configPath)) {
|
|
318
|
+
try {
|
|
319
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
320
|
+
const parsedConfig = toml.parse(content);
|
|
321
|
+
|
|
322
|
+
// 深度合并,保留原有的所有配置
|
|
323
|
+
config = {
|
|
324
|
+
...parsedConfig,
|
|
325
|
+
// 只覆盖这些字段
|
|
326
|
+
model: parsedConfig.model || config.model,
|
|
327
|
+
model_reasoning_effort: parsedConfig.model_reasoning_effort || config.model_reasoning_effort,
|
|
328
|
+
model_reasoning_summary_format: parsedConfig.model_reasoning_summary_format || config.model_reasoning_summary_format,
|
|
329
|
+
network_access: parsedConfig.network_access || config.network_access,
|
|
330
|
+
disable_response_storage: parsedConfig.disable_response_storage !== undefined ? parsedConfig.disable_response_storage : config.disable_response_storage,
|
|
331
|
+
show_raw_agent_reasoning: parsedConfig.show_raw_agent_reasoning !== undefined ? parsedConfig.show_raw_agent_reasoning : config.show_raw_agent_reasoning,
|
|
332
|
+
// mcp_servers 和 projects 会从 parsedConfig 自动继承
|
|
333
|
+
// model_provider 会根据动态切换情况决定是否更新
|
|
334
|
+
};
|
|
335
|
+
} catch (err) {
|
|
336
|
+
// ignore read error, use defaults
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 判断是否已启用动态切换
|
|
341
|
+
const isProxyMode = config.model_provider === 'cc-proxy';
|
|
342
|
+
const existingProviders = (config && typeof config.model_providers === 'object') ? config.model_providers : {};
|
|
343
|
+
const existingProxyProvider = existingProviders['cc-proxy'];
|
|
344
|
+
|
|
345
|
+
// 只有当未启用动态切换时,才更新 model_provider
|
|
346
|
+
if (!isProxyMode) {
|
|
347
|
+
const enabledChannels = allChannels.filter(c => c.enabled !== false);
|
|
348
|
+
const defaultProvider = enabledChannels[0]?.providerKey || allChannels[0]?.providerKey || 'openai';
|
|
349
|
+
config.model_provider = defaultProvider;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// 重建 model_providers 配置,先保留已有的非渠道 provider,避免丢失用户自定义配置
|
|
353
|
+
config.model_providers = { ...existingProviders };
|
|
354
|
+
|
|
355
|
+
// 在代理模式下,先保留 cc-proxy provider,避免被覆盖导致缺少 provider
|
|
356
|
+
if (isProxyMode) {
|
|
357
|
+
if (existingProxyProvider) {
|
|
358
|
+
config.model_providers['cc-proxy'] = existingProxyProvider;
|
|
359
|
+
} else {
|
|
360
|
+
// 回退默认的代理配置(使用默认端口),确保 provider 存在
|
|
361
|
+
config.model_providers['cc-proxy'] = {
|
|
362
|
+
name: 'cc-proxy',
|
|
363
|
+
base_url: 'http://127.0.0.1:10089/v1',
|
|
364
|
+
wire_api: 'responses',
|
|
365
|
+
env_key: 'CC_PROXY_KEY'
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
for (const channel of allChannels) {
|
|
371
|
+
config.model_providers[channel.providerKey] = {
|
|
372
|
+
name: channel.name,
|
|
373
|
+
base_url: channel.baseUrl,
|
|
374
|
+
wire_api: channel.wireApi,
|
|
375
|
+
env_key: channel.envKey,
|
|
376
|
+
requires_openai_auth: channel.requiresOpenaiAuth !== false
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
// 添加额外查询参数(如 Azure 的 api-version)
|
|
380
|
+
if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
|
|
381
|
+
config.model_providers[channel.providerKey].query_params = channel.queryParams;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 使用 TOML 序列化写入配置(保留注释和格式)
|
|
386
|
+
try {
|
|
387
|
+
const tomlContent = tomlStringify(config);
|
|
388
|
+
// 在开头添加标记注释
|
|
389
|
+
const annotatedContent = `# Codex Configuration
|
|
390
|
+
# Managed by Coding-Tool
|
|
391
|
+
# WARNING: MCP servers and projects are preserved automatically
|
|
392
|
+
|
|
393
|
+
${tomlContent}`;
|
|
394
|
+
|
|
395
|
+
fs.writeFileSync(configPath, annotatedContent, 'utf8');
|
|
396
|
+
} catch (err) {
|
|
397
|
+
console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
|
|
398
|
+
// 降级处理:如果 tomlStringify 失败,使用手工拼接(但这样会丢失注释)
|
|
399
|
+
const fallbackContent = JSON.stringify(config, null, 2);
|
|
400
|
+
fs.writeFileSync(configPath, fallbackContent, 'utf8');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 更新 auth.json
|
|
404
|
+
let auth = {};
|
|
405
|
+
if (fs.existsSync(authPath)) {
|
|
406
|
+
try {
|
|
407
|
+
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.warn('[Codex Channels] Failed to read auth.json, creating new');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 更新所有渠道的 API Key
|
|
414
|
+
for (const channel of allChannels) {
|
|
415
|
+
if (channel.apiKey) {
|
|
416
|
+
auth[channel.envKey] = channel.apiKey;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
421
|
+
|
|
422
|
+
// 注意:环境变量注入在 createChannel 和 updateChannel 时已经处理
|
|
423
|
+
// 这里不再重复注入,避免多次写入 shell 配置文件
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// 获取所有启用的渠道(供调度器使用)
|
|
427
|
+
function getEnabledChannels() {
|
|
428
|
+
const data = loadChannels();
|
|
429
|
+
return data.channels.filter(c => c.enabled !== false);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 保存渠道顺序
|
|
433
|
+
function saveChannelOrder(order) {
|
|
434
|
+
const data = loadChannels();
|
|
435
|
+
|
|
436
|
+
// 按照给定的顺序重新排列
|
|
437
|
+
const orderedChannels = [];
|
|
438
|
+
for (const id of order) {
|
|
439
|
+
const channel = data.channels.find(c => c.id === id);
|
|
440
|
+
if (channel) {
|
|
441
|
+
orderedChannels.push(channel);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 添加不在顺序中的渠道(新添加的)
|
|
446
|
+
for (const channel of data.channels) {
|
|
447
|
+
if (!orderedChannels.find(c => c.id === channel.id)) {
|
|
448
|
+
orderedChannels.push(channel);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
data.channels = orderedChannels;
|
|
453
|
+
saveChannels(data);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 同步所有渠道的环境变量到 shell 配置文件
|
|
458
|
+
* 确保用户可以直接使用 codex 命令而无需手动设置环境变量
|
|
459
|
+
* 这个函数会在服务启动时自动调用
|
|
460
|
+
*/
|
|
461
|
+
function syncAllChannelEnvVars() {
|
|
462
|
+
try {
|
|
463
|
+
const data = loadChannels();
|
|
464
|
+
const channels = data.channels || [];
|
|
465
|
+
|
|
466
|
+
if (channels.length === 0) {
|
|
467
|
+
return { success: true, synced: 0 };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
let syncedCount = 0;
|
|
471
|
+
const results = [];
|
|
472
|
+
|
|
473
|
+
for (const channel of channels) {
|
|
474
|
+
if (channel.apiKey && channel.envKey) {
|
|
475
|
+
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
476
|
+
if (injectResult.success) {
|
|
477
|
+
syncedCount++;
|
|
478
|
+
results.push({ envKey: channel.envKey, success: true });
|
|
479
|
+
} else {
|
|
480
|
+
results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
|
|
486
|
+
return { success: true, synced: syncedCount, results };
|
|
487
|
+
} catch (err) {
|
|
488
|
+
console.error('[Codex Channels] Failed to sync env vars:', err);
|
|
489
|
+
return { success: false, error: err.message };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* 将指定渠道应用到 Codex 配置文件
|
|
495
|
+
* 类似 Claude 的"写入配置"功能,将渠道设置为当前激活的 provider
|
|
496
|
+
*
|
|
497
|
+
* @param {string} channelId - 渠道 ID
|
|
498
|
+
* @returns {Object} 应用结果
|
|
499
|
+
*/
|
|
500
|
+
function applyChannelToSettings(channelId) {
|
|
501
|
+
const data = loadChannels();
|
|
502
|
+
const channel = data.channels.find(c => c.id === channelId);
|
|
503
|
+
|
|
504
|
+
if (!channel) {
|
|
505
|
+
throw new Error('Channel not found');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const codexDir = getCodexDir();
|
|
509
|
+
|
|
510
|
+
if (!fs.existsSync(codexDir)) {
|
|
511
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const configPath = path.join(codexDir, 'config.toml');
|
|
515
|
+
const authPath = path.join(codexDir, 'auth.json');
|
|
516
|
+
|
|
517
|
+
// 读取现有配置,保留 mcp_servers, projects 等
|
|
518
|
+
let config = {
|
|
519
|
+
model: 'gpt-4',
|
|
520
|
+
model_reasoning_effort: 'high',
|
|
521
|
+
model_reasoning_summary_format: 'experimental',
|
|
522
|
+
network_access: 'enabled',
|
|
523
|
+
disable_response_storage: false,
|
|
524
|
+
show_raw_agent_reasoning: true
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
if (fs.existsSync(configPath)) {
|
|
528
|
+
try {
|
|
529
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
530
|
+
const parsedConfig = toml.parse(content);
|
|
531
|
+
// 深度合并,保留原有的所有配置
|
|
532
|
+
config = { ...parsedConfig };
|
|
533
|
+
} catch (err) {
|
|
534
|
+
console.warn('[Codex Channels] Failed to read existing config, using defaults');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 设置当前渠道为 model_provider
|
|
539
|
+
config.model_provider = channel.providerKey;
|
|
540
|
+
|
|
541
|
+
// 确保 model_providers 对象存在
|
|
542
|
+
if (!config.model_providers) {
|
|
543
|
+
config.model_providers = {};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 添加/更新当前渠道的 provider 配置
|
|
547
|
+
config.model_providers[channel.providerKey] = {
|
|
548
|
+
name: channel.name,
|
|
549
|
+
base_url: channel.baseUrl,
|
|
550
|
+
wire_api: channel.wireApi || 'responses',
|
|
551
|
+
env_key: channel.envKey,
|
|
552
|
+
requires_openai_auth: channel.requiresOpenaiAuth !== false
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// 添加额外查询参数(如 Azure 的 api-version)
|
|
556
|
+
if (channel.queryParams && Object.keys(channel.queryParams).length > 0) {
|
|
557
|
+
config.model_providers[channel.providerKey].query_params = channel.queryParams;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 使用 TOML 序列化写入配置
|
|
561
|
+
try {
|
|
562
|
+
const tomlContent = tomlStringify(config);
|
|
563
|
+
const annotatedContent = `# Codex Configuration
|
|
564
|
+
# Managed by Coding-Tool
|
|
565
|
+
# Current provider: ${channel.name}
|
|
566
|
+
|
|
567
|
+
${tomlContent}`;
|
|
568
|
+
|
|
569
|
+
fs.writeFileSync(configPath, annotatedContent, 'utf8');
|
|
570
|
+
console.log(`[Codex Channels] Applied channel ${channel.name} to config.toml`);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
console.error('[Codex Channels] Failed to write config with TOML stringify:', err);
|
|
573
|
+
throw new Error('Failed to write config.toml: ' + err.message);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// 更新 auth.json
|
|
577
|
+
let auth = {};
|
|
578
|
+
if (fs.existsSync(authPath)) {
|
|
579
|
+
try {
|
|
580
|
+
auth = JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
581
|
+
} catch (err) {
|
|
582
|
+
console.warn('[Codex Channels] Failed to read auth.json, creating new');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// 添加当前渠道的 API Key
|
|
587
|
+
if (channel.apiKey && channel.envKey) {
|
|
588
|
+
auth[channel.envKey] = channel.apiKey;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
|
|
592
|
+
|
|
593
|
+
// 注入环境变量到 shell 配置文件
|
|
594
|
+
if (channel.apiKey && channel.envKey) {
|
|
595
|
+
const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
|
|
596
|
+
if (injectResult.success) {
|
|
597
|
+
console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return channel;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 服务启动时自动同步环境变量(静默执行,不影响其他功能)
|
|
605
|
+
try {
|
|
606
|
+
const data = loadChannels();
|
|
607
|
+
if (data.channels && data.channels.length > 0) {
|
|
608
|
+
syncAllChannelEnvVars();
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
// 静默失败,不影响模块加载
|
|
612
|
+
console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
module.exports = {
|
|
616
|
+
getChannels,
|
|
617
|
+
createChannel,
|
|
618
|
+
updateChannel,
|
|
619
|
+
deleteChannel,
|
|
620
|
+
getEnabledChannels,
|
|
621
|
+
saveChannelOrder,
|
|
622
|
+
syncAllChannelEnvVars,
|
|
623
|
+
writeCodexConfigForMultiChannel,
|
|
624
|
+
applyChannelToSettings
|
|
625
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const toml = require('toml');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 获取 Codex 配置目录
|
|
8
|
+
*/
|
|
9
|
+
function getCodexDir() {
|
|
10
|
+
return path.join(os.homedir(), '.codex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 读取 config.toml
|
|
15
|
+
*/
|
|
16
|
+
function loadConfig() {
|
|
17
|
+
const configPath = path.join(getCodexDir(), 'config.toml');
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(configPath)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
25
|
+
return toml.parse(content);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
console.error('[Codex] Failed to parse config.toml:', err);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 读取 auth.json
|
|
34
|
+
*/
|
|
35
|
+
function loadAuth() {
|
|
36
|
+
const authPath = path.join(getCodexDir(), 'auth.json');
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(authPath)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(fs.readFileSync(authPath, 'utf8'));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error('[Codex] Failed to parse auth.json:', err);
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 读取 history.jsonl
|
|
52
|
+
*/
|
|
53
|
+
function loadHistory() {
|
|
54
|
+
const historyPath = path.join(getCodexDir(), 'history.jsonl');
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(historyPath)) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(historyPath, 'utf8');
|
|
62
|
+
const lines = content.trim().split('\n').filter(line => line);
|
|
63
|
+
|
|
64
|
+
return lines.map(line => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(line);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}).filter(Boolean);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error('[Codex] Failed to read history.jsonl:', err);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 检查 Codex 是否已安装
|
|
79
|
+
*/
|
|
80
|
+
function isCodexInstalled() {
|
|
81
|
+
return fs.existsSync(getCodexDir());
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = {
|
|
85
|
+
getCodexDir,
|
|
86
|
+
loadConfig,
|
|
87
|
+
loadAuth,
|
|
88
|
+
loadHistory,
|
|
89
|
+
isCodexInstalled
|
|
90
|
+
};
|