@adversity/coding-tool-x 3.1.1 → 3.1.3

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 (69) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
  3. package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
  4. package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-Bf_11LhH.js} +1 -1
  5. package/dist/web/assets/Home-BRnW4FTS.js +1 -0
  6. package/dist/web/assets/Home-CyCIx4BA.css +1 -0
  7. package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-B9J32GhW.js} +1 -1
  8. package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-5a19MWJk.js} +1 -1
  9. package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
  10. package/dist/web/assets/SessionList-Cxg5bAdT.js +1 -0
  11. package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-CVBr0CLi.js} +1 -1
  12. package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-D2Xe_Q0H.js} +1 -1
  13. package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-C7dwV94C.js} +1 -1
  14. package/dist/web/assets/icons-BxcwoY5F.js +1 -0
  15. package/dist/web/assets/index-BS9RA6SN.js +2 -0
  16. package/dist/web/assets/index-DUNAVDGb.css +1 -0
  17. package/dist/web/assets/naive-ui-BIXcURHZ.js +1 -0
  18. package/dist/web/assets/{vendors-CO3Upi1d.js → vendors-i5CBGnlm.js} +1 -1
  19. package/dist/web/assets/{vue-vendor-DqyWIXEb.js → vue-vendor-PKd8utv_.js} +1 -1
  20. package/dist/web/index.html +6 -6
  21. package/package.json +1 -1
  22. package/src/config/default.js +7 -27
  23. package/src/config/loader.js +6 -3
  24. package/src/config/model-metadata.js +167 -0
  25. package/src/config/model-metadata.json +125 -0
  26. package/src/config/model-pricing.js +23 -93
  27. package/src/server/api/channels.js +16 -39
  28. package/src/server/api/codex-channels.js +15 -43
  29. package/src/server/api/commands.js +0 -77
  30. package/src/server/api/config.js +4 -1
  31. package/src/server/api/gemini-channels.js +16 -40
  32. package/src/server/api/opencode-channels.js +108 -56
  33. package/src/server/api/opencode-proxy.js +42 -33
  34. package/src/server/api/opencode-sessions.js +4 -69
  35. package/src/server/api/sessions.js +11 -68
  36. package/src/server/api/settings.js +138 -0
  37. package/src/server/api/skills.js +0 -44
  38. package/src/server/api/statistics.js +115 -1
  39. package/src/server/codex-proxy-server.js +32 -59
  40. package/src/server/gemini-proxy-server.js +21 -18
  41. package/src/server/index.js +13 -7
  42. package/src/server/opencode-proxy-server.js +1232 -197
  43. package/src/server/proxy-server.js +8 -8
  44. package/src/server/services/codex-sessions.js +105 -6
  45. package/src/server/services/commands-service.js +0 -29
  46. package/src/server/services/config-templates-service.js +38 -28
  47. package/src/server/services/env-checker.js +97 -9
  48. package/src/server/services/env-manager.js +29 -1
  49. package/src/server/services/opencode-channels.js +3 -1
  50. package/src/server/services/opencode-sessions.js +486 -218
  51. package/src/server/services/opencode-settings-manager.js +172 -36
  52. package/src/server/services/plugins-service.js +37 -28
  53. package/src/server/services/pty-manager.js +22 -18
  54. package/src/server/services/response-decoder.js +21 -0
  55. package/src/server/services/skill-service.js +1 -49
  56. package/src/server/services/speed-test.js +40 -3
  57. package/src/server/services/statistics-service.js +238 -1
  58. package/src/server/utils/pricing.js +51 -60
  59. package/src/server/websocket-server.js +24 -5
  60. package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
  61. package/dist/web/assets/Home-Di2qsylF.css +0 -1
  62. package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
  63. package/dist/web/assets/SessionList-lZ0LKzfT.js +0 -1
  64. package/dist/web/assets/icons-kcfLIMBB.js +0 -1
  65. package/dist/web/assets/index-Ufv5rCa5.css +0 -1
  66. package/dist/web/assets/index-lAkrRC3h.js +0 -2
  67. package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
  68. package/src/server/api/convert.js +0 -260
  69. package/src/server/services/session-converter.js +0 -577
