@adversity/coding-tool-x 3.1.2 → 3.1.4
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 +23 -0
- package/dist/web/assets/Analytics-Blo8_rhE.css +1 -0
- package/dist/web/assets/Analytics-DEIGaSFz.js +39 -0
- package/dist/web/assets/{ConfigTemplates-DvcbKKdS.js → ConfigTemplates-BZaehll1.js} +1 -1
- package/dist/web/assets/{Home-BJKPCBuk.css → Home-CyCIx4BA.css} +1 -1
- package/dist/web/assets/{Home-Cw-F_Wnu.js → Home-DIIH5bAk.js} +1 -1
- package/dist/web/assets/{PluginManager-jy_4GVxI.js → PluginManager-b4IlavFA.js} +1 -1
- package/dist/web/assets/{ProjectList-Df1-NcNr.js → ProjectList-QaqzjEea.js} +1 -1
- package/dist/web/assets/SessionList-CXUr6S7w.css +1 -0
- package/dist/web/assets/SessionList-Cz_hrGmL.js +1 -0
- package/dist/web/assets/{SkillManager-IRdseMKB.js → SkillManager-B-Rcb9xY.js} +1 -1
- package/dist/web/assets/{Terminal-BasTyDut.js → Terminal-C8CFJEkx.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-D-D2kK1V.js → WorkspaceManager-XaQj8BRE.js} +1 -1
- package/dist/web/assets/icons-BxcwoY5F.js +1 -0
- package/dist/web/assets/index-B_kPXCbH.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 -29
- package/src/config/loader.js +6 -3
- package/src/config/model-metadata.js +102 -350
- package/src/config/model-metadata.json +125 -0
- 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 +25 -51
- package/src/server/api/opencode-proxy.js +1 -1
- package/src/server/api/opencode-sessions.js +0 -7
- package/src/server/api/sessions.js +11 -68
- package/src/server/api/settings.js +66 -39
- package/src/server/api/skills.js +0 -44
- package/src/server/api/statistics.js +115 -1
- package/src/server/codex-proxy-server.js +26 -55
- package/src/server/gemini-proxy-server.js +15 -14
- package/src/server/index.js +0 -3
- package/src/server/opencode-proxy-server.js +45 -121
- package/src/server/proxy-server.js +2 -4
- 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 +73 -8
- package/src/server/services/plugins-service.js +37 -28
- package/src/server/services/pty-manager.js +22 -18
- 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/dist/web/assets/SessionList-BGJWyneI.css +0 -1
- package/dist/web/assets/SessionList-UWcZtC2r.js +0 -1
- package/dist/web/assets/icons-kcfLIMBB.js +0 -1
- package/dist/web/assets/index-CoB3zF0K.css +0 -1
- package/dist/web/assets/index-CryrSLv8.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
|
@@ -13,12 +13,12 @@ const { recordSuccess, recordFailure } = require('./services/channel-health');
|
|
|
13
13
|
const { loadConfig } = require('../config/loader');
|
|
14
14
|
const DEFAULT_CONFIG = require('../config/default');
|
|
15
15
|
const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
|
|
16
|
-
const {
|
|
16
|
+
const { resolveModelPricing } = require('./utils/pricing');
|
|
17
|
+
const { getDefaultSpeedTestModelByToolType } = require('../config/model-metadata');
|
|
17
18
|
const { recordRequest: recordOpenCodeRequest } = require('./services/opencode-statistics-service');
|
|
18
19
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
19
20
|
const { getEnabledChannels, getEffectiveApiKey } = require('./services/opencode-channels');
|
|
20
|
-
const {
|
|
21
|
-
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
21
|
+
const { fetchModelsFromProvider, getCachedModelInfo } = require('./services/model-detector');
|
|
22
22
|
|
|
23
23
|
let proxyServer = null;
|
|
24
24
|
let proxyApp = null;
|
|
@@ -32,7 +32,7 @@ const requestMetadata = new Map();
|
|
|
32
32
|
const printedRedirectCache = new Map();
|
|
33
33
|
|
|
34
34
|
// OpenAI 模型定价(每百万 tokens 的价格,单位:美元)
|
|
35
|
-
//
|
|
35
|
+
// 作为 model-metadata 未覆盖时的兜底值
|
|
36
36
|
const PRICING = {
|
|
37
37
|
'gpt-4o': { input: 2.5, output: 10 },
|
|
38
38
|
'gpt-4o-2024-11-20': { input: 2.5, output: 10 },
|
|
@@ -174,61 +174,33 @@ function resolveOpenCodeTarget(baseUrl = '', requestPath = '') {
|
|
|
174
174
|
* 计算请求成本
|
|
175
175
|
*/
|
|
176
176
|
function calculateCost(model, tokens) {
|
|
177
|
-
let
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// 非 Claude 模型,使用 PRICING 对象(OpenAI 等)
|
|
201
|
-
pricing = PRICING[model];
|
|
202
|
-
|
|
203
|
-
// 如果没有精确匹配,尝试模糊匹配
|
|
204
|
-
if (!pricing) {
|
|
205
|
-
const modelLower = model.toLowerCase();
|
|
206
|
-
if (modelLower.includes('gpt-4o-mini')) {
|
|
207
|
-
pricing = PRICING['gpt-4o-mini'];
|
|
208
|
-
} else if (modelLower.includes('gpt-4o')) {
|
|
209
|
-
pricing = PRICING['gpt-4o'];
|
|
210
|
-
} else if (modelLower.includes('gpt-4')) {
|
|
211
|
-
pricing = PRICING['gpt-4'];
|
|
212
|
-
} else if (modelLower.includes('gpt-3.5')) {
|
|
213
|
-
pricing = PRICING['gpt-3.5-turbo'];
|
|
214
|
-
} else if (modelLower.includes('o1-mini')) {
|
|
215
|
-
pricing = PRICING['o1-mini'];
|
|
216
|
-
} else if (modelLower.includes('o1-pro')) {
|
|
217
|
-
pricing = PRICING['o1-pro'];
|
|
218
|
-
} else if (modelLower.includes('o1')) {
|
|
219
|
-
pricing = PRICING['o1'];
|
|
220
|
-
} else if (modelLower.includes('o3-mini')) {
|
|
221
|
-
pricing = PRICING['o3-mini'];
|
|
222
|
-
} else if (modelLower.includes('o3')) {
|
|
223
|
-
pricing = PRICING['o3'];
|
|
224
|
-
} else if (modelLower.includes('o4-mini')) {
|
|
225
|
-
pricing = PRICING['o4-mini'];
|
|
226
|
-
}
|
|
177
|
+
let fallbackPricing = PRICING[model];
|
|
178
|
+
if (!fallbackPricing) {
|
|
179
|
+
const modelLower = String(model || '').toLowerCase();
|
|
180
|
+
if (modelLower.includes('gpt-4o-mini')) {
|
|
181
|
+
fallbackPricing = PRICING['gpt-4o-mini'];
|
|
182
|
+
} else if (modelLower.includes('gpt-4o')) {
|
|
183
|
+
fallbackPricing = PRICING['gpt-4o'];
|
|
184
|
+
} else if (modelLower.includes('gpt-4')) {
|
|
185
|
+
fallbackPricing = PRICING['gpt-4'];
|
|
186
|
+
} else if (modelLower.includes('gpt-3.5')) {
|
|
187
|
+
fallbackPricing = PRICING['gpt-3.5-turbo'];
|
|
188
|
+
} else if (modelLower.includes('o1-mini')) {
|
|
189
|
+
fallbackPricing = PRICING['o1-mini'];
|
|
190
|
+
} else if (modelLower.includes('o1-pro')) {
|
|
191
|
+
fallbackPricing = PRICING['o1-pro'];
|
|
192
|
+
} else if (modelLower.includes('o1')) {
|
|
193
|
+
fallbackPricing = PRICING['o1'];
|
|
194
|
+
} else if (modelLower.includes('o3-mini')) {
|
|
195
|
+
fallbackPricing = PRICING['o3-mini'];
|
|
196
|
+
} else if (modelLower.includes('o3')) {
|
|
197
|
+
fallbackPricing = PRICING['o3'];
|
|
198
|
+
} else if (modelLower.includes('o4-mini')) {
|
|
199
|
+
fallbackPricing = PRICING['o4-mini'];
|
|
227
200
|
}
|
|
228
201
|
}
|
|
229
202
|
|
|
230
|
-
|
|
231
|
-
pricing = resolvePricing('opencode', pricing, OPENCODE_BASE_PRICING);
|
|
203
|
+
const pricing = resolveModelPricing('opencode', model, fallbackPricing, OPENCODE_BASE_PRICING);
|
|
232
204
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : OPENCODE_BASE_PRICING.input;
|
|
233
205
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : OPENCODE_BASE_PRICING.output;
|
|
234
206
|
const cacheCreationRate = typeof pricing.cacheCreation === 'number' ? pricing.cacheCreation : inputRate * 1.25;
|
|
@@ -357,6 +329,17 @@ function normalizeGatewaySourceType(channel) {
|
|
|
357
329
|
return 'codex';
|
|
358
330
|
}
|
|
359
331
|
|
|
332
|
+
function isConverterEntryChannel(channel) {
|
|
333
|
+
const presetId = String(channel?.presetId || '').trim().toLowerCase();
|
|
334
|
+
return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getDefaultModelsByGatewaySourceType(gatewaySourceType) {
|
|
338
|
+
if (gatewaySourceType === 'claude') return [getDefaultSpeedTestModelByToolType('claude')];
|
|
339
|
+
if (gatewaySourceType === 'gemini') return [getDefaultSpeedTestModelByToolType('gemini')];
|
|
340
|
+
return [getDefaultSpeedTestModelByToolType('codex')];
|
|
341
|
+
}
|
|
342
|
+
|
|
360
343
|
function mapStainlessOs() {
|
|
361
344
|
switch (process.platform) {
|
|
362
345
|
case 'darwin':
|
|
@@ -408,47 +391,6 @@ function isChatCompletionsPath(pathname) {
|
|
|
408
391
|
return normalized.endsWith('/v1/chat/completions') || normalized.endsWith('/chat/completions');
|
|
409
392
|
}
|
|
410
393
|
|
|
411
|
-
function collectPreferredProbeModels(channel) {
|
|
412
|
-
const candidates = [];
|
|
413
|
-
if (!channel || typeof channel !== 'object') return candidates;
|
|
414
|
-
|
|
415
|
-
candidates.push(channel.model);
|
|
416
|
-
candidates.push(channel.speedTestModel);
|
|
417
|
-
|
|
418
|
-
const modelConfig = channel.modelConfig;
|
|
419
|
-
if (modelConfig && typeof modelConfig === 'object') {
|
|
420
|
-
candidates.push(modelConfig.model);
|
|
421
|
-
candidates.push(modelConfig.opusModel);
|
|
422
|
-
candidates.push(modelConfig.sonnetModel);
|
|
423
|
-
candidates.push(modelConfig.haikuModel);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (Array.isArray(channel.modelRedirects)) {
|
|
427
|
-
channel.modelRedirects.forEach((rule) => {
|
|
428
|
-
candidates.push(rule?.from);
|
|
429
|
-
candidates.push(rule?.to);
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const seen = new Set();
|
|
434
|
-
const models = [];
|
|
435
|
-
candidates.forEach((model) => {
|
|
436
|
-
if (typeof model !== 'string') return;
|
|
437
|
-
const trimmed = model.trim();
|
|
438
|
-
if (!trimmed) return;
|
|
439
|
-
const key = trimmed.toLowerCase();
|
|
440
|
-
if (seen.has(key)) return;
|
|
441
|
-
seen.add(key);
|
|
442
|
-
models.push(trimmed);
|
|
443
|
-
});
|
|
444
|
-
return models;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function isConverterPresetChannel(channel) {
|
|
448
|
-
const presetId = String(channel?.presetId || '').trim().toLowerCase();
|
|
449
|
-
return presetId === 'entry_claude' || presetId === 'entry_codex' || presetId === 'entry_gemini';
|
|
450
|
-
}
|
|
451
|
-
|
|
452
394
|
function extractTextFragments(value, fragments) {
|
|
453
395
|
if (value === null || value === undefined) return;
|
|
454
396
|
if (typeof value === 'string') {
|
|
@@ -4826,10 +4768,15 @@ async function collectProxyModelList(channels = [], options = {}) {
|
|
|
4826
4768
|
};
|
|
4827
4769
|
|
|
4828
4770
|
const forceRefresh = options.forceRefresh === true;
|
|
4829
|
-
const probePreferredModels = options.probePreferredModels === true;
|
|
4830
4771
|
const useCacheOnly = options.useCacheOnly === true;
|
|
4831
4772
|
// 模型列表聚合改为串行探测,避免并发触发上游会话窗口限流
|
|
4832
4773
|
for (const channel of channels) {
|
|
4774
|
+
if (isConverterEntryChannel(channel)) {
|
|
4775
|
+
const defaults = getDefaultModelsByGatewaySourceType(normalizeGatewaySourceType(channel));
|
|
4776
|
+
defaults.forEach(add);
|
|
4777
|
+
continue;
|
|
4778
|
+
}
|
|
4779
|
+
|
|
4833
4780
|
if (useCacheOnly) {
|
|
4834
4781
|
const cacheEntry = getCachedModelInfo(channel?.id);
|
|
4835
4782
|
const cachedFetched = Array.isArray(cacheEntry?.fetchedModels) ? cacheEntry.fetchedModels : [];
|
|
@@ -4845,30 +4792,7 @@ async function collectProxyModelList(channels = [], options = {}) {
|
|
|
4845
4792
|
const listedModels = Array.isArray(listResult?.models) ? listResult.models : [];
|
|
4846
4793
|
if (listedModels.length > 0) {
|
|
4847
4794
|
listedModels.forEach(add);
|
|
4848
|
-
// 默认沿用 /v1/models 结果;仅在显式要求时继续探测默认模型。
|
|
4849
|
-
if (!probePreferredModels) {
|
|
4850
|
-
continue;
|
|
4851
|
-
}
|
|
4852
4795
|
}
|
|
4853
|
-
|
|
4854
|
-
const shouldProbeByDefault = !!listResult?.disabledByConfig;
|
|
4855
|
-
|
|
4856
|
-
// 默认仅入口转换器渠道执行模型探测;若已禁用 /v1/models 则对全部渠道启用默认探测。
|
|
4857
|
-
// 当显式要求 probePreferredModels 时,无论 /v1/models 是否返回都执行默认模型探测。
|
|
4858
|
-
if (!probePreferredModels && !shouldProbeByDefault && !isConverterPresetChannel(channel)) {
|
|
4859
|
-
continue;
|
|
4860
|
-
}
|
|
4861
|
-
|
|
4862
|
-
const channelType = normalizeGatewaySourceType(channel);
|
|
4863
|
-
// eslint-disable-next-line no-await-in-loop
|
|
4864
|
-
const probe = await probeModelAvailability(channel, channelType, {
|
|
4865
|
-
forceRefresh,
|
|
4866
|
-
stopOnFirstAvailable: false,
|
|
4867
|
-
toolType: 'opencode',
|
|
4868
|
-
preferredModels: collectPreferredProbeModels(channel)
|
|
4869
|
-
});
|
|
4870
|
-
const available = Array.isArray(probe?.availableModels) ? probe.availableModels : [];
|
|
4871
|
-
available.forEach(add);
|
|
4872
4796
|
} catch (err) {
|
|
4873
4797
|
console.warn(`[OpenCode Proxy] Build model list failed for ${channel?.name || channel?.id || 'unknown'}:`, err.message);
|
|
4874
4798
|
}
|
|
@@ -8,12 +8,11 @@ const { recordSuccess, recordFailure } = require('./services/channel-health');
|
|
|
8
8
|
const { broadcastLog, broadcastSchedulerState } = require('./websocket-server');
|
|
9
9
|
const { loadConfig } = require('../config/loader');
|
|
10
10
|
const DEFAULT_CONFIG = require('../config/default');
|
|
11
|
-
const {
|
|
11
|
+
const { resolveModelPricing } = require('./utils/pricing');
|
|
12
12
|
const { recordRequest } = require('./services/statistics-service');
|
|
13
13
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
14
14
|
const { createDecodedStream } = require('./services/response-decoder');
|
|
15
15
|
const eventBus = require('../plugins/event-bus');
|
|
16
|
-
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
17
16
|
const { getEffectiveApiKey } = require('./services/channels');
|
|
18
17
|
|
|
19
18
|
let proxyServer = null;
|
|
@@ -95,8 +94,7 @@ function redirectModel(originalModel, channel) {
|
|
|
95
94
|
* @returns {number} 成本(美元)
|
|
96
95
|
*/
|
|
97
96
|
function calculateCost(model, tokens) {
|
|
98
|
-
const
|
|
99
|
-
const pricing = resolveModelPricing('claude', model, hardcodedPricing, CLAUDE_BASE_PRICING);
|
|
97
|
+
const pricing = resolveModelPricing('claude', model, {}, CLAUDE_BASE_PRICING);
|
|
100
98
|
|
|
101
99
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
|
|
102
100
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : CLAUDE_BASE_PRICING.output;
|
|
@@ -12,9 +12,6 @@ const { RepoScannerBase } = require('./repo-scanner-base');
|
|
|
12
12
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
13
13
|
const {
|
|
14
14
|
parseCommandContent,
|
|
15
|
-
detectCommandFormat,
|
|
16
|
-
convertCommandToCodex,
|
|
17
|
-
convertCommandToClaude,
|
|
18
15
|
parseFrontmatter
|
|
19
16
|
} = require('./format-converter');
|
|
20
17
|
|
|
@@ -569,32 +566,6 @@ class CommandsService {
|
|
|
569
566
|
|
|
570
567
|
// ==================== 格式转换 ====================
|
|
571
568
|
|
|
572
|
-
/**
|
|
573
|
-
* 转换命令格式
|
|
574
|
-
* @param {string} content - 命令内容
|
|
575
|
-
* @param {string} targetFormat - 目标格式 ('claude' | 'codex')
|
|
576
|
-
*/
|
|
577
|
-
convertCommandFormat(content, targetFormat) {
|
|
578
|
-
const sourceFormat = detectCommandFormat(content);
|
|
579
|
-
|
|
580
|
-
if (sourceFormat === targetFormat) {
|
|
581
|
-
return { content, warnings: [], format: targetFormat };
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
if (targetFormat === 'codex') {
|
|
585
|
-
return convertCommandToCodex(content);
|
|
586
|
-
} else {
|
|
587
|
-
return convertCommandToClaude(content);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* 检测命令格式
|
|
593
|
-
* @param {string} content - 命令内容
|
|
594
|
-
*/
|
|
595
|
-
detectFormat(content) {
|
|
596
|
-
return detectCommandFormat(content);
|
|
597
|
-
}
|
|
598
569
|
}
|
|
599
570
|
|
|
600
571
|
module.exports = {
|
|
@@ -28,6 +28,7 @@ const BUILTIN_TEMPLATES = [
|
|
|
28
28
|
id: 'full-stack',
|
|
29
29
|
name: '全栈开发',
|
|
30
30
|
description: '前后端全栈开发配置,包含代码编辑、文档查询、版本控制等常用工具',
|
|
31
|
+
cliType: 'claude',
|
|
31
32
|
// 兼容旧字段
|
|
32
33
|
claudeMd: { enabled: false, content: '' },
|
|
33
34
|
// 新的多 AI 配置
|
|
@@ -159,6 +160,7 @@ You are an experienced full-stack developer focused on delivering high-quality c
|
|
|
159
160
|
id: 'architecture',
|
|
160
161
|
name: '方案设计',
|
|
161
162
|
description: '专注于技术方案设计、架构评审、系统设计,适合需求分析和技术决策场景',
|
|
163
|
+
cliType: 'claude',
|
|
162
164
|
claudeMd: { enabled: false, content: '' },
|
|
163
165
|
aiConfigs: {
|
|
164
166
|
claude: {
|
|
@@ -308,6 +310,7 @@ You are a senior technical architect focused on system design and technical plan
|
|
|
308
310
|
id: 'code-review',
|
|
309
311
|
name: '代码审查',
|
|
310
312
|
description: '专注于代码审查、质量评估、安全检查,适合 PR Review 和代码质量改进',
|
|
313
|
+
cliType: 'claude',
|
|
311
314
|
claudeMd: { enabled: false, content: '' },
|
|
312
315
|
aiConfigs: {
|
|
313
316
|
claude: {
|
|
@@ -467,6 +470,7 @@ For each issue:
|
|
|
467
470
|
id: 'minimal',
|
|
468
471
|
name: '最小配置',
|
|
469
472
|
description: '纯净环境,不添加任何额外配置,适合已有完善配置的项目',
|
|
473
|
+
cliType: 'claude',
|
|
470
474
|
claudeMd: { enabled: false, content: '' },
|
|
471
475
|
aiConfigs: {
|
|
472
476
|
claude: { enabled: false, content: '' },
|
|
@@ -619,6 +623,7 @@ function createCustomTemplate(template) {
|
|
|
619
623
|
id,
|
|
620
624
|
name: template.name,
|
|
621
625
|
description: template.description || '',
|
|
626
|
+
cliType: template.cliType || 'claude',
|
|
622
627
|
claudeMd: template.claudeMd || { enabled: false, content: '' },
|
|
623
628
|
aiConfigs: normalizeAiConfigs(template.aiConfigs, template.claudeMd),
|
|
624
629
|
skills: template.skills || [],
|
|
@@ -651,6 +656,7 @@ function updateCustomTemplate(id, updates) {
|
|
|
651
656
|
custom[index] = {
|
|
652
657
|
...custom[index],
|
|
653
658
|
...updates,
|
|
659
|
+
cliType: updates.cliType !== undefined ? updates.cliType : (custom[index].cliType || 'claude'),
|
|
654
660
|
aiConfigs: normalizeAiConfigs(updates.aiConfigs || custom[index].aiConfigs, updates.claudeMd || custom[index].claudeMd),
|
|
655
661
|
id: custom[index].id, // 保持 ID 不变
|
|
656
662
|
isBuiltin: false,
|
|
@@ -971,12 +977,15 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
971
977
|
}
|
|
972
978
|
}
|
|
973
979
|
|
|
974
|
-
// 2. 写入 Agents
|
|
980
|
+
// 2. 写入 Agents(根据选中的 AI 类型决定写入哪些目录)
|
|
975
981
|
if (template.agents?.length > 0) {
|
|
976
|
-
const agentTargets = [
|
|
977
|
-
|
|
978
|
-
{ baseDir: path.join(targetDir, '.
|
|
979
|
-
|
|
982
|
+
const agentTargets = [];
|
|
983
|
+
if (aiConfigTypes.includes('claude')) {
|
|
984
|
+
agentTargets.push({ baseDir: path.join(targetDir, '.claude', 'agents'), prefix: '.claude/agents' });
|
|
985
|
+
}
|
|
986
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
987
|
+
agentTargets.push({ baseDir: path.join(targetDir, '.opencode', 'agents'), prefix: '.opencode/agents' });
|
|
988
|
+
}
|
|
980
989
|
|
|
981
990
|
for (const target of agentTargets) {
|
|
982
991
|
ensureDir(target.baseDir);
|
|
@@ -994,12 +1003,15 @@ function applyTemplateToProject(targetDir, templateId, options = {}) {
|
|
|
994
1003
|
}
|
|
995
1004
|
}
|
|
996
1005
|
|
|
997
|
-
// 3. 写入 Commands
|
|
1006
|
+
// 3. 写入 Commands(根据选中的 AI 类型决定写入哪些目录)
|
|
998
1007
|
if (template.commands?.length > 0) {
|
|
999
|
-
const commandTargets = [
|
|
1000
|
-
|
|
1001
|
-
{ baseDir: path.join(targetDir, '.
|
|
1002
|
-
|
|
1008
|
+
const commandTargets = [];
|
|
1009
|
+
if (aiConfigTypes.includes('claude')) {
|
|
1010
|
+
commandTargets.push({ baseDir: path.join(targetDir, '.claude', 'commands'), prefix: '.claude/commands' });
|
|
1011
|
+
}
|
|
1012
|
+
if (aiConfigTypes.includes('opencode')) {
|
|
1013
|
+
commandTargets.push({ baseDir: path.join(targetDir, '.opencode', 'commands'), prefix: '.opencode/commands' });
|
|
1014
|
+
}
|
|
1003
1015
|
|
|
1004
1016
|
for (const target of commandTargets) {
|
|
1005
1017
|
ensureDir(target.baseDir);
|
|
@@ -1189,16 +1201,16 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
1189
1201
|
preview.summary.skills = template.skills.length;
|
|
1190
1202
|
}
|
|
1191
1203
|
|
|
1192
|
-
// 检查 Agents
|
|
1204
|
+
// 检查 Agents(根据选中的 AI 类型决定预览哪些目录)
|
|
1193
1205
|
if (template.agents?.length > 0) {
|
|
1206
|
+
const agentPrefixes = [];
|
|
1207
|
+
if (aiConfigTypes.includes('claude')) agentPrefixes.push('.claude/agents');
|
|
1208
|
+
if (aiConfigTypes.includes('opencode')) agentPrefixes.push('.opencode/agents');
|
|
1209
|
+
|
|
1194
1210
|
for (const agent of template.agents) {
|
|
1195
1211
|
const fileName = agent.fileName || agent.name.toLowerCase().replace(/\s+/g, '-');
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
`.opencode/agents/${fileName}.md`
|
|
1199
|
-
];
|
|
1200
|
-
|
|
1201
|
-
for (const relativePath of relativePaths) {
|
|
1212
|
+
for (const prefix of agentPrefixes) {
|
|
1213
|
+
const relativePath = `${prefix}/${fileName}.md`;
|
|
1202
1214
|
const fullPath = path.join(targetDir, relativePath);
|
|
1203
1215
|
if (fs.existsSync(fullPath)) {
|
|
1204
1216
|
preview.willOverwrite.push(relativePath);
|
|
@@ -1210,19 +1222,17 @@ function previewTemplateApplication(targetDir, templateId, options = {}) {
|
|
|
1210
1222
|
}
|
|
1211
1223
|
}
|
|
1212
1224
|
|
|
1213
|
-
// 检查 Commands
|
|
1225
|
+
// 检查 Commands(根据选中的 AI 类型决定预览哪些目录)
|
|
1214
1226
|
if (template.commands?.length > 0) {
|
|
1227
|
+
const commandPrefixes = [];
|
|
1228
|
+
if (aiConfigTypes.includes('claude')) commandPrefixes.push('.claude/commands');
|
|
1229
|
+
if (aiConfigTypes.includes('opencode')) commandPrefixes.push('.opencode/commands');
|
|
1230
|
+
|
|
1215
1231
|
for (const command of template.commands) {
|
|
1216
|
-
const
|
|
1217
|
-
command.namespace
|
|
1218
|
-
?
|
|
1219
|
-
:
|
|
1220
|
-
command.namespace
|
|
1221
|
-
? `.opencode/commands/${command.namespace}/${command.name}.md`
|
|
1222
|
-
: `.opencode/commands/${command.name}.md`
|
|
1223
|
-
];
|
|
1224
|
-
|
|
1225
|
-
for (const relativePath of relativePaths) {
|
|
1232
|
+
for (const prefix of commandPrefixes) {
|
|
1233
|
+
const relativePath = command.namespace
|
|
1234
|
+
? `${prefix}/${command.namespace}/${command.name}.md`
|
|
1235
|
+
: `${prefix}/${command.name}.md`;
|
|
1226
1236
|
const fullPath = path.join(targetDir, relativePath);
|
|
1227
1237
|
if (fs.existsSync(fullPath)) {
|
|
1228
1238
|
preview.willOverwrite.push(relativePath);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
|
+
const crypto = require('crypto');
|
|
11
12
|
|
|
12
13
|
// 各平台需要检测的环境变量关键词
|
|
13
14
|
const PLATFORM_KEYWORDS = {
|
|
@@ -92,8 +93,10 @@ function checkEnvConflicts(platform = null) {
|
|
|
92
93
|
// 3. 检测系统配置文件
|
|
93
94
|
conflicts.push(...checkSystemConfigs(keywords));
|
|
94
95
|
|
|
95
|
-
//
|
|
96
|
-
|
|
96
|
+
// 去重 + 只保留“真实冲突”(同名变量在多个来源且值不一致)
|
|
97
|
+
const deduplicated = deduplicateConflicts(conflicts);
|
|
98
|
+
const realConflicts = filterRealConflicts(deduplicated);
|
|
99
|
+
return sanitizeConflicts(realConflicts);
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/**
|
|
@@ -122,6 +125,7 @@ function checkProcessEnv(keywords) {
|
|
|
122
125
|
conflicts.push({
|
|
123
126
|
varName: key,
|
|
124
127
|
varValue: maskSensitiveValue(value),
|
|
128
|
+
valueFingerprint: hashValue(value),
|
|
125
129
|
sourceType: 'process',
|
|
126
130
|
sourcePath: 'Process Environment',
|
|
127
131
|
platform: detectPlatform(key)
|
|
@@ -204,6 +208,7 @@ function parseConfigFile(filePath, keywords) {
|
|
|
204
208
|
conflicts.push({
|
|
205
209
|
varName,
|
|
206
210
|
varValue: maskSensitiveValue(normalizedValue),
|
|
211
|
+
valueFingerprint: hashValue(normalizedValue),
|
|
207
212
|
sourceType: 'file',
|
|
208
213
|
sourcePath: `${filePath}:${i + 1}`,
|
|
209
214
|
filePath,
|
|
@@ -235,17 +240,17 @@ function parseConfigFile(filePath, keywords) {
|
|
|
235
240
|
function matchesKeywords(varName, keywords) {
|
|
236
241
|
const upperName = varName.toUpperCase();
|
|
237
242
|
|
|
238
|
-
// 首先检查是否包含平台关键词
|
|
239
|
-
const hasKeyword = keywords.some(keyword => upperName.includes(keyword));
|
|
240
|
-
if (!hasKeyword) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
243
|
// 检查是否精确匹配已知敏感变量
|
|
245
244
|
if (EXACT_SENSITIVE_VARS.includes(upperName)) {
|
|
246
245
|
return true;
|
|
247
246
|
}
|
|
248
247
|
|
|
248
|
+
// 首先检查是否命中平台关键词(按 token 匹配,避免 OPENAI2 误判为 OPENAI)
|
|
249
|
+
const hasKeyword = keywords.some(keyword => matchesKeywordToken(upperName, keyword));
|
|
250
|
+
if (!hasKeyword) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
249
254
|
// 检查是否以敏感后缀结尾
|
|
250
255
|
const hasSensitiveSuffix = SENSITIVE_PATTERNS.some(suffix =>
|
|
251
256
|
upperName.endsWith(suffix)
|
|
@@ -254,6 +259,16 @@ function matchesKeywords(varName, keywords) {
|
|
|
254
259
|
return hasSensitiveSuffix;
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
function matchesKeywordToken(varName, keyword) {
|
|
263
|
+
const token = String(keyword || '').trim().toUpperCase();
|
|
264
|
+
if (!token) return false;
|
|
265
|
+
|
|
266
|
+
return varName === token ||
|
|
267
|
+
varName.startsWith(`${token}_`) ||
|
|
268
|
+
varName.endsWith(`_${token}`) ||
|
|
269
|
+
varName.includes(`_${token}_`);
|
|
270
|
+
}
|
|
271
|
+
|
|
257
272
|
/**
|
|
258
273
|
* 检测变量属于哪个平台
|
|
259
274
|
*/
|
|
@@ -298,6 +313,56 @@ function maskSensitiveValue(value) {
|
|
|
298
313
|
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
|
299
314
|
}
|
|
300
315
|
|
|
316
|
+
function hashValue(value) {
|
|
317
|
+
return crypto
|
|
318
|
+
.createHash('sha256')
|
|
319
|
+
.update(String(value ?? ''), 'utf8')
|
|
320
|
+
.digest('hex');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function filterRealConflicts(conflicts) {
|
|
324
|
+
const grouped = new Map();
|
|
325
|
+
|
|
326
|
+
for (const conflict of conflicts) {
|
|
327
|
+
const key = String(conflict.varName || '').toUpperCase();
|
|
328
|
+
if (!grouped.has(key)) {
|
|
329
|
+
grouped.set(key, []);
|
|
330
|
+
}
|
|
331
|
+
grouped.get(key).push(conflict);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const results = [];
|
|
335
|
+
for (const group of grouped.values()) {
|
|
336
|
+
if (group.length < 2) {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const sourceCount = new Set(group.map(item => item.sourcePath)).size;
|
|
341
|
+
if (sourceCount < 2) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const valueVariants = new Set(
|
|
346
|
+
group
|
|
347
|
+
.map(item => item.valueFingerprint)
|
|
348
|
+
.filter(Boolean)
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// 同名变量在多个来源但值一致,不算冲突
|
|
352
|
+
if (valueVariants.size <= 1) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
results.push(...group);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function sanitizeConflicts(conflicts) {
|
|
363
|
+
return conflicts.map(({ valueFingerprint, ...rest }) => rest);
|
|
364
|
+
}
|
|
365
|
+
|
|
301
366
|
/**
|
|
302
367
|
* 去重冲突列表
|
|
303
368
|
*/
|
|
@@ -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
|
}
|