@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.
- package/CHANGELOG.md +41 -0
- package/dist/web/assets/Analytics-BIqc8Rin.css +1 -0
- package/dist/web/assets/Analytics-D2V09DHH.js +39 -0
- package/dist/web/assets/{ConfigTemplates-ZrK_s7ma.js → ConfigTemplates-Bf_11LhH.js} +1 -1
- package/dist/web/assets/Home-BRnW4FTS.js +1 -0
- package/dist/web/assets/Home-CyCIx4BA.css +1 -0
- package/dist/web/assets/{PluginManager-BD7QUZbU.js → PluginManager-B9J32GhW.js} +1 -1
- package/dist/web/assets/{ProjectList-DRb1DuHV.js → ProjectList-5a19MWJk.js} +1 -1
- package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
- package/dist/web/assets/SessionList-Cxg5bAdT.js +1 -0
- package/dist/web/assets/{SkillManager-C1xG5B4Q.js → SkillManager-CVBr0CLi.js} +1 -1
- package/dist/web/assets/{Terminal-DksBo_lM.js → Terminal-D2Xe_Q0H.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Burx7XOo.js → WorkspaceManager-C7dwV94C.js} +1 -1
- package/dist/web/assets/icons-BxcwoY5F.js +1 -0
- package/dist/web/assets/index-BS9RA6SN.js +2 -0
- package/dist/web/assets/index-DUNAVDGb.css +1 -0
- package/dist/web/assets/naive-ui-BIXcURHZ.js +1 -0
- package/dist/web/assets/{vendors-CO3Upi1d.js → vendors-i5CBGnlm.js} +1 -1
- package/dist/web/assets/{vue-vendor-DqyWIXEb.js → vue-vendor-PKd8utv_.js} +1 -1
- package/dist/web/index.html +6 -6
- package/package.json +1 -1
- package/src/config/default.js +7 -27
- package/src/config/loader.js +6 -3
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +23 -93
- package/src/server/api/channels.js +16 -39
- package/src/server/api/codex-channels.js +15 -43
- package/src/server/api/commands.js +0 -77
- package/src/server/api/config.js +4 -1
- package/src/server/api/gemini-channels.js +16 -40
- package/src/server/api/opencode-channels.js +108 -56
- package/src/server/api/opencode-proxy.js +42 -33
- package/src/server/api/opencode-sessions.js +4 -69
- package/src/server/api/sessions.js +11 -68
- package/src/server/api/settings.js +138 -0
- package/src/server/api/skills.js +0 -44
- package/src/server/api/statistics.js +115 -1
- package/src/server/codex-proxy-server.js +32 -59
- package/src/server/gemini-proxy-server.js +21 -18
- package/src/server/index.js +13 -7
- package/src/server/opencode-proxy-server.js +1232 -197
- package/src/server/proxy-server.js +8 -8
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/commands-service.js +0 -29
- package/src/server/services/config-templates-service.js +38 -28
- package/src/server/services/env-checker.js +97 -9
- package/src/server/services/env-manager.js +29 -1
- package/src/server/services/opencode-channels.js +3 -1
- package/src/server/services/opencode-sessions.js +486 -218
- package/src/server/services/opencode-settings-manager.js +172 -36
- package/src/server/services/plugins-service.js +37 -28
- package/src/server/services/pty-manager.js +22 -18
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/skill-service.js +1 -49
- package/src/server/services/speed-test.js +40 -3
- package/src/server/services/statistics-service.js +238 -1
- package/src/server/utils/pricing.js +51 -60
- package/src/server/websocket-server.js +24 -5
- package/dist/web/assets/Home-B8YfhZ3c.js +0 -1
- package/dist/web/assets/Home-Di2qsylF.css +0 -1
- package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
- package/dist/web/assets/SessionList-lZ0LKzfT.js +0 -1
- package/dist/web/assets/icons-kcfLIMBB.js +0 -1
- package/dist/web/assets/index-Ufv5rCa5.css +0 -1
- package/dist/web/assets/index-lAkrRC3h.js +0 -2
- package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
- package/src/server/api/convert.js +0 -260
- 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
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
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 `${
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
273
|
-
|
|
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 === '
|
|
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
|
|
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
|
|
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
|
|
626
|
+
testModel = modelProbe?.preferredTestModel
|
|
627
|
+
|| normalizeNonEmptyString(model)
|
|
628
|
+
|| getConfiguredDefaultSpeedTestModel('gemini');
|
|
592
629
|
const useCliFormat = shouldUseGeminiCliFormat(parsedUrl);
|
|
593
630
|
|
|
594
631
|
const cliRequestConfig = {
|