@@ -1,6 +1,25 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { NATIVE_PATHS } = require('../../config/paths');
4
+ const { resolveModelMetadata } = require('../../config/model-metadata');
5
+
6
+ /**
7
+ * 根据模型 ID 查找 limit(context + output)
8
+ * 委托给集中式 model-metadata.js
9
+ */
10
+ function resolveModelLimit(modelId) {
11
+ const meta = resolveModelMetadata(modelId);
12
+ return meta ? meta.limit : null;
13
+ }
14
+
15
+ /**
16
+ * 根据模型 ID 查找定价信息
17
+ * 委托给集中式 model-metadata.js
18
+ */
19
+ function resolveModelCost(modelId) {
20
+ const meta = resolveModelMetadata(modelId);
21
+ return meta ? meta.pricing : null;
22
+ }
4
23
 
5
24
  const CONFIG_DIR = NATIVE_PATHS.opencode.config;
6
25
  const CONFIG_PATHS = {
@@ -13,6 +32,7 @@ const EMPTY_SENTINEL = '__CC_TOOL_NO_FILE__';
13
32
  const PROXY_PROVIDER_ID = 'ctx-proxy';
14
33
  const LEGACY_PROVIDER_ID = 'openai';
15
34
  const PROXY_API_KEY = 'PROXY_KEY';
35
+ const MANAGED_PROVIDER_MARKER = '__ctx_managed__';
16
36
 
17
37
  function ensureConfigDir() {
18
38
  if (!fs.existsSync(CONFIG_DIR)) {
@@ -106,18 +126,28 @@ function writeConfig(filePath, config) {
106
126
  fs.writeFileSync(filePath, content, 'utf8');
107
127
  }
108
128
 
109
- function normalizeOpenCodeModel(modelId) {
129
+ function normalizeOpenCodeModel(modelId, providerId) {
110
130
  const normalized = String(modelId || '').trim();
111
131
  if (!normalized) {
112
132
  return '';
113
133
  }
114
134
 
115
- // OpenCode 要求格式为 provider/model。这里统一绑定到 ctx-proxy provider,
116
- // 避免落到内置 openai provider 的模型清单。
117
- if (normalized.startsWith(`${PROXY_PROVIDER_ID}/`)) {
135
+ const pid = String(providerId || PROXY_PROVIDER_ID).trim() || PROXY_PROVIDER_ID;
136
+
137
+ // Already has a provider/ prefix - keep as-is only if it matches the expected provider
138
+ if (normalized.includes('/')) {
118
139
  return normalized;
119
140
  }
120
- return `${PROXY_PROVIDER_ID}/${normalized}`;
141
+ return `${pid}/${normalized}`;
142
+ }
143
+
144
+ function sanitizeProviderKey(name) {
145
+ return String(name || '')
146
+ .toLowerCase()
147
+ .replace(/[^a-z0-9-]/g, '-')
148
+ .replace(/-+/g, '-')
149
+ .replace(/^-|-$/g, '')
150
+ || 'channel';
121
151
  }
122
152
 
123
153
  function isLocalProxyBaseUrl(url) {
@@ -141,8 +171,16 @@ function isManagedProxyProvider(provider) {
141
171
 
142
172
  function isManagedProxyConfig(config) {
143
173
  if (!config || typeof config !== 'object') return false;
144
- return isManagedProxyProvider(config?.provider?.[PROXY_PROVIDER_ID])
145
- || isLegacyProxyProvider(config?.provider?.[LEGACY_PROVIDER_ID]);
174
+ // Check legacy single-provider format
175
+ if (isManagedProxyProvider(config?.provider?.[PROXY_PROVIDER_ID])
176
+ || isLegacyProxyProvider(config?.provider?.[LEGACY_PROVIDER_ID])) {
177
+ return true;
178
+ }
179
+ // Check per-channel provider format (any provider with PROXY_API_KEY + local baseURL)
180
+ if (config?.provider && typeof config.provider === 'object') {
181
+ return Object.values(config.provider).some(p => isManagedProxyProvider(p));
182
+ }
183
+ return false;
146
184
  }
147
185
 
148
186
  function buildModelsMap(models = [], fallbackModel = '') {
@@ -156,7 +194,28 @@ function buildModelsMap(models = [], fallbackModel = '') {
156
194
  const key = trimmed.toLowerCase();
157
195
  if (seen.has(key)) return;
158
196
  seen.add(key);
159
- map[trimmed] = { name: trimmed };
197
+
198
+ const entry = { name: trimmed };
199
+
200
+ // 注入 limit(context + output),供 OpenCode 显示 "X% used"
201
+ // OpenCode schema 要求 limit 必须同时包含 context 和 output
202
+ const limit = resolveModelLimit(trimmed);
203
+ if (limit) {
204
+ entry.limit = { context: limit.context, output: limit.output };
205
+ }
206
+
207
+ // 注入定价信息,供 OpenCode 计算 cost
208
+ const pricing = resolveModelCost(trimmed);
209
+ if (pricing) {
210
+ entry.cost = {
211
+ input: pricing.input,
212
+ output: pricing.output,
213
+ cache_read: pricing.cacheRead,
214
+ cache_write: pricing.cacheCreation
215
+ };
216
+ }
217
+
218
+ map[trimmed] = entry;
160
219
  };
161
220
 
162
221
  if (Array.isArray(models)) {
@@ -168,9 +227,21 @@ function buildModelsMap(models = [], fallbackModel = '') {
168
227
  }
169
228
 
170
229
  function resolveProxyBaseUrl(config) {
171
- return config?.provider?.[PROXY_PROVIDER_ID]?.options?.baseURL
172
- || config?.provider?.[LEGACY_PROVIDER_ID]?.options?.baseURL
173
- || '';
230
+ if (config?.provider?.[PROXY_PROVIDER_ID]?.options?.baseURL) {
231
+ return config.provider[PROXY_PROVIDER_ID].options.baseURL;
232
+ }
233
+ if (config?.provider?.[LEGACY_PROVIDER_ID]?.options?.baseURL) {
234
+ return config.provider[LEGACY_PROVIDER_ID].options.baseURL;
235
+ }
236
+ // Check per-channel managed providers
237
+ if (config?.provider && typeof config.provider === 'object') {
238
+ for (const p of Object.values(config.provider)) {
239
+ if (isManagedProxyProvider(p) && p?.options?.baseURL) {
240
+ return p.options.baseURL;
241
+ }
242
+ }
243
+ }
244
+ return '';
174
245
  }
175
246
 
176
247
  function backupConfig(filePath) {
@@ -257,38 +328,98 @@ function setProxyConfig(proxyPort, options = {}) {
257
328
  if (isLegacyProxyProvider(next.provider[LEGACY_PROVIDER_ID])) {
258
329
  delete next.provider[LEGACY_PROVIDER_ID];
259
330
  }
260
-
261
331
  if (Object.prototype.hasOwnProperty.call(next.provider[LEGACY_PROVIDER_ID] || {}, 'model')) {
262
332
  delete next.provider[LEGACY_PROVIDER_ID].model;
263
333
  }
264
334
 
265
- const modelsMap = buildModelsMap(options.models, options.model);
266
- const modelIds = Object.keys(modelsMap);
267
-
268
- if (modelIds.length > 0) {
269
- next.provider[PROXY_PROVIDER_ID] = {
270
- npm: '@ai-sdk/openai-compatible',
271
- name: 'CTX Proxy',
272
- options: {
273
- baseURL: `http://127.0.0.1:${proxyPort}/v1`,
274
- apiKey: PROXY_API_KEY
275
- },
276
- models: modelsMap
277
- };
335
+ // Remove old single ctx-proxy provider (superseded by per-channel providers)
336
+ delete next.provider[PROXY_PROVIDER_ID];
337
+
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
+ });
344
+
345
+ const channels = Array.isArray(options.channels) ? options.channels : null;
346
+
347
+ if (channels && channels.length > 0) {
348
+ // Per-channel mode: write one provider entry per channel
349
+ const usedKeys = new Set();
350
+ let firstProviderId = null;
351
+ let firstModelId = null;
352
+
353
+ channels.forEach((ch) => {
354
+ const rawKey = sanitizeProviderKey(ch.providerKey || ch.name || '');
355
+ // Ensure uniqueness
356
+ let key = rawKey;
357
+ let suffix = 2;
358
+ while (usedKeys.has(key)) {
359
+ key = `${rawKey}-${suffix}`;
360
+ suffix += 1;
361
+ }
362
+ usedKeys.add(key);
363
+
364
+ const modelsMap = buildModelsMap(ch.models, ch.model);
365
+ const modelIds = Object.keys(modelsMap);
366
+
367
+ if (modelIds.length > 0) {
368
+ next.provider[key] = {
369
+ npm: '@ai-sdk/openai-compatible',
370
+ name: ch.name || key,
371
+ options: {
372
+ baseURL: `http://127.0.0.1:${proxyPort}/v1`,
373
+ apiKey: PROXY_API_KEY
374
+ },
375
+ models: modelsMap
376
+ };
377
+
378
+ if (firstProviderId === null) {
379
+ firstProviderId = key;
380
+ firstModelId = ch.model || modelIds[0] || null;
381
+ }
382
+ }
383
+ });
384
+
385
+ // Write top-level model pointing to first channel's first model
386
+ const topModel = options.model || (firstProviderId && firstModelId
387
+ ? `${firstProviderId}/${firstModelId}`
388
+ : null);
389
+ if (topModel) {
390
+ const resolved = normalizeOpenCodeModel(topModel, firstProviderId || PROXY_PROVIDER_ID);
391
+ if (resolved) {
392
+ next.model = resolved;
393
+ }
394
+ } else if (isOldManagedModelRef(next.model)) {
395
+ delete next.model;
396
+ }
278
397
  } else {
279
- // 无模型时不暴露 provider,避免出现误导性的 provider.openai/provider 列表。
280
- delete next.provider[PROXY_PROVIDER_ID];
281
- }
398
+ // Fallback: legacy flat-model mode (single ctx-proxy provider)
399
+ const modelsMap = buildModelsMap(options.models, options.model);
400
+ const modelIds = Object.keys(modelsMap);
401
+
402
+ if (modelIds.length > 0) {
403
+ next.provider[PROXY_PROVIDER_ID] = {
404
+ npm: '@ai-sdk/openai-compatible',
405
+ name: 'CTX Proxy',
406
+ options: {
407
+ baseURL: `http://127.0.0.1:${proxyPort}/v1`,
408
+ apiKey: PROXY_API_KEY
409
+ },
410
+ models: modelsMap
411
+ };
412
+ }
282
413
 
283
- // 写入顶层 model(OpenCode 要求 provider/model 格式),无显式模型时兜底第一个模型。
284
- const fallbackModel = options.model || modelIds[0] || '';
285
- if (fallbackModel) {
286
- const resolvedModel = normalizeOpenCodeModel(fallbackModel);
287
- if (resolvedModel) {
288
- next.model = resolvedModel;
414
+ const fallbackModel = options.model || modelIds[0] || '';
415
+ if (fallbackModel) {
416
+ const resolvedModel = normalizeOpenCodeModel(fallbackModel, PROXY_PROVIDER_ID);
417
+ if (resolvedModel) {
418
+ next.model = resolvedModel;
419
+ }
420
+ } else if (isOldManagedModelRef(next.model)) {
421
+ delete next.model;
289
422
  }
290
- } else if (String(next.model || '').startsWith(`${PROXY_PROVIDER_ID}/`) || String(next.model || '').startsWith(`${LEGACY_PROVIDER_ID}/`)) {
291
- delete next.model;
292
423
  }
293
424
 
294
425
  writeConfig(filePath, next);
@@ -296,6 +427,11 @@ function setProxyConfig(proxyPort, options = {}) {
296
427
  return { success: true, port: proxyPort, path: filePath };
297
428
  }
298
429
 
430
+ function isOldManagedModelRef(modelRef) {
431
+ const s = String(modelRef || '');
432
+ return s.startsWith(`${PROXY_PROVIDER_ID}/`) || s.startsWith(`${LEGACY_PROVIDER_ID}/`);
433
+ }
434
+
299
435
  function restoreSettings() {
300
436
  const restored = [
301
437
  restoreConfig(CONFIG_PATHS.opencodec),
@@ -703,46 +703,55 @@ class PluginsService {
703
703
  getRepos() {
704
704
  const repos = [];
705
705
  const seenRepos = new Set();
706
+ const pushRepo = (repo) => {
707
+ if (!repo || !repo.owner || !repo.name) return;
708
+ const key = `${repo.owner}/${repo.name}`;
709
+ if (seenRepos.has(key)) return;
710
+ repos.push(repo);
711
+ seenRepos.add(key);
712
+ };
713
+ const parseRepoUrl = (url) => {
714
+ if (!url || typeof url !== 'string') return null;
715
+ const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
716
+ if (!match) return null;
717
+ return { owner: match[1], name: match[2], url };
718
+ };
706
719
 
707
720
  // 1. Load our own config
708
721
  const config = this.loadReposConfig();
709
722
  for (const repo of config.repos || []) {
710
- const key = `${repo.owner}/${repo.name}`;
711
- if (!seenRepos.has(key)) {
712
- repos.push(repo);
713
- seenRepos.add(key);
714
- }
723
+ pushRepo(repo);
715
724
  }
716
725
 
717
726
  // 2. Load Claude Code's native marketplace config (Claude only)
718
727
  if (!this._isOpenCode() && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
719
728
  try {
720
729
  const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
721
-
722
- for (const [marketplaceName, marketplaceData] of Object.entries(marketplaces)) {
723
- if (marketplaceData.source && marketplaceData.source.url) {
724
- const url = marketplaceData.source.url;
725
- const match = url.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
726
-
727
- if (match) {
728
- const [, owner, name] = match;
729
- const key = `${owner}/${name}`;
730
-
731
- if (!seenRepos.has(key)) {
732
- repos.push({
733
- owner,
734
- name,
735
- url,
736
- branch: 'main', // Default branch
737
- enabled: true,
738
- source: 'claude-native',
739
- lastUpdated: marketplaceData.lastUpdated
740
- });
741
- seenRepos.add(key);
742
- }
743
- }
730
+ const entries = [];
731
+ if (Array.isArray(marketplaces)) {
732
+ entries.push(...marketplaces.map(item => ({ key: '', data: item })));
733
+ } else if (marketplaces && typeof marketplaces === 'object') {
734
+ entries.push(...Object.entries(marketplaces).map(([key, data]) => ({ key, data })));
735
+ if (Array.isArray(marketplaces.marketplaces)) {
736
+ entries.push(...marketplaces.marketplaces.map(item => ({ key: item?.name || '', data: item })));
744
737
  }
745
738
  }
739
+
740
+ for (const { key, data } of entries) {
741
+ const sourceUrl = data?.source?.url || data?.url || data?.repoUrl || data?.repository;
742
+ const parsed = parseRepoUrl(sourceUrl);
743
+ if (!parsed) continue;
744
+ pushRepo({
745
+ owner: parsed.owner,
746
+ name: parsed.name,
747
+ url: parsed.url,
748
+ branch: data?.source?.branch || data?.branch || 'main',
749
+ enabled: data?.enabled !== false,
750
+ source: 'claude-native',
751
+ marketplace: key || data?.name || '',
752
+ lastUpdated: data?.lastUpdated
753
+ });
754
+ }
746
755
  } catch (err) {
747
756
  console.error('[PluginsService] Failed to read known_marketplaces.json:', err.message);
748
757
  }
@@ -48,6 +48,14 @@ class PtyManager {
48
48
  }
49
49
  }
50
50
 
51
+ isDirectoryPath(candidate) {
52
+ try {
53
+ return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();
54
+ } catch (err) {
55
+ return false;
56
+ }
57
+ }
58
+
51
59
  resolveWorkingDirectory(cwd) {
52
60
  const fallback = os.homedir();
53
61
  if (typeof cwd !== 'string') {
@@ -69,28 +77,22 @@ class PtyManager {
69
77
  }
70
78
 
71
79
  // 先尝试直接使用(支持相对路径)
72
- try {
73
- if (fs.existsSync(normalized) && fs.statSync(normalized).isDirectory()) {
74
- return path.isAbsolute(normalized) ? normalized : path.resolve(process.cwd(), normalized);
75
- }
76
- } catch (err) {
77
- // 忽略错误,继续尝试其他候选路径
80
+ if (this.isDirectoryPath(normalized)) {
81
+ return path.isAbsolute(normalized) ? normalized : path.resolve(process.cwd(), normalized);
78
82
  }
79
83
 
80
84
  // 相对路径:优先按进程 cwd 解析,其次按用户 home 解析(用于 .codex 这类隐藏目录)
81
85
  if (!path.isAbsolute(normalized)) {
82
86
  const candidates = [
83
87
  path.resolve(process.cwd(), normalized),
88
+ path.resolve(process.cwd(), '..', normalized),
89
+ path.resolve(process.cwd(), '..', '..', normalized),
84
90
  path.resolve(os.homedir(), normalized)
85
91
  ];
86
92
 
87
93
  for (const candidate of candidates) {
88
- try {
89
- if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
90
- return candidate;
91
- }
92
- } catch (err) {
93
- // 忽略错误
94
+ if (this.isDirectoryPath(candidate)) {
95
+ return candidate;
94
96
  }
95
97
  }
96
98
  }
@@ -269,16 +271,18 @@ class PtyManager {
269
271
  console.log(`[PTY] Resolved cwd: ${originalCwd} -> ${cwd}`);
270
272
  }
271
273
 
272
- try {
273
- if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
274
+ if (!this.isDirectoryPath(cwd)) {
275
+ const fallbackCandidates = [process.cwd(), os.homedir()];
276
+ const fallbackCwd = fallbackCandidates.find((candidate) => this.isDirectoryPath(candidate));
277
+
278
+ if (fallbackCwd) {
279
+ console.warn(`[PTY] Working directory not found: ${cwd}, fallback to ${fallbackCwd}`);
280
+ cwd = fallbackCwd;
281
+ } else {
274
282
  const error = `Working directory not found: ${cwd}`;
275
283
  console.error('[PTY]', error);
276
284
  throw new Error(error);
277
285
  }
278
- } catch (err) {
279
- const error = `Working directory not found: ${cwd}`;
280
- console.error('[PTY]', error);
281
- throw new Error(error);
282
286
  }
283
287
 
284
288
  console.log(`[PTY] Creating terminal: shell=${shell}, cwd=${cwd}`);
@@ -0,0 +1,21 @@
1
+ const zlib = require('zlib');
2
+
3
+ function createDecodedStream(res) {
4
+ const encoding = String(res.headers['content-encoding'] || '').toLowerCase();
5
+
6
+ if (encoding.includes('gzip')) {
7
+ return res.pipe(zlib.createGunzip());
8
+ }
9
+ if (encoding.includes('deflate')) {
10
+ return res.pipe(zlib.createInflate());
11
+ }
12
+ if (encoding.includes('br') && typeof zlib.createBrotliDecompress === 'function') {
13
+ return res.pipe(zlib.createBrotliDecompress());
14
+ }
15
+
16
+ return res;
17
+ }
18
+
19
+ module.exports = {
20
+ createDecodedStream
21
+ };
@@ -15,9 +15,6 @@ const { pipeline } = require('stream/promises');
15
15
  const AdmZip = require('adm-zip');
16
16
  const {
17
17
  parseSkillContent,
18
- detectSkillFormat,
19
- convertSkillToCodex,
20
- convertSkillToClaude
21
18
  } = require('./format-converter');
22
19
  const { NATIVE_PATHS } = require('../../config/paths');
23
20
 
@@ -705,25 +702,6 @@ class SkillService {
705
702
  return this.validateOpenCodeSkillMetadata(metadata, directory);
706
703
  }
707
704
 
708
- /**
709
- * 转换技能格式
710
- * @param {string} content - 技能内容
711
- * @param {string} targetFormat - 目标格式 ('claude' | 'codex')
712
- */
713
- convertSkillFormat(content, targetFormat) {
714
- const sourceFormat = detectSkillFormat(content);
715
-
716
- if (sourceFormat === targetFormat) {
717
- return { content, warnings: [], format: targetFormat };
718
- }
719
-
720
- if (targetFormat === 'codex') {
721
- return convertSkillToCodex(content);
722
- } else {
723
- return convertSkillToClaude(content);
724
- }
725
- }
726
-
727
705
  /**
728
706
  * 检查技能是否已安装
729
707
  */
@@ -880,9 +858,7 @@ class SkillService {
880
858
  fs.mkdirSync(dest, { recursive: true });
881
859
  this.copyDirRecursive(sourceDir, dest);
882
860
 
883
- if (this.platform === 'codex') {
884
- this.convertInstalledSkillToCodex(dest);
885
- } else if (this.platform === 'opencode') {
861
+ if (this.platform === 'opencode') {
886
862
  const skillMdPath = path.join(dest, 'SKILL.md');
887
863
  if (fs.existsSync(skillMdPath)) {
888
864
  const validationError = this.validateOpenCodeSkillContent(
@@ -976,22 +952,6 @@ class SkillService {
976
952
  }
977
953
  }
978
954
 
979
- /**
980
- * 将安装后的 SKILL.md 转换为 Codex 兼容格式
981
- */
982
- convertInstalledSkillToCodex(skillDir) {
983
- const skillMdPath = path.join(skillDir, 'SKILL.md');
984
- if (!fs.existsSync(skillMdPath)) return;
985
-
986
- try {
987
- const content = fs.readFileSync(skillMdPath, 'utf-8');
988
- const converted = convertSkillToCodex(content);
989
- fs.writeFileSync(skillMdPath, converted.content, 'utf-8');
990
- } catch (err) {
991
- console.warn('[SkillService] Convert skill to codex format failed:', err.message);
992
- }
993
- }
994
-
995
955
  /**
996
956
  * 创建自定义技能
997
957
  */
@@ -1051,10 +1011,6 @@ ${content}
1051
1011
  // 写入文件
1052
1012
  fs.writeFileSync(path.join(dest, 'SKILL.md'), skillMdContent, 'utf-8');
1053
1013
 
1054
- if (this.platform === 'codex') {
1055
- this.convertInstalledSkillToCodex(dest);
1056
- }
1057
-
1058
1014
  // 清除缓存,让列表刷新
1059
1015
  this.skillsCache = null;
1060
1016
  this.cacheTime = 0;
@@ -1122,10 +1078,6 @@ ${content}
1122
1078
  }
1123
1079
  }
1124
1080
 
1125
- if (this.platform === 'codex') {
1126
- this.convertInstalledSkillToCodex(dest);
1127
- }
1128
-
1129
1081
  // 清除缓存
1130
1082
  this.skillsCache = null;
1131
1083
  this.cacheTime = 0;
@@ -12,6 +12,7 @@ const { getEffectiveApiKey: getClaudeEffectiveApiKey } = require('./channels');
12
12
  const { getEffectiveApiKey: getCodexEffectiveApiKey } = require('./codex-channels');
13
13
  const { getEffectiveApiKey: getGeminiEffectiveApiKey } = require('./gemini-channels');
14
14
  const { getEffectiveApiKey: getOpenCodeEffectiveApiKey } = require('./opencode-channels');
15
+ const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
15
16
 
16
17
  // 测试结果缓存
17
18
  const testResultsCache = new Map();
@@ -87,6 +88,21 @@ function resolveExplicitModel(channel, model) {
87
88
  );
88
89
  }
89
90
 
91
+ function resolveToolTypeForSpeedTest(channelType, channel) {
92
+ if (channelType === 'claude' || channelType === 'codex' || channelType === 'gemini') {
93
+ return channelType;
94
+ }
95
+ const gatewaySourceType = normalizeNonEmptyString(channel?.gatewaySourceType);
96
+ if (gatewaySourceType === 'claude' || gatewaySourceType === 'codex' || gatewaySourceType === 'gemini') {
97
+ return gatewaySourceType;
98
+ }
99
+ return 'codex';
100
+ }
101
+
102
+ function getConfiguredDefaultSpeedTestModel(toolType) {
103
+ return normalizeNonEmptyString(getDefaultSpeedTestModelByToolType(toolType));
104
+ }
105
+
90
106
  function resolveEffectiveApiKey(channel, channelType) {
91
107
  switch (channelType) {
92
108
  case 'codex':
@@ -441,6 +457,21 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
441
457
  };
442
458
  console.log(`[SpeedTest] Using explicit model: ${explicitModel}`);
443
459
  } else {
460
+ const defaultSpeedTestModel = getConfiguredDefaultSpeedTestModel(
461
+ resolveToolTypeForSpeedTest(channelType, channel)
462
+ );
463
+ if (defaultSpeedTestModel) {
464
+ modelProbe = {
465
+ preferredTestModel: defaultSpeedTestModel,
466
+ availableModels: [defaultSpeedTestModel],
467
+ cached: false,
468
+ method: 'default_config'
469
+ };
470
+ console.log(`[SpeedTest] Using default speedTestModel from config: ${defaultSpeedTestModel}`);
471
+ }
472
+ }
473
+
474
+ if (!modelProbe) {
444
475
  // Fall back to auto-detection
445
476
  try {
446
477
  modelProbe = await probeModelAvailability(channel, channelType, { stopOnFirstAvailable: true });
@@ -512,7 +543,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
512
543
  }
513
544
  apiPath += '?beta=true';
514
545
 
515
- testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'claude-sonnet-4-20250514';
546
+ testModel = modelProbe?.preferredTestModel
547
+ || normalizeNonEmptyString(model)
548
+ || getConfiguredDefaultSpeedTestModel('claude');
516
549
  const sessionId = Math.random().toString(36).substring(2, 15);
517
550
  primaryRequestConfig = {
518
551
  apiPath,
@@ -550,7 +583,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
550
583
  };
551
584
  } else if (channelType === 'codex') {
552
585
  const apiPath = buildCodexResponsesPath(parsedUrl);
553
- testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gpt-5-codex';
586
+ testModel = modelProbe?.preferredTestModel
587
+ || normalizeNonEmptyString(model)
588
+ || getConfiguredDefaultSpeedTestModel('codex');
554
589
  const codexSessionId = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
555
590
 
556
591
  const baseBody = {
@@ -588,7 +623,9 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
588
623
  isStreamingResponse: true
589
624
  };
590
625
  } else if (channelType === 'gemini') {
591
- testModel = modelProbe?.preferredTestModel || normalizeNonEmptyString(model) || 'gemini-2.5-pro';
626
+ testModel = modelProbe?.preferredTestModel
627
+ || normalizeNonEmptyString(model)
628
+ || getConfiguredDefaultSpeedTestModel('gemini');
592
629
  const useCliFormat = shouldUseGeminiCliFormat(parsedUrl);
593
630
 
594
631
  const cliRequestConfig = {