@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
|
@@ -8,11 +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
|
+
const { createDecodedStream } = require('./services/response-decoder');
|
|
14
15
|
const eventBus = require('../plugins/event-bus');
|
|
15
|
-
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
16
16
|
const { getEffectiveApiKey } = require('./services/channels');
|
|
17
17
|
|
|
18
18
|
let proxyServer = null;
|
|
@@ -94,8 +94,7 @@ function redirectModel(originalModel, channel) {
|
|
|
94
94
|
* @returns {number} 成本(美元)
|
|
95
95
|
*/
|
|
96
96
|
function calculateCost(model, tokens) {
|
|
97
|
-
const
|
|
98
|
-
const pricing = resolveModelPricing('claude', model, hardcodedPricing, CLAUDE_BASE_PRICING);
|
|
97
|
+
const pricing = resolveModelPricing('claude', model, {}, CLAUDE_BASE_PRICING);
|
|
99
98
|
|
|
100
99
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
|
|
101
100
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : CLAUDE_BASE_PRICING.output;
|
|
@@ -334,11 +333,12 @@ async function startProxyServer(options = {}) {
|
|
|
334
333
|
cacheRead: 0,
|
|
335
334
|
model: ''
|
|
336
335
|
};
|
|
336
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
parsedStream.on('data', (chunk) => {
|
|
339
339
|
if (isResponseClosed) return;
|
|
340
340
|
|
|
341
|
-
buffer += chunk.toString();
|
|
341
|
+
buffer += chunk.toString('utf8');
|
|
342
342
|
|
|
343
343
|
const events = buffer.split('\n\n');
|
|
344
344
|
buffer = events.pop() || '';
|
|
@@ -448,9 +448,9 @@ async function startProxyServer(options = {}) {
|
|
|
448
448
|
}
|
|
449
449
|
};
|
|
450
450
|
|
|
451
|
-
|
|
451
|
+
parsedStream.on('end', finalize);
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
parsedStream.on('error', (err) => {
|
|
454
454
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
455
455
|
console.error('Proxy response error:', err);
|
|
456
456
|
}
|
|
@@ -3,6 +3,15 @@ const path = require('path');
|
|
|
3
3
|
const { getCodexDir } = require('./codex-config');
|
|
4
4
|
const { parseSession, parseSessionMeta, extractSessionMeta, readJSONL } = require('./codex-parser');
|
|
5
5
|
|
|
6
|
+
const COUNTS_CACHE_TTL_MS = 30 * 1000;
|
|
7
|
+
const FAST_META_READ_BYTES = 64 * 1024;
|
|
8
|
+
const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
|
|
9
|
+
|
|
10
|
+
let countsCache = {
|
|
11
|
+
expiresAt: 0,
|
|
12
|
+
value: EMPTY_COUNTS
|
|
13
|
+
};
|
|
14
|
+
|
|
6
15
|
/**
|
|
7
16
|
* 获取会话目录
|
|
8
17
|
*/
|
|
@@ -437,6 +446,7 @@ function deleteProject(projectName) {
|
|
|
437
446
|
console.error('[Codex] Failed to clean session order:', err.message);
|
|
438
447
|
}
|
|
439
448
|
|
|
449
|
+
invalidateProjectAndSessionCountsCache();
|
|
440
450
|
return { success: true, deletedCount };
|
|
441
451
|
}
|
|
442
452
|
|
|
@@ -524,6 +534,7 @@ function deleteSession(sessionId) {
|
|
|
524
534
|
// 忽略别名不存在的错误
|
|
525
535
|
}
|
|
526
536
|
|
|
537
|
+
invalidateProjectAndSessionCountsCache();
|
|
527
538
|
return { success: true };
|
|
528
539
|
}
|
|
529
540
|
|
|
@@ -577,6 +588,7 @@ function forkSession(sessionId) {
|
|
|
577
588
|
forkRelations[newSessionId] = sessionId;
|
|
578
589
|
saveForkRelations(forkRelations);
|
|
579
590
|
|
|
591
|
+
invalidateProjectAndSessionCountsCache();
|
|
580
592
|
return {
|
|
581
593
|
newSessionId,
|
|
582
594
|
forkedFrom: sessionId,
|
|
@@ -628,19 +640,106 @@ function saveProjectOrder(order) {
|
|
|
628
640
|
saveClaudeProjectOrder({ projectsDir: getCodexDir() }, order);
|
|
629
641
|
}
|
|
630
642
|
|
|
643
|
+
function invalidateProjectAndSessionCountsCache() {
|
|
644
|
+
countsCache.expiresAt = 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractCodexProjectNameFromMeta(metaPayload = {}) {
|
|
648
|
+
const repoUrl = metaPayload?.git?.repository_url || metaPayload?.git?.repositoryUrl;
|
|
649
|
+
if (typeof repoUrl === 'string' && repoUrl.trim()) {
|
|
650
|
+
const parsedName = repoUrl.split('/').pop();
|
|
651
|
+
if (parsedName) {
|
|
652
|
+
const normalized = parsedName.replace(/\.git$/i, '').trim();
|
|
653
|
+
if (normalized) return normalized;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const cwd = metaPayload?.cwd;
|
|
658
|
+
if (typeof cwd === 'string' && cwd.trim()) {
|
|
659
|
+
return path.basename(cwd.trim());
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return '';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function readSessionMetaPayloadFast(filePath) {
|
|
666
|
+
let fd;
|
|
667
|
+
try {
|
|
668
|
+
fd = fs.openSync(filePath, 'r');
|
|
669
|
+
const buffer = Buffer.alloc(FAST_META_READ_BYTES);
|
|
670
|
+
const bytesRead = fs.readSync(fd, buffer, 0, FAST_META_READ_BYTES, 0);
|
|
671
|
+
if (bytesRead <= 0) return null;
|
|
672
|
+
|
|
673
|
+
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
674
|
+
const lines = chunk.split('\n');
|
|
675
|
+
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
const trimmed = line.trim();
|
|
678
|
+
if (!trimmed) continue;
|
|
679
|
+
let parsed;
|
|
680
|
+
try {
|
|
681
|
+
parsed = JSON.parse(trimmed);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (parsed?.type === 'session_meta' && parsed?.payload && typeof parsed.payload === 'object') {
|
|
686
|
+
return parsed.payload;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch (err) {
|
|
690
|
+
return null;
|
|
691
|
+
} finally {
|
|
692
|
+
if (fd !== undefined) {
|
|
693
|
+
try {
|
|
694
|
+
fs.closeSync(fd);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
// ignore close errors
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function calculateProjectAndSessionCounts() {
|
|
705
|
+
const sessions = scanSessionFiles();
|
|
706
|
+
if (sessions.length === 0) {
|
|
707
|
+
return EMPTY_COUNTS;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const projectNames = new Set();
|
|
711
|
+
sessions.forEach((session) => {
|
|
712
|
+
const payload = readSessionMetaPayloadFast(session.filePath);
|
|
713
|
+
const projectName = extractCodexProjectNameFromMeta(payload || {});
|
|
714
|
+
if (projectName) {
|
|
715
|
+
projectNames.add(projectName);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
projectCount: projectNames.size,
|
|
721
|
+
sessionCount: sessions.length
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
631
725
|
/**
|
|
632
726
|
* 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
|
|
633
727
|
*/
|
|
634
728
|
function getProjectAndSessionCounts() {
|
|
729
|
+
const now = Date.now();
|
|
730
|
+
if (countsCache.expiresAt > now) {
|
|
731
|
+
return countsCache.value;
|
|
732
|
+
}
|
|
733
|
+
|
|
635
734
|
try {
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
sessionCount: sessions.length
|
|
735
|
+
const counts = calculateProjectAndSessionCounts();
|
|
736
|
+
countsCache = {
|
|
737
|
+
value: counts,
|
|
738
|
+
expiresAt: now + COUNTS_CACHE_TTL_MS
|
|
641
739
|
};
|
|
740
|
+
return counts;
|
|
642
741
|
} catch (err) {
|
|
643
|
-
return
|
|
742
|
+
return countsCache.value || EMPTY_COUNTS;
|
|
644
743
|
}
|
|
645
744
|
}
|
|
646
745
|
|
|
@@ -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 = {
|
|
@@ -47,8 +48,11 @@ const SHELL_CONFIG_FILES = process.platform === 'win32'
|
|
|
47
48
|
? [
|
|
48
49
|
'.bashrc',
|
|
49
50
|
'.bash_profile',
|
|
51
|
+
'.bash_login',
|
|
50
52
|
'.zshrc',
|
|
53
|
+
'.zshenv',
|
|
51
54
|
'.zprofile',
|
|
55
|
+
'.zlogin',
|
|
52
56
|
'.profile',
|
|
53
57
|
path.join('Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
54
58
|
path.join('Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
|
@@ -56,8 +60,11 @@ const SHELL_CONFIG_FILES = process.platform === 'win32'
|
|
|
56
60
|
: [
|
|
57
61
|
'.bashrc',
|
|
58
62
|
'.bash_profile',
|
|
63
|
+
'.bash_login',
|
|
59
64
|
'.zshrc',
|
|
65
|
+
'.zshenv',
|
|
60
66
|
'.zprofile',
|
|
67
|
+
'.zlogin',
|
|
61
68
|
'.profile'
|
|
62
69
|
];
|
|
63
70
|
|
|
@@ -86,8 +93,10 @@ function checkEnvConflicts(platform = null) {
|
|
|
86
93
|
// 3. 检测系统配置文件
|
|
87
94
|
conflicts.push(...checkSystemConfigs(keywords));
|
|
88
95
|
|
|
89
|
-
//
|
|
90
|
-
|
|
96
|
+
// 去重 + 只保留“真实冲突”(同名变量在多个来源且值不一致)
|
|
97
|
+
const deduplicated = deduplicateConflicts(conflicts);
|
|
98
|
+
const realConflicts = filterRealConflicts(deduplicated);
|
|
99
|
+
return sanitizeConflicts(realConflicts);
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
@@ -108,10 +117,15 @@ function checkProcessEnv(keywords) {
|
|
|
108
117
|
const conflicts = [];
|
|
109
118
|
|
|
110
119
|
for (const [key, value] of Object.entries(process.env)) {
|
|
120
|
+
if (!hasNonEmptyValue(value)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
if (matchesKeywords(key, keywords)) {
|
|
112
125
|
conflicts.push({
|
|
113
126
|
varName: key,
|
|
114
127
|
varValue: maskSensitiveValue(value),
|
|
128
|
+
valueFingerprint: hashValue(value),
|
|
115
129
|
sourceType: 'process',
|
|
116
130
|
sourcePath: 'Process Environment',
|
|
117
131
|
platform: detectPlatform(key)
|
|
@@ -183,11 +197,18 @@ function parseConfigFile(filePath, keywords) {
|
|
|
183
197
|
|
|
184
198
|
if (matched) {
|
|
185
199
|
const [, varName, varValue] = matched;
|
|
200
|
+
const normalizedValue = cleanValue(varValue);
|
|
201
|
+
|
|
202
|
+
// 空值通常是占位或已清理状态,不再视为冲突
|
|
203
|
+
if (!hasNonEmptyValue(normalizedValue)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
186
206
|
|
|
187
207
|
if (matchesKeywords(varName, keywords)) {
|
|
188
208
|
conflicts.push({
|
|
189
209
|
varName,
|
|
190
|
-
varValue: maskSensitiveValue(
|
|
210
|
+
varValue: maskSensitiveValue(normalizedValue),
|
|
211
|
+
valueFingerprint: hashValue(normalizedValue),
|
|
191
212
|
sourceType: 'file',
|
|
192
213
|
sourcePath: `${filePath}:${i + 1}`,
|
|
193
214
|
filePath,
|
|
@@ -219,17 +240,17 @@ function parseConfigFile(filePath, keywords) {
|
|
|
219
240
|
function matchesKeywords(varName, keywords) {
|
|
220
241
|
const upperName = varName.toUpperCase();
|
|
221
242
|
|
|
222
|
-
// 首先检查是否包含平台关键词
|
|
223
|
-
const hasKeyword = keywords.some(keyword => upperName.includes(keyword));
|
|
224
|
-
if (!hasKeyword) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
243
|
// 检查是否精确匹配已知敏感变量
|
|
229
244
|
if (EXACT_SENSITIVE_VARS.includes(upperName)) {
|
|
230
245
|
return true;
|
|
231
246
|
}
|
|
232
247
|
|
|
248
|
+
// 首先检查是否命中平台关键词(按 token 匹配,避免 OPENAI2 误判为 OPENAI)
|
|
249
|
+
const hasKeyword = keywords.some(keyword => matchesKeywordToken(upperName, keyword));
|
|
250
|
+
if (!hasKeyword) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
233
254
|
// 检查是否以敏感后缀结尾
|
|
234
255
|
const hasSensitiveSuffix = SENSITIVE_PATTERNS.some(suffix =>
|
|
235
256
|
upperName.endsWith(suffix)
|
|
@@ -238,6 +259,16 @@ function matchesKeywords(varName, keywords) {
|
|
|
238
259
|
return hasSensitiveSuffix;
|
|
239
260
|
}
|
|
240
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
|
+
|
|
241
272
|
/**
|
|
242
273
|
* 检测变量属于哪个平台
|
|
243
274
|
*/
|
|
@@ -266,6 +297,13 @@ function cleanValue(value) {
|
|
|
266
297
|
return cleaned;
|
|
267
298
|
}
|
|
268
299
|
|
|
300
|
+
/**
|
|
301
|
+
* 判断值是否为非空
|
|
302
|
+
*/
|
|
303
|
+
function hasNonEmptyValue(value) {
|
|
304
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
305
|
+
}
|
|
306
|
+
|
|
269
307
|
/**
|
|
270
308
|
* 遮蔽敏感值
|
|
271
309
|
*/
|
|
@@ -275,6 +313,56 @@ function maskSensitiveValue(value) {
|
|
|
275
313
|
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
|
276
314
|
}
|
|
277
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
|
+
|
|
278
366
|
/**
|
|
279
367
|
* 去重冲突列表
|
|
280
368
|
*/
|
|
@@ -124,11 +124,15 @@ function deleteEnvVars(conflicts) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// 同步清理当前服务进程中的同名变量,避免“删除后立即复检仍提示冲突”
|
|
128
|
+
const clearedProcessVars = clearProcessEnvVars(conflicts);
|
|
129
|
+
|
|
127
130
|
return {
|
|
128
131
|
backupPath: backupInfo.backupPath,
|
|
129
132
|
timestamp: backupInfo.timestamp,
|
|
130
133
|
results,
|
|
131
|
-
processConflictsSkipped: processConflicts.length
|
|
134
|
+
processConflictsSkipped: processConflicts.length,
|
|
135
|
+
clearedProcessVars
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
|
|
@@ -220,6 +224,30 @@ function groupByFile(conflicts) {
|
|
|
220
224
|
return groups;
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
/**
|
|
228
|
+
* 清理当前 Node 进程中的环境变量
|
|
229
|
+
*/
|
|
230
|
+
function clearProcessEnvVars(conflicts) {
|
|
231
|
+
const names = new Set();
|
|
232
|
+
|
|
233
|
+
for (const conflict of conflicts || []) {
|
|
234
|
+
const varName = String(conflict?.varName || '').trim();
|
|
235
|
+
if (varName) {
|
|
236
|
+
names.add(varName);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cleared = [];
|
|
241
|
+
for (const varName of names) {
|
|
242
|
+
if (Object.prototype.hasOwnProperty.call(process.env, varName)) {
|
|
243
|
+
delete process.env[varName];
|
|
244
|
+
cleared.push(varName);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return cleared;
|
|
249
|
+
}
|
|
250
|
+
|
|
223
251
|
/**
|
|
224
252
|
* 从文件中移除环境变量
|
|
225
253
|
*/
|
|
@@ -46,7 +46,8 @@ function loadChannels() {
|
|
|
46
46
|
modelRedirects: ch.modelRedirects || [],
|
|
47
47
|
speedTestModel: ch.speedTestModel || null,
|
|
48
48
|
wireApi: ch.wireApi || 'openai', // OpenCode 默认使用 OpenAI 兼容格式
|
|
49
|
-
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType)
|
|
49
|
+
gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType),
|
|
50
|
+
allowedModels: ch.allowedModels || []
|
|
50
51
|
};
|
|
51
52
|
normalized.providerKey = deriveProviderKey(normalized);
|
|
52
53
|
return normalized;
|
|
@@ -101,6 +102,7 @@ function createChannel(name, baseUrl, apiKey, extraConfig = {}) {
|
|
|
101
102
|
providerKey: extraConfig.providerKey || null,
|
|
102
103
|
presetId: extraConfig.presetId || null,
|
|
103
104
|
websiteUrl: extraConfig.websiteUrl || '',
|
|
105
|
+
allowedModels: extraConfig.allowedModels || [],
|
|
104
106
|
createdAt: Date.now(),
|
|
105
107
|
updatedAt: Date.now()
|
|
106
108
|
};
|