@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. 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
+ };