@adversity/coding-tool-x 3.1.2 → 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 (59) hide show
  1. package/CHANGELOG.md +17 -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-DvcbKKdS.js → ConfigTemplates-Bf_11LhH.js} +1 -1
  5. package/dist/web/assets/{Home-Cw-F_Wnu.js → Home-BRnW4FTS.js} +1 -1
  6. package/dist/web/assets/{Home-BJKPCBuk.css → Home-CyCIx4BA.css} +1 -1
  7. package/dist/web/assets/{PluginManager-jy_4GVxI.js → PluginManager-B9J32GhW.js} +1 -1
  8. package/dist/web/assets/{ProjectList-Df1-NcNr.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-IRdseMKB.js → SkillManager-CVBr0CLi.js} +1 -1
  12. package/dist/web/assets/{Terminal-BasTyDut.js → Terminal-D2Xe_Q0H.js} +1 -1
  13. package/dist/web/assets/{WorkspaceManager-D-D2kK1V.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 -29
  23. package/src/config/loader.js +6 -3
  24. package/src/config/model-metadata.js +102 -350
  25. package/src/config/model-metadata.json +125 -0
  26. package/src/server/api/channels.js +16 -39
  27. package/src/server/api/codex-channels.js +15 -43
  28. package/src/server/api/commands.js +0 -77
  29. package/src/server/api/config.js +4 -1
  30. package/src/server/api/gemini-channels.js +16 -40
  31. package/src/server/api/opencode-channels.js +25 -51
  32. package/src/server/api/opencode-proxy.js +1 -1
  33. package/src/server/api/opencode-sessions.js +0 -7
  34. package/src/server/api/sessions.js +11 -68
  35. package/src/server/api/settings.js +66 -39
  36. package/src/server/api/skills.js +0 -44
  37. package/src/server/api/statistics.js +115 -1
  38. package/src/server/codex-proxy-server.js +26 -55
  39. package/src/server/gemini-proxy-server.js +15 -14
  40. package/src/server/index.js +0 -3
  41. package/src/server/opencode-proxy-server.js +45 -121
  42. package/src/server/proxy-server.js +2 -4
  43. package/src/server/services/commands-service.js +0 -29
  44. package/src/server/services/config-templates-service.js +38 -28
  45. package/src/server/services/env-checker.js +73 -8
  46. package/src/server/services/plugins-service.js +37 -28
  47. package/src/server/services/pty-manager.js +22 -18
  48. package/src/server/services/skill-service.js +1 -49
  49. package/src/server/services/speed-test.js +40 -3
  50. package/src/server/services/statistics-service.js +238 -1
  51. package/src/server/utils/pricing.js +51 -60
  52. package/dist/web/assets/SessionList-BGJWyneI.css +0 -1
  53. package/dist/web/assets/SessionList-UWcZtC2r.js +0 -1
  54. package/dist/web/assets/icons-kcfLIMBB.js +0 -1
  55. package/dist/web/assets/index-CoB3zF0K.css +0 -1
  56. package/dist/web/assets/index-CryrSLv8.js +0 -2
  57. package/dist/web/assets/naive-ui-CSrLusZZ.js +0 -1
  58. package/src/server/api/convert.js +0 -260
  59. 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 { resolvePricing } = require('./utils/pricing');
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 { probeModelAvailability, fetchModelsFromProvider, getCachedModelInfo } = require('./services/model-detector');
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
- // Claude 模型使用 config/model-pricing.js 中的集中定价
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 pricing;
178
-
179
- // 首先检查是否是 Claude 模型,使用集中定价
180
- if (model.startsWith('claude-') || model.toLowerCase().includes('claude')) {
181
- pricing = CLAUDE_MODEL_PRICING[model];
182
-
183
- // 如果没有精确匹配,尝试模糊匹配 Claude 模型
184
- if (!pricing) {
185
- const modelLower = model.toLowerCase();
186
- // 查找最接近的 Claude 模型
187
- for (const [key, value] of Object.entries(CLAUDE_MODEL_PRICING)) {
188
- if (key.toLowerCase().includes(modelLower) || modelLower.includes(key.toLowerCase())) {
189
- pricing = value;
190
- break;
191
- }
192
- }
193
- }
194
-
195
- // 如果仍然没有找到,使用默认 Sonnet 定价
196
- if (!pricing) {
197
- pricing = CLAUDE_MODEL_PRICING['claude-sonnet-4-5-20250929'];
198
- }
199
- } else {
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 { resolvePricing, resolveModelPricing } = require('./utils/pricing');
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 hardcodedPricing = CLAUDE_MODEL_PRICING[model] || {};
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
- { baseDir: path.join(targetDir, '.claude', 'agents'), prefix: '.claude/agents' },
978
- { baseDir: path.join(targetDir, '.opencode', 'agents'), prefix: '.opencode/agents' }
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
- { baseDir: path.join(targetDir, '.claude', 'commands'), prefix: '.claude/commands' },
1001
- { baseDir: path.join(targetDir, '.opencode', 'commands'), prefix: '.opencode/commands' }
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 relativePaths = [
1197
- `.claude/agents/${fileName}.md`,
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 relativePaths = [
1217
- command.namespace
1218
- ? `.claude/commands/${command.namespace}/${command.name}.md`
1219
- : `.claude/commands/${command.name}.md`,
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
- return deduplicateConflicts(conflicts);
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
- 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
  }