coding-tool-x 3.4.0 → 3.4.2

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 (41) hide show
  1. package/dist/web/assets/{Analytics-DEjfL5Jx.js → Analytics-CbGxotgz.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DkRL_-tf.js → ConfigTemplates-oP6nrFEb.js} +1 -1
  3. package/dist/web/assets/{Home-CF-L640I.js → Home-DMntmEvh.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BzNYTdNB.js → PluginManager-BUC_c7nH.js} +1 -1
  5. package/dist/web/assets/{ProjectList-C0-JgHMM.js → ProjectList-CW8J49n7.js} +1 -1
  6. package/dist/web/assets/{SessionList-CkZUdX5N.js → SessionList-7lYnF92v.js} +1 -1
  7. package/dist/web/assets/{SkillManager-Cak0-4d4.js → SkillManager-Cs08216i.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-CGDJzwEr.js → WorkspaceManager-CY-oGtyB.js} +1 -1
  9. package/dist/web/assets/{index-Dz7v9OM0.css → index-5qy5NMIP.css} +1 -1
  10. package/dist/web/assets/index-ClCqKpvX.js +2 -0
  11. package/dist/web/index.html +2 -2
  12. package/package.json +6 -2
  13. package/src/commands/doctor.js +2 -2
  14. package/src/commands/resume.js +1 -0
  15. package/src/commands/update.js +2 -1
  16. package/src/plugins/plugin-installer.js +1 -0
  17. package/src/server/api/claude-hooks.js +2 -3
  18. package/src/server/api/workspaces.js +2 -1
  19. package/src/server/codex-proxy-server.js +4 -92
  20. package/src/server/gemini-proxy-server.js +5 -28
  21. package/src/server/opencode-proxy-server.js +3 -93
  22. package/src/server/proxy-server.js +2 -57
  23. package/src/server/services/base/base-channel-service.js +247 -0
  24. package/src/server/services/base/proxy-utils.js +152 -0
  25. package/src/server/services/channel-health.js +30 -19
  26. package/src/server/services/channels.js +125 -293
  27. package/src/server/services/codex-channels.js +148 -513
  28. package/src/server/services/codex-env-manager.js +81 -21
  29. package/src/server/services/codex-settings-manager.js +20 -5
  30. package/src/server/services/gemini-channels.js +2 -7
  31. package/src/server/services/mcp-client.js +2 -1
  32. package/src/server/services/notification-hooks.js +9 -8
  33. package/src/server/services/oauth-credentials-service.js +12 -2
  34. package/src/server/services/opencode-channels.js +7 -9
  35. package/src/server/services/opencode-sessions.js +4 -2
  36. package/src/server/services/plugins-service.js +2 -1
  37. package/src/server/services/repo-scanner-base.js +1 -0
  38. package/src/server/services/skill-service.js +4 -2
  39. package/src/server/services/workspace-service.js +1 -0
  40. package/src/utils/port-helper.js +5 -5
  41. package/dist/web/assets/index-D_WItvHE.js +0 -2
@@ -1,16 +1,11 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const BaseChannelService = require('./base/base-channel-service');
3
4
  const { isProxyConfig } = require('./settings-manager');
4
5
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
5
6
  const { clearNativeOAuth } = require('./native-oauth-adapters');
6
7
 
7
- function getChannelsFilePath() {
8
- const dir = path.dirname(PATHS.channels.claude);
9
- if (!fs.existsSync(dir)) {
10
- fs.mkdirSync(dir, { recursive: true });
11
- }
12
- return PATHS.channels.claude;
13
- }
8
+ // ── Claude 特有工具函数 ──
14
9
 
