coding-tool-x 3.3.7 → 3.3.8

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 (48) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/web/assets/{Analytics-IW6eAy9u.js → Analytics-DLpoDZ2M.js} +1 -1
  3. package/dist/web/assets/{ConfigTemplates-BPtkTMSc.js → ConfigTemplates-D_hRb55W.js} +1 -1
  4. package/dist/web/assets/Home-BMoFdAwy.css +1 -0
  5. package/dist/web/assets/Home-DNwp-0J-.js +1 -0
  6. package/dist/web/assets/{PluginManager-BGx9MSDV.js → PluginManager-JXsyym1s.js} +1 -1
  7. package/dist/web/assets/{ProjectList-BCn-mrCx.js → ProjectList-DZWSeb-q.js} +1 -1
  8. package/dist/web/assets/{SessionList-CzLfebJQ.js → SessionList-Cs624DR3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-CXz2vBQx.js → SkillManager-bEliz7qz.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-CHtgMfKc.js → WorkspaceManager-J3RecFGn.js} +1 -1
  11. package/dist/web/assets/{icons-B29onFfZ.js → icons-Cuc23WS7.js} +1 -1
  12. package/dist/web/assets/index-BXeSvAwU.js +2 -0
  13. package/dist/web/assets/index-DWAC3Tdv.css +1 -0
  14. package/dist/web/index.html +3 -3
  15. package/package.json +3 -2
  16. package/src/commands/toggle-proxy.js +100 -5
  17. package/src/config/paths.js +102 -19
  18. package/src/server/api/channels.js +9 -0
  19. package/src/server/api/codex-channels.js +9 -0
  20. package/src/server/api/codex-proxy.js +22 -11
  21. package/src/server/api/gemini-proxy.js +22 -11
  22. package/src/server/api/oauth-credentials.js +163 -0
  23. package/src/server/api/opencode-proxy.js +22 -10
  24. package/src/server/api/plugins.js +3 -1
  25. package/src/server/api/proxy.js +39 -44
  26. package/src/server/api/skills.js +91 -13
  27. package/src/server/codex-proxy-server.js +1 -11
  28. package/src/server/index.js +1 -0
  29. package/src/server/services/channels.js +18 -22
  30. package/src/server/services/codex-channels.js +124 -175
  31. package/src/server/services/codex-config.js +2 -5
  32. package/src/server/services/codex-settings-manager.js +12 -348
  33. package/src/server/services/config-export-service.js +23 -2
  34. package/src/server/services/gemini-channels.js +11 -9
  35. package/src/server/services/mcp-service.js +33 -16
  36. package/src/server/services/native-keychain.js +243 -0
  37. package/src/server/services/native-oauth-adapters.js +890 -0
  38. package/src/server/services/oauth-credentials-service.js +786 -0
  39. package/src/server/services/oauth-utils.js +49 -0
  40. package/src/server/services/opencode-channels.js +13 -9
  41. package/src/server/services/opencode-settings-manager.js +169 -16
  42. package/src/server/services/plugins-service.js +22 -1
  43. package/src/server/services/settings-manager.js +13 -0
  44. package/src/server/services/skill-service.js +712 -332
  45. package/dist/web/assets/Home-BsSioaaB.css +0 -1
  46. package/dist/web/assets/Home-obifg_9E.js +0 -1
  47. package/dist/web/assets/index-C7LPdVsN.js +0 -2
  48. package/dist/web/assets/index-eEmjZKWP.css +0 -1