15
10
  function getActiveChannelIdPath() {
16
11
  const dir = path.dirname(PATHS.activeChannel.claude);
@@ -43,298 +38,23 @@ function loadActiveChannelId() {
43
38
  return null;
44
39
  }
45
40
 
46
- let channelsCache = null;
47
- let channelsCacheInitialized = false;
48
- const DEFAULT_CHANNELS = { channels: [] };
49
-
50
- function normalizeNumber(value, defaultValue, max = null) {
51
- const num = Number(value);
52
- if (!Number.isFinite(num) || num <= 0) {
53
- return defaultValue;
54
- }
55
- if (max !== null && num > max) {
56
- return max;
57
- }
58
- return num;
59
- }
60
-
61
- function normalizeGatewaySourceType(value, fallback = 'claude') {
62
- const normalized = String(value || '').trim().toLowerCase();
63
- if (normalized === 'claude') return 'claude';
64
- if (normalized === 'codex') return 'codex';
65
- if (normalized === 'gemini') return 'gemini';
66
- return fallback;
67
- }
68
-
69
41
  function extractApiKeyFromHelper(apiKeyHelper) {
70
42
  if (typeof apiKeyHelper !== 'string' || !apiKeyHelper.trim()) {
71
43
  return '';
72
44
  }
73
-
74
45
  const helper = apiKeyHelper.trim();
75
46
  let match = helper.match(/^echo\s+["']([^"']+)["']$/);
76
- if (match && match[1]) {
77
- return match[1];
78
- }
79
-
47
+ if (match && match[1]) return match[1];
80
48
  match = helper.match(/^printf\s+["'][^"']*["']\s+["']([^"']+)["']$/);
81
- if (match && match[1]) {
82
- return match[1];
83
- }
84
-
49
+ if (match && match[1]) return match[1];
85
50
  return '';
86
51
  }
87
52
 
88
53
  function buildApiKeyHelperCommand() {
89
- // 避免把明文 API Key 写入可执行命令,降低注入风险
90
- return 'printf "%s" "${ANTHROPIC_AUTH_TOKEN:-${ANTHROPIC_API_KEY:-}}"';
91
- }
92
-
93
- function applyChannelDefaults(channel) {
94
- const normalized = { ...channel };
95
- if (normalized.enabled === undefined) {
96
- normalized.enabled = true;
97
- } else {
98
- normalized.enabled = !!normalized.enabled;
99
- }
100
-
101
- normalized.weight = normalizeNumber(normalized.weight, 1, 100);
102
-
103
- if (normalized.maxConcurrency === undefined ||
104
- normalized.maxConcurrency === null ||
105
- normalized.maxConcurrency === 0) {
106
- normalized.maxConcurrency = null;
107
- } else {
108
- normalized.maxConcurrency = normalizeNumber(normalized.maxConcurrency, 1, 100);
109
- }
110
-
111
- normalized.gatewaySourceType = normalizeGatewaySourceType(normalized.gatewaySourceType, 'claude');
112
-
113
- return normalized;
54
+ return 'echo \'ctx-managed\'';
114
55
  }
115
56
 
116
- function readChannelsFromFile() {
117
- const filePath = getChannelsFilePath();
118
- try {
119
- if (fs.existsSync(filePath)) {
120
- const content = fs.readFileSync(filePath, 'utf8');
121
- const data = JSON.parse(content);
122
- data.channels = (data.channels || []).map(applyChannelDefaults);
123
- return data;
124
- }
125
- } catch (error) {
126
- console.error('Error loading channels:', error);
127
- }
128
- return { ...DEFAULT_CHANNELS };
129
- }
130
-
131
- function initializeChannelsCache() {
132
- if (channelsCacheInitialized) return;
133
- channelsCache = readChannelsFromFile();
134
- channelsCacheInitialized = true;
135
-
136
- try {
137
- const filePath = getChannelsFilePath();
138
- fs.watchFile(filePath, { persistent: false }, () => {
139
- channelsCache = readChannelsFromFile();
140
- });
141
- } catch (err) {
142
- console.error('Failed to watch channels file:', err);
143
- }
144
- }
145
-
146
- function loadChannels() {
147
- if (!channelsCacheInitialized) {
148
- initializeChannelsCache();
149
- }
150
- return JSON.parse(JSON.stringify(channelsCache));
151
- }
152
-
153
- function saveChannels(data) {
154
- const filePath = getChannelsFilePath();
155
- const payload = {
156
- ...data,
157
- channels: (data.channels || []).map(applyChannelDefaults)
158
- };
159
- fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
160
- channelsCache = JSON.parse(JSON.stringify(payload));
161
- }
162
-
163
- function getCurrentSettings() {
164
- try {
165
- const settingsPath = getClaudeSettingsPath();
166
- if (!fs.existsSync(settingsPath)) {
167
- return null;
168
- }
169
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
170
- const nativeOAuth = require('./native-oauth-adapters').readNativeOAuth('claude');
171
-
172
- let baseUrl = settings.env?.ANTHROPIC_BASE_URL || '';
173
- let apiKey = settings.env?.ANTHROPIC_API_KEY || '';
174
- if (!apiKey && !nativeOAuth) {
175
- apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN || '';
176
- }
177
-
178
- if (!apiKey && settings.apiKeyHelper) {
179
- apiKey = extractApiKeyFromHelper(settings.apiKeyHelper);
180
- }
181
-
182
- if (!baseUrl && !apiKey) {
183
- return null;
184
- }
185
-
186
- return { baseUrl, apiKey };
187
- } catch (error) {
188
- console.error('Error reading current settings:', error);
189
- return null;
190
- }
191
- }
192
-
193
- function getBestChannelForRestore() {
194
- const data = loadChannels();
195
- const enabledChannels = data.channels.filter(ch => ch.enabled !== false);
196
-
197
- if (enabledChannels.length === 0) {
198
- return data.channels[0];
199
- }
200
-
201
- enabledChannels.sort((a, b) => (b.weight || 1) - (a.weight || 1));
202
- return enabledChannels[0];
203
- }
204
-
205
- function getAllChannels() {
206
- const data = loadChannels();
207
- return data.channels;
208
- }
209
-
210
- function getCurrentChannel() {
211
- const channels = getAllChannels();
212
- if (!Array.isArray(channels) || channels.length === 0) {
213
- return null;
214
- }
215
-
216
- const activeChannelId = loadActiveChannelId();
217
- if (activeChannelId) {
218
- const matched = channels.find(ch => ch.id === activeChannelId);
219
- if (matched) {
220
- return matched;
221
- }
222
- }
223
-
224
- return channels.find(ch => ch.enabled !== false) || channels[0];
225
- }
226
-
227
- function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig = {}) {
228
- const data = loadChannels();
229
- const newChannel = applyChannelDefaults({
230
- id: `channel-${Date.now()}`,
231
- name,
232
- baseUrl,
233
- apiKey,
234
- createdAt: Date.now(),
235
- websiteUrl: websiteUrl || undefined,
236
- enabled: extraConfig.enabled !== undefined ? !!extraConfig.enabled : true,
237
- weight: extraConfig.weight,
238
- maxConcurrency: extraConfig.maxConcurrency,
239
- presetId: extraConfig.presetId || 'official',
240
- modelConfig: extraConfig.modelConfig || null,
241
- modelRedirects: extraConfig.modelRedirects || [],
242
- proxyUrl: extraConfig.proxyUrl || '',
243
- speedTestModel: extraConfig.speedTestModel || null,
244
- gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'claude')
245
- });
246
-
247
- data.channels.push(newChannel);
248
- saveChannels(data);
249
- return newChannel;
250
- }
251
-
252
- function updateChannel(id, updates) {
253
- const data = loadChannels();
254
- const index = data.channels.findIndex(ch => ch.id === id);
255
-
256
- if (index === -1) {
257
- throw new Error('Channel not found');
258
- }
259
-
260
- // Store old channel data before updates
261
- const oldChannel = { ...data.channels[index] };
262
-
263
- const merged = { ...data.channels[index], ...updates };
264
- const nextChannel = applyChannelDefaults({
265
- ...merged,
266
- weight: merged.weight,
267
- maxConcurrency: merged.maxConcurrency,
268
- enabled: merged.enabled,
269
- presetId: merged.presetId,
270
- modelConfig: merged.modelConfig,
271
- modelRedirects: merged.modelRedirects || [],
272
- proxyUrl: merged.proxyUrl,
273
- speedTestModel: merged.speedTestModel,
274
- gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'claude'),
275
- updatedAt: Date.now()
276
- });
277
- data.channels[index] = nextChannel;
278
-
279
- // Get proxy status
280
- const { getProxyStatus } = require('../proxy-server');
281
- const proxyStatus = getProxyStatus();
282
- const isProxyRunning = proxyStatus.running;
283
-
284
- // Single-channel enforcement: enabling a channel disables all others ONLY when proxy is OFF
285
- // When proxy is ON (dynamic switching), multiple channels can be enabled simultaneously
286
- if (!isProxyRunning && nextChannel.enabled && !oldChannel.enabled) {
287
- data.channels.forEach((ch, i) => {
288
- if (i !== index && ch.enabled) {
289
- ch.enabled = false;
290
- }
291
- });
292
- console.log(`[Single-channel mode] Enabled "${nextChannel.name}", disabled all others`);
293
- }
294
-
295
- saveChannels(data);
296
-
297
- // Sync settings.json only when proxy is OFF.
298
- // In dynamic switching mode, defer local config writes until proxy stop.
299
- if (!isProxyRunning && nextChannel.enabled) {
300
- console.log(`[Settings-sync] Channel "${nextChannel.name}" enabled, syncing settings.json...`);
301
- updateClaudeSettingsWithModelConfig(nextChannel);
302
- }
303
-
304
- return data.channels[index];
305
- }
306
-
307
- async function deleteChannel(id) {
308
- const data = loadChannels();
309
- const index = data.channels.findIndex(ch => ch.id === id);
310
-
311
- if (index === -1) {
312
- throw new Error('Channel not found');
313
- }
314
-
315
- data.channels.splice(index, 1);
316
- saveChannels(data);
317
-
318
- return { success: true };
319
- }
320
-
321
- function applyChannelToSettings(id) {
322
- const data = loadChannels();
323
- const channel = data.channels.find(ch => ch.id === id);
324
-
325
- if (!channel) {
326
- throw new Error('Channel not found');
327
- }
328
-
329
- // In single-channel mode, only this channel should be enabled
330
- data.channels.forEach(ch => {
331
- ch.enabled = ch.id === id;
332
- });
333
- saveChannels(data);
334
- updateClaudeSettingsWithModelConfig(channel);
335
-
336
- return channel;
337
- }
57
+ // ── Claude 原生设置写入 ──
338
58
 
339
59
  function updateClaudeSettingsWithModelConfig(channel) {
340
60
  clearNativeOAuth('claude');
@@ -386,7 +106,6 @@ function updateClaudeSettingsWithModelConfig(channel) {
386
106
  }
387
107
 
388
108
  settings.apiKeyHelper = buildApiKeyHelperCommand();
389
-
390
109
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
391
110
  }
392
111
 
@@ -415,18 +134,131 @@ function updateClaudeSettings(baseUrl, apiKey) {
415
134
  }
416
135
 
417
136
  settings.apiKeyHelper = buildApiKeyHelperCommand();
418
-
419
137
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
420
138
  }
421
139
 
140
+ // ── ClaudeChannelService ──
141
+
142
+ class ClaudeChannelService extends BaseChannelService {
143
+ constructor() {
144
+ super({
145
+ platform: 'claude',
146
+ channelsFilePath: PATHS.channels.claude,
147
+ defaultGatewaySource: 'claude',
148
+ isProxyRunning: () => isProxyConfig(),
149
+ });
150
+ // Claude 特有:文件监听缓存
151
+ this._cache = null;
152
+ this._cacheInitialized = false;
153
+ }
154
+
155
+ _generateId() {
156
+ return `channel-${Date.now()}`;
157
+ }
158
+
159
+ // Claude 使用缓存 + fs.watchFile
160
+ loadChannels() {
161
+ if (this._cacheInitialized && this._cache) {
162
+ return { channels: this._cache.channels.map(ch => this._applyDefaults(ch)) };
163
+ }
164
+
165
+ const data = super.loadChannels();
166
+ this._cache = data;
167
+ this._cacheInitialized = true;
168
+
169
+ // 设置文件监听
170
+ try {
171
+ fs.watchFile(this.channelsFilePath, { interval: 2000 }, () => {
172
+ try {
173
+ this._cache = null;
174
+ this._cacheInitialized = false;
175
+ } catch (_) {}
176
+ });
177
+ } catch (_) {}
178
+
179
+ return data;
180
+ }
181
+
182
+ saveChannels(data) {
183
+ super.saveChannels(data);
184
+ this._cache = data;
185
+ this._cacheInitialized = true;
186
+ }
187
+
188
+ _applyToNativeSettings(channel) {
189
+ updateClaudeSettingsWithModelConfig(channel);
190
+ }
191
+
192
+ getEffectiveApiKey(channel) {
193
+ return channel?.apiKey || null;
194
+ }
195
+ }
196
+
197
+ // ── 单例 + 兼容导出 ──
198
+
199
+ const service = new ClaudeChannelService();
200
+
201
+ function getAllChannels() {
202
+ const data = service.loadChannels();
203
+ return data.channels;
204
+ }
205
+
206
+ function getCurrentChannel() {
207
+ const channels = getAllChannels();
208
+ const activeId = loadActiveChannelId();
209
+ if (activeId) {
210
+ const active = channels.find(ch => ch.id === activeId);
211
+ if (active) return active;
212
+ }
213
+ return channels.find(ch => ch.enabled !== false) || channels[0] || null;
214
+ }
215
+
216
+ function getCurrentSettings() {
217
+ const channel = getCurrentChannel();
218
+ if (!channel) return null;
219
+ return {
220
+ baseUrl: channel.baseUrl,
221
+ apiKey: channel.apiKey,
222
+ channelName: channel.name,
223
+ channelId: channel.id,
224
+ };
225
+ }
226
+
227
+ function getBestChannelForRestore() {
228
+ const channels = getAllChannels();
229
+ const enabled = channels.filter(ch => ch.enabled !== false);
230
+ if (enabled.length > 0) return enabled[0];
231
+ return channels[0] || null;
232
+ }
233
+
234
+ function createChannel(name, baseUrl, apiKey, websiteUrl, extraConfig) {
235
+ return service.createChannel({
236
+ name,
237
+ baseUrl,
238
+ apiKey,
239
+ websiteUrl,
240
+ ...extraConfig,
241
+ });
242
+ }
243
+
244
+ function updateChannel(id, updates) {
245
+ return service.updateChannel(id, updates);
246
+ }
247
+
248
+ function deleteChannel(id) {
249
+ return service.deleteChannel(id);
250
+ }
251
+
252
+ function applyChannelToSettings(id) {
253
+ return service.applyChannelToSettings(id);
254
+ }
255
+
422
256
  function getEffectiveApiKey(channel) {
423
- return channel.apiKey || null;
257
+ return service.getEffectiveApiKey(channel);
424
258
  }
425
259
 
426
260
  function disableAllChannels() {
427
- const data = loadChannels();
428
- data.channels.forEach(ch => { ch.enabled = false; });
429
- saveChannels(data);
261
+ return service.disableAllChannels();
430
262
  }
431
263
 
432
264
  module.exports = {
@@ -441,5 +273,5 @@ module.exports = {
441
273
  updateClaudeSettings,
442
274
  updateClaudeSettingsWithModelConfig,
443
275
  getEffectiveApiKey,
444
- disableAllChannels
276
+ disableAllChannels,
445
277
  };