@@ -0,0 +1,49 @@
1
+ const fs = require('fs');
2
+ const crypto = require('crypto');
3
+
4
+ function maskToken(value) {
5
+ const token = String(value || '').trim();
6
+ if (!token) {
7
+ return '';
8
+ }
9
+ if (token.length <= 8) {
10
+ return `${token.slice(0, 2)}***`;
11
+ }
12
+ return `${token.slice(0, 4)}...${token.slice(-4)}`;
13
+ }
14
+
15
+ function decodeJwtPayload(token) {
16
+ if (typeof token !== 'string' || !token.includes('.')) {
17
+ return null;
18
+ }
19
+
20
+ try {
21
+ const payload = token.split('.')[1];
22
+ const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
23
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, '=');
24
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function removeFileIfExists(filePath) {
31
+ try {
32
+ if (filePath && fs.existsSync(filePath)) {
33
+ fs.unlinkSync(filePath);
34
+ }
35
+ } catch {
36
+ // ignore cleanup failures
37
+ }
38
+ }
39
+
40
+ function sha256(value) {
41
+ return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');
42
+ }
43
+
44
+ module.exports = {
45
+ maskToken,
46
+ decodeJwtPayload,
47
+ removeFileIfExists,
48
+ sha256
49
+ };
@@ -2,6 +2,8 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { PATHS } = require('../../config/paths');
5
+ const { clearNativeOAuth } = require('./native-oauth-adapters');
6
+ const { setChannelConfig } = require('./opencode-settings-manager');
5
7
 
6
8
  /**
7
9
  * OpenCode 渠道管理服务
@@ -189,14 +191,6 @@ function updateChannel(channelId, updates) {
189
191
  console.log(`[OpenCode Single-channel mode] Enabled "${merged.name}", disabled all others`);
190
192
  }
191
193
 
192
- // Prevent disabling last enabled channel when proxy is OFF
193
- if (!isProxyRunning && !merged.enabled && oldChannel.enabled) {
194
- const enabledCount = data.channels.filter(ch => ch.enabled).length;
195
- if (enabledCount === 0) {
196
- throw new Error('无法禁用最后一个启用的渠道。请先启用其他渠道或启动动态切换。');
197
- }
198
- }
199
-
200
194
  saveChannels(data);
201
195
  return data.channels[index];
202
196
  }
@@ -259,6 +253,9 @@ function applyChannelToSettings(channelId) {
259
253
  });
260
254
  saveChannels(data);
261
255
 
256
+ clearNativeOAuth('opencode');
257
+ setChannelConfig(channel);
258
+
262
259
  return channel;
263
260
  }
264
261
 
@@ -371,6 +368,12 @@ async function getEffectiveApiKey(channel) {
371
368
  return candidates[0] || null;
372
369
  }
373
370
 
371
+ function disableAllChannels() {
372
+ const data = loadChannels();
373
+ data.channels.forEach(ch => { ch.enabled = false; });
374
+ saveChannels(data);
375
+ }
376
+
374
377
  module.exports = {
375
378
  getChannels,
376
379
  createChannel,
@@ -380,5 +383,6 @@ module.exports = {
380
383
  saveChannelOrder,
381
384
  applyChannelToSettings,
382
385
  getEffectiveApiKey,
383
- getEffectiveApiKeyCandidates
386
+ getEffectiveApiKeyCandidates,
387
+ disableAllChannels
384
388
  };
@@ -169,6 +169,11 @@ function isManagedProxyProvider(provider) {
169
169
  return apiKey === PROXY_API_KEY && isLocalProxyBaseUrl(baseUrl);
170
170
  }
171
171
 
172
+ function isManagedChannelProvider(provider) {
173
+ if (!provider || typeof provider !== 'object') return false;
174
+ return provider?.[MANAGED_PROVIDER_MARKER] === true;
175
+ }
176
+
172
177
  function isManagedProxyConfig(config) {
173
178
  if (!config || typeof config !== 'object') return false;
174
179
  // Check legacy single-provider format
@@ -244,6 +249,92 @@ function resolveProxyBaseUrl(config) {
244
249
  return '';
245
250
  }
246
251
 
252
+ function collectChannelModelCandidates(channel = {}) {
253
+ const seen = new Set();
254
+ const models = [];
255
+ const add = (value) => {
256
+ if (typeof value !== 'string') return;
257
+ const trimmed = value.trim();
258
+ if (!trimmed) return;
259
+ const key = trimmed.toLowerCase();
260
+ if (seen.has(key)) return;
261
+ seen.add(key);
262
+ models.push(trimmed);
263
+ };
264
+
265
+ add(channel.model);
266
+ add(channel.speedTestModel);
267
+
268
+ if (Array.isArray(channel.allowedModels)) {
269
+ channel.allowedModels.forEach(add);
270
+ }
271
+
272
+ if (channel.modelConfig && typeof channel.modelConfig === 'object') {
273
+ add(channel.modelConfig.model);
274
+ add(channel.modelConfig.opusModel);
275
+ add(channel.modelConfig.sonnetModel);
276
+ add(channel.modelConfig.haikuModel);
277
+ }
278
+
279
+ if (Array.isArray(channel.modelRedirects)) {
280
+ channel.modelRedirects.forEach((rule) => {
281
+ add(rule?.from);
282
+ add(rule?.to);
283
+ });
284
+ }
285
+
286
+ return models;
287
+ }
288
+
289
+ function clearManagedProviders(config) {
290
+ const removedProviderKeys = [];
291
+
292
+ if (!config?.provider || typeof config.provider !== 'object') {
293
+ return removedProviderKeys;
294
+ }
295
+
296
+ if (isLegacyProxyProvider(config.provider[LEGACY_PROVIDER_ID])) {
297
+ delete config.provider[LEGACY_PROVIDER_ID];
298
+ removedProviderKeys.push(LEGACY_PROVIDER_ID);
299
+ }
300
+
301
+ if (isManagedProxyProvider(config.provider[PROXY_PROVIDER_ID])) {
302
+ delete config.provider[PROXY_PROVIDER_ID];
303
+ removedProviderKeys.push(PROXY_PROVIDER_ID);
304
+ }
305
+
306
+ Object.keys(config.provider).forEach((key) => {
307
+ const provider = config.provider[key];
308
+ if (isManagedProxyProvider(provider) || isManagedChannelProvider(provider)) {
309
+ delete config.provider[key];
310
+ removedProviderKeys.push(key);
311
+ }
312
+ });
313
+
314
+ if (Object.keys(config.provider).length === 0) {
315
+ delete config.provider;
316
+ }
317
+
318
+ return removedProviderKeys;
319
+ }
320
+
321
+ function clearManagedModelRef(config, removedProviderKeys = []) {
322
+ const modelRef = String(config?.model || '').trim();
323
+ if (!modelRef) {
324
+ return;
325
+ }
326
+
327
+ if (isOldManagedModelRef(modelRef)) {
328
+ delete config.model;
329
+ return;
330
+ }
331
+
332
+ const providerId = modelRef.includes('/') ? modelRef.split('/')[0] : '';
333
+ if (providerId && removedProviderKeys.includes(providerId)) {
334
+ delete config.model;
335
+ }
336
+ }
337
+
247
338
  function backupConfig(filePath) {
248
339
  ensureConfigDir();
249
340
  const backupPath = getBackupPath(filePath);
@@ -324,23 +415,13 @@ function setProxyConfig(proxyPort, options = {}) {
324
415
  if (!next.provider || typeof next.provider !== 'object') {
325
416
  next.provider = {};
326
417
  }
327
- // 清理历史 openai 代理注入,避免 /models 出现与代理无关的 openai 模型列表。
328
- if (isLegacyProxyProvider(next.provider[LEGACY_PROVIDER_ID])) {
329
- delete next.provider[LEGACY_PROVIDER_ID];
330
- }
331
- if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
332
- delete next.provider[LEGACY_PROVIDER_ID].model;
333
- }
334
-
335
- // Remove old single ctx-proxy provider (superseded by per-channel providers)
336
- delete next.provider[PROXY_PROVIDER_ID];
418
+ const removedProviderKeys = clearManagedProviders(next);
419
+ clearManagedModelRef(next, removedProviderKeys);
337
420
 
338
- // Remove any previously managed per-channel providers that are no longer in the current list
339
- Object.keys(next.provider).forEach((key) => {
340
- if (isManagedProxyProvider(next.provider[key])) {
341
- delete next.provider[key];
342
- }
343
- });
421
+ // clearManagedProviders 可能在清空所有 provider delete next.provider,重新确保存在
422
+ if (!next.provider || typeof next.provider !== 'object') {
423
+ next.provider = {};
424
+ }
344
425
 
345
426
  const channels = Array.isArray(options.channels) ? options.channels : null;
346
427
 
@@ -427,6 +508,73 @@ function setProxyConfig(proxyPort, options = {}) {
427
508
  return { success: true, port: proxyPort, path: filePath };
428
509
  }
429
510
 
511
+ function setChannelConfig(channel = {}) {
512
+ const filePath = selectConfigPath();
513
+ backupConfig(filePath);
514
+
515
+ const current = readConfig(filePath);
516
+ const next = (current && typeof current === 'object') ? current : {};
517
+
518
+ if (!next.provider || typeof next.provider !== 'object') {
519
+ next.provider = {};
520
+ }
521
+
522
+ const removedProviderKeys = clearManagedProviders(next);
523
+ clearManagedModelRef(next, removedProviderKeys);
524
+
525
+ const baseProviderKey = sanitizeProviderKey(channel.providerKey || channel.name || 'ctx-channel');
526
+ let providerKey = baseProviderKey;
527
+ let suffix = 2;
528
+ while (next.provider[providerKey] && !isManagedChannelProvider(next.provider[providerKey])) {
529
+ providerKey = `${baseProviderKey}-${suffix}`;
530
+ suffix += 1;
531
+ }
532
+
533
+ const modelCandidates = collectChannelModelCandidates(channel);
534
+ const fallbackModel = String(channel.model || channel.speedTestModel || modelCandidates[0] || '').trim();
535
+ const modelsMap = buildModelsMap(modelCandidates, fallbackModel);
536
+ const modelIds = Object.keys(modelsMap);
537
+
538
+ if (modelIds.length === 0) {
539
+ throw new Error('OpenCode 渠道缺少可写入的模型,请至少配置默认模型或可用模型。');
540
+ }
541
+
542
+ next.provider[providerKey] = {
543
+ [MANAGED_PROVIDER_MARKER]: true,
544
+ npm: '@ai-sdk/openai-compatible',
545
+ name: channel.name || providerKey,
546
+ options: {
547
+ baseURL: String(channel.baseUrl || '').trim(),
548
+ apiKey: String(channel.apiKey || '').trim()
549
+ },
550
+ models: modelsMap
551
+ };
552
+
553
+ const topModel = normalizeOpenCodeModel(fallbackModel || modelIds[0], providerKey);
554
+ if (topModel) {
555
+ next.model = topModel;
556
+ } else {
557
+ clearManagedModelRef(next, [providerKey]);
558
+ }
559
+
560
+ writeConfig(filePath, next);
561
+ return { success: true, path: filePath, providerKey, model: next.model || null };
562
+ }
563
+
564
+ function clearManagedChannelConfig() {
565
+ const filePath = selectConfigPath();
566
+ if (!fs.existsSync(filePath)) {
567
+ return { success: true, path: filePath };
568
+ }
569
+
570
+ const current = readConfig(filePath);
571
+ const next = (current && typeof current === 'object') ? current : {};
572
+ const removedProviderKeys = clearManagedProviders(next);
573
+ clearManagedModelRef(next, removedProviderKeys);
574
+ writeConfig(filePath, next);
575
+ return { success: true, path: filePath };
576
+ }
577
+
430
578
  function isOldManagedModelRef(modelRef) {
431
579
  const s = String(modelRef || '');
432
580
  return s.startsWith(`${PROXY_PROVIDER_ID}/`) || s.startsWith(`${LEGACY_PROVIDER_ID}/`);
@@ -485,7 +633,12 @@ function getCurrentProxyPort() {
485
633
  module.exports = {
486
634
  configExists,
487
635
  hasBackup,
636
+ readConfig,
637
+ writeConfig,
638
+ selectConfigPath,
488
639
  setProxyConfig,
640
+ setChannelConfig,
641
+ clearManagedChannelConfig,
489
642
  restoreSettings,
490
643
  deleteBackup,
491
644
  isProxyConfig,
@@ -109,6 +109,9 @@ class PluginsService {
109
109
  this.ccToolConfigDir = path.join(HOME_DIR, '.cc-tool');
110
110
  this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
111
111
  this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
112
+ const prefix = this.platform === 'opencode' ? 'opencode-' : '';
113
+ this.marketCachePath = path.join(this.ccToolConfigDir, `${prefix}plugins-market-cache.json`);
114
+ this._marketCache = null;
112
115
  }
113
116
 
114
117
  _ensureDir(dirPath) {
@@ -1153,7 +1156,20 @@ class PluginsService {
1153
1156
  * Get market plugins from configured repositories
1154
1157
  * @returns {Promise<Array>} List of available market plugins
1155
1158
  */
1156
- async getMarketPlugins() {
1159
+ async getMarketPlugins(forceRefresh = false) {
1160
+ if (!forceRefresh) {
1161
+ if (this._marketCache) return this._marketCache;
1162
+ try {
1163
+ if (fs.existsSync(this.marketCachePath)) {
1164
+ const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
1165
+ if (Array.isArray(data.plugins)) {
1166
+ this._marketCache = data.plugins;
1167
+ return this._marketCache;
1168
+ }
1169
+ }
1170
+ } catch (err) { /* ignore */ }
1171
+ }
1172
+
1157
1173
  const repos = this.getRepos().filter(r => r.enabled);
1158
1174
  const marketPlugins = [];
1159
1175
 
@@ -1260,6 +1276,11 @@ class PluginsService {
1260
1276
  plugin.isInstalled = installedNames.has(plugin.name);
1261
1277
  });
1262
1278
 
1279
+ this._marketCache = marketPlugins;
1280
+ try {
1281
+ fs.writeFileSync(this.marketCachePath, JSON.stringify({ plugins: marketPlugins }), 'utf-8');
1282
+ } catch (err) { /* ignore */ }
1283
+
1263
1284
  return marketPlugins;
1264
1285
  }
1265
1286
  }
@@ -84,6 +84,18 @@ function restoreSettings() {
84
84
  }
85
85
  }
86
86
 
87
+ function deleteBackup() {
88
+ try {
89
+ if (fs.existsSync(getBackupPath())) {
90
+ fs.unlinkSync(getBackupPath());
91
+ }
92
+ return { success: true };
93
+ } catch (err) {
94
+ console.warn('Failed to delete Claude backup file:', err.message);
95
+ return { success: false, error: err.message };
96
+ }
97
+ }
98
+
87
99
  // 设置代理配置
88
100
  function setProxyConfig(proxyPort) {
89
101
  try {
@@ -156,6 +168,7 @@ module.exports = {
156
168
  writeSettings,
157
169
  backupSettings,
158
170
  restoreSettings,
171
+ deleteBackup,
159
172
  setProxyConfig,
160
173
  isProxyConfig,
161
174
  getCurrentProxyPort