coding-tool-x 3.2.0
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 +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Cache Manager
|
|
3
|
+
*
|
|
4
|
+
* 提供全局缓存管理,支持TTL、自动清理、LRU策略
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class CacheManager {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.caches = new Map();
|
|
10
|
+
this.ttls = new Map();
|
|
11
|
+
this.accessTimes = new Map();
|
|
12
|
+
this.maxSize = options.maxSize || 1000; // 最大缓存条目数
|
|
13
|
+
this.defaultTTL = options.defaultTTL || 300000; // 默认5分钟
|
|
14
|
+
this.cleanupInterval = options.cleanupInterval || 60000; // 1分钟清理一次
|
|
15
|
+
|
|
16
|
+
// 启动自动清理
|
|
17
|
+
this.startCleanup();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 设置缓存
|
|
22
|
+
* @param {string} key - 缓存键
|
|
23
|
+
* @param {any} value - 缓存值
|
|
24
|
+
* @param {number} ttl - 过期时间(毫秒),默认使用defaultTTL
|
|
25
|
+
*/
|
|
26
|
+
set(key, value, ttl) {
|
|
27
|
+
const expiryTime = Date.now() + (ttl || this.defaultTTL);
|
|
28
|
+
|
|
29
|
+
// 如果缓存已满,清理最少使用的条目
|
|
30
|
+
if (this.caches.size >= this.maxSize) {
|
|
31
|
+
this.evictLRU();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.caches.set(key, value);
|
|
35
|
+
this.ttls.set(key, expiryTime);
|
|
36
|
+
this.accessTimes.set(key, Date.now());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 获取缓存
|
|
41
|
+
* @param {string} key - 缓存键
|
|
42
|
+
* @returns {any|null} 缓存值,不存在或过期返回null
|
|
43
|
+
*/
|
|
44
|
+
get(key) {
|
|
45
|
+
if (!this.caches.has(key)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 检查是否过期
|
|
50
|
+
if (Date.now() > this.ttls.get(key)) {
|
|
51
|
+
this.delete(key);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 更新访问时间(LRU)
|
|
56
|
+
this.accessTimes.set(key, Date.now());
|
|
57
|
+
return this.caches.get(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 删除缓存
|
|
62
|
+
* @param {string} key - 缓存键
|
|
63
|
+
*/
|
|
64
|
+
delete(key) {
|
|
65
|
+
this.caches.delete(key);
|
|
66
|
+
this.ttls.delete(key);
|
|
67
|
+
this.accessTimes.delete(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 清空所有缓存
|
|
72
|
+
*/
|
|
73
|
+
clear() {
|
|
74
|
+
this.caches.clear();
|
|
75
|
+
this.ttls.clear();
|
|
76
|
+
this.accessTimes.clear();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 检查缓存是否存在且有效
|
|
81
|
+
* @param {string} key - 缓存键
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
has(key) {
|
|
85
|
+
return this.get(key) !== null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取或设置缓存(如果不存在则调用工厂函数)
|
|
90
|
+
* @param {string} key - 缓存键
|
|
91
|
+
* @param {Function} factory - 工厂函数,返回值或Promise
|
|
92
|
+
* @param {number} ttl - 过期时间
|
|
93
|
+
* @returns {any|Promise<any>}
|
|
94
|
+
*/
|
|
95
|
+
async getOrSet(key, factory, ttl) {
|
|
96
|
+
const cached = this.get(key);
|
|
97
|
+
if (cached !== null) {
|
|
98
|
+
return cached;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const value = await factory();
|
|
102
|
+
this.set(key, value, ttl);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* LRU驱逐策略:移除最久未使用的条目
|
|
108
|
+
*/
|
|
109
|
+
evictLRU() {
|
|
110
|
+
let oldestKey = null;
|
|
111
|
+
let oldestTime = Infinity;
|
|
112
|
+
|
|
113
|
+
for (const [key, accessTime] of this.accessTimes) {
|
|
114
|
+
if (accessTime < oldestTime) {
|
|
115
|
+
oldestTime = accessTime;
|
|
116
|
+
oldestKey = key;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (oldestKey) {
|
|
121
|
+
this.delete(oldestKey);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 启动自动清理过期缓存
|
|
127
|
+
*/
|
|
128
|
+
startCleanup() {
|
|
129
|
+
this.cleanupTimer = setInterval(() => {
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
const expiredKeys = [];
|
|
132
|
+
|
|
133
|
+
for (const [key, expiryTime] of this.ttls) {
|
|
134
|
+
if (now > expiryTime) {
|
|
135
|
+
expiredKeys.push(key);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
expiredKeys.forEach(key => this.delete(key));
|
|
140
|
+
|
|
141
|
+
if (expiredKeys.length > 0) {
|
|
142
|
+
console.log(`[CacheManager] Cleaned up ${expiredKeys.length} expired entries`);
|
|
143
|
+
}
|
|
144
|
+
}, this.cleanupInterval);
|
|
145
|
+
|
|
146
|
+
// 确保Node.js进程退出时清理定时器
|
|
147
|
+
if (this.cleanupTimer.unref) {
|
|
148
|
+
this.cleanupTimer.unref();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 停止自动清理
|
|
154
|
+
*/
|
|
155
|
+
stopCleanup() {
|
|
156
|
+
if (this.cleanupTimer) {
|
|
157
|
+
clearInterval(this.cleanupTimer);
|
|
158
|
+
this.cleanupTimer = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 获取缓存统计信息
|
|
164
|
+
* @returns {object}
|
|
165
|
+
*/
|
|
166
|
+
getStats() {
|
|
167
|
+
return {
|
|
168
|
+
size: this.caches.size,
|
|
169
|
+
maxSize: this.maxSize,
|
|
170
|
+
keys: Array.from(this.caches.keys())
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 创建全局缓存实例
|
|
176
|
+
const globalCache = new CacheManager({
|
|
177
|
+
maxSize: 1000,
|
|
178
|
+
defaultTTL: 300000, // 5分钟
|
|
179
|
+
cleanupInterval: 60000 // 1分钟
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 预定义的缓存键前缀
|
|
183
|
+
const CacheKeys = {
|
|
184
|
+
PROJECTS: 'projects:',
|
|
185
|
+
SESSIONS: 'sessions:',
|
|
186
|
+
SKILLS: 'skills:',
|
|
187
|
+
CONFIG_TEMPLATES: 'config-templates:',
|
|
188
|
+
REPOS: 'repos:',
|
|
189
|
+
HAS_MESSAGES: 'has-messages:'
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
module.exports = {
|
|
193
|
+
CacheManager,
|
|
194
|
+
globalCache,
|
|
195
|
+
CacheKeys
|
|
196
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境变量检测服务
|
|
3
|
+
*
|
|
4
|
+
* 检测系统中可能导致 API 配置冲突的环境变量
|
|
5
|
+
* 支持 macOS/Linux/Windows 的 shell 配置文件检测
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
// 各平台需要检测的环境变量关键词
|
|
14
|
+
const PLATFORM_KEYWORDS = {
|
|
15
|
+
claude: ['ANTHROPIC'],
|
|
16
|
+
codex: ['OPENAI'],
|
|
17
|
+
gemini: ['GEMINI', 'GOOGLE_GEMINI']
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 敏感变量后缀模式 - 只有同时匹配关键词和这些后缀的变量才会被检测
|
|
21
|
+
// 这样可以过滤掉 IDE 集成等无害变量(如 GEMINI_CLI_IDE_WORKSPACE_PATH)
|
|
22
|
+
const SENSITIVE_PATTERNS = [
|
|
23
|
+
'_API_KEY',
|
|
24
|
+
'_SECRET_KEY',
|
|
25
|
+
'_ACCESS_TOKEN',
|
|
26
|
+
'_AUTH_TOKEN',
|
|
27
|
+
'_BASE_URL',
|
|
28
|
+
'_API_BASE',
|
|
29
|
+
'_API_URL',
|
|
30
|
+
'_ENDPOINT',
|
|
31
|
+
'_API_ENDPOINT'
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// 精确匹配的敏感变量名(不需要后缀匹配)
|
|
35
|
+
const EXACT_SENSITIVE_VARS = [
|
|
36
|
+
'ANTHROPIC_API_KEY',
|
|
37
|
+
'ANTHROPIC_BASE_URL',
|
|
38
|
+
'OPENAI_API_KEY',
|
|
39
|
+
'OPENAI_BASE_URL',
|
|
40
|
+
'OPENAI_API_BASE',
|
|
41
|
+
'GEMINI_API_KEY',
|
|
42
|
+
'GOOGLE_API_KEY',
|
|
43
|
+
'GOOGLE_GEMINI_API_KEY'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// 需要检测的 shell 配置文件
|
|
47
|
+
const SHELL_CONFIG_FILES = process.platform === 'win32'
|
|
48
|
+
? [
|
|
49
|
+
'.bashrc',
|
|
50
|
+
'.bash_profile',
|
|
51
|
+
'.bash_login',
|
|
52
|
+
'.zshrc',
|
|
53
|
+
'.zshenv',
|
|
54
|
+
'.zprofile',
|
|
55
|
+
'.zlogin',
|
|
56
|
+
'.profile',
|
|
57
|
+
path.join('Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
|
|
58
|
+
path.join('Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1')
|
|
59
|
+
]
|
|
60
|
+
: [
|
|
61
|
+
'.bashrc',
|
|
62
|
+
'.bash_profile',
|
|
63
|
+
'.bash_login',
|
|
64
|
+
'.zshrc',
|
|
65
|
+
'.zshenv',
|
|
66
|
+
'.zprofile',
|
|
67
|
+
'.zlogin',
|
|
68
|
+
'.profile'
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// 系统级配置文件
|
|
72
|
+
const SYSTEM_CONFIG_FILES = [
|
|
73
|
+
'/etc/profile',
|
|
74
|
+
'/etc/bashrc',
|
|
75
|
+
'/etc/zshrc'
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 检测环境变量冲突
|
|
80
|
+
* @param {string} platform - 平台名称: claude/codex/gemini,不传则检测所有
|
|
81
|
+
* @returns {Array} 冲突列表
|
|
82
|
+
*/
|
|
83
|
+
function checkEnvConflicts(platform = null) {
|
|
84
|
+
const keywords = getKeywords(platform);
|
|
85
|
+
const conflicts = [];
|
|
86
|
+
|
|
87
|
+
// 1. 检测当前进程环境变量
|
|
88
|
+
conflicts.push(...checkProcessEnv(keywords));
|
|
89
|
+
|
|
90
|
+
// 2. 检测用户 shell 配置文件
|
|
91
|
+
conflicts.push(...checkShellConfigs(keywords));
|
|
92
|
+
|
|
93
|
+
// 3. 检测系统配置文件
|
|
94
|
+
conflicts.push(...checkSystemConfigs(keywords));
|
|
95
|
+
|
|
96
|
+
// 去重 + 只保留“真实冲突”(同名变量在多个来源且值不一致)
|
|
97
|
+
const deduplicated = deduplicateConflicts(conflicts);
|
|
98
|
+
const realConflicts = filterRealConflicts(deduplicated);
|
|
99
|
+
return sanitizeConflicts(realConflicts);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 获取需要检测的关键词
|
|
104
|
+
*/
|
|
105
|
+
function getKeywords(platform) {
|
|
106
|
+
if (platform && PLATFORM_KEYWORDS[platform]) {
|
|
107
|
+
return PLATFORM_KEYWORDS[platform];
|
|
108
|
+
}
|
|
109
|
+
// 返回所有关键词
|
|
110
|
+
return Object.values(PLATFORM_KEYWORDS).flat();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 检测当前进程环境变量
|
|
115
|
+
*/
|
|
116
|
+
function checkProcessEnv(keywords) {
|
|
117
|
+
const conflicts = [];
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
120
|
+
if (!hasNonEmptyValue(value)) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (matchesKeywords(key, keywords)) {
|
|
125
|
+
conflicts.push({
|
|
126
|
+
varName: key,
|
|
127
|
+
varValue: maskSensitiveValue(value),
|
|
128
|
+
valueFingerprint: hashValue(value),
|
|
129
|
+
sourceType: 'process',
|
|
130
|
+
sourcePath: 'Process Environment',
|
|
131
|
+
platform: detectPlatform(key)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return conflicts;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 检测用户 shell 配置文件
|
|
141
|
+
*/
|
|
142
|
+
function checkShellConfigs(keywords) {
|
|
143
|
+
const conflicts = [];
|
|
144
|
+
const homeDir = os.homedir();
|
|
145
|
+
|
|
146
|
+
for (const fileName of SHELL_CONFIG_FILES) {
|
|
147
|
+
const filePath = path.isAbsolute(fileName) ? fileName : path.join(homeDir, fileName);
|
|
148
|
+
const fileConflicts = parseConfigFile(filePath, keywords);
|
|
149
|
+
conflicts.push(...fileConflicts);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return conflicts;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 检测系统配置文件
|
|
157
|
+
*/
|
|
158
|
+
function checkSystemConfigs(keywords) {
|
|
159
|
+
const conflicts = [];
|
|
160
|
+
|
|
161
|
+
for (const filePath of SYSTEM_CONFIG_FILES) {
|
|
162
|
+
const fileConflicts = parseConfigFile(filePath, keywords);
|
|
163
|
+
conflicts.push(...fileConflicts);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return conflicts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 解析配置文件,查找环境变量定义
|
|
171
|
+
*/
|
|
172
|
+
function parseConfigFile(filePath, keywords) {
|
|
173
|
+
const conflicts = [];
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (!fs.existsSync(filePath)) {
|
|
177
|
+
return conflicts;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
181
|
+
const lines = content.split('\n');
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
const line = lines[i];
|
|
185
|
+
const trimmed = line.trim();
|
|
186
|
+
|
|
187
|
+
// 跳过注释行
|
|
188
|
+
if (trimmed.startsWith('#')) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 匹配 sh/bash/zsh 的 export VAR=value 或 VAR=value
|
|
193
|
+
const exportMatch = trimmed.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
194
|
+
// 匹配 PowerShell 的 $env:VAR = value
|
|
195
|
+
const psMatch = trimmed.match(/^\$env:([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i);
|
|
196
|
+
const matched = exportMatch || psMatch;
|
|
197
|
+
|
|
198
|
+
if (matched) {
|
|
199
|
+
const [, varName, varValue] = matched;
|
|
200
|
+
const normalizedValue = cleanValue(varValue);
|
|
201
|
+
|
|
202
|
+
// 空值通常是占位或已清理状态,不再视为冲突
|
|
203
|
+
if (!hasNonEmptyValue(normalizedValue)) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (matchesKeywords(varName, keywords)) {
|
|
208
|
+
conflicts.push({
|
|
209
|
+
varName,
|
|
210
|
+
varValue: maskSensitiveValue(normalizedValue),
|
|
211
|
+
valueFingerprint: hashValue(normalizedValue),
|
|
212
|
+
sourceType: 'file',
|
|
213
|
+
sourcePath: `${filePath}:${i + 1}`,
|
|
214
|
+
filePath,
|
|
215
|
+
lineNumber: i + 1,
|
|
216
|
+
platform: detectPlatform(varName)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch (err) {
|
|
222
|
+
// 忽略无法读取的文件
|
|
223
|
+
console.debug(`[EnvChecker] Cannot read ${filePath}:`, err.message);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return conflicts;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 检查变量名是否匹配关键词且为敏感变量
|
|
231
|
+
*
|
|
232
|
+
* 双重过滤逻辑:
|
|
233
|
+
* 1. 变量名必须包含平台关键词(ANTHROPIC/OPENAI/GEMINI)
|
|
234
|
+
* 2. 同时满足以下条件之一:
|
|
235
|
+
* - 精确匹配已知敏感变量名
|
|
236
|
+
* - 变量名以敏感后缀结尾(如 _API_KEY, _BASE_URL)
|
|
237
|
+
*
|
|
238
|
+
* 这样可以过滤掉无害的 IDE 集成变量(如 GEMINI_CLI_IDE_WORKSPACE_PATH)
|
|
239
|
+
*/
|
|
240
|
+
function matchesKeywords(varName, keywords) {
|
|
241
|
+
const upperName = varName.toUpperCase();
|
|
242
|
+
|
|
243
|
+
// 检查是否精确匹配已知敏感变量
|
|
244
|
+
if (EXACT_SENSITIVE_VARS.includes(upperName)) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 首先检查是否命中平台关键词(按 token 匹配,避免 OPENAI2 误判为 OPENAI)
|
|
249
|
+
const hasKeyword = keywords.some(keyword => matchesKeywordToken(upperName, keyword));
|
|
250
|
+
if (!hasKeyword) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 检查是否以敏感后缀结尾
|
|
255
|
+
const hasSensitiveSuffix = SENSITIVE_PATTERNS.some(suffix =>
|
|
256
|
+
upperName.endsWith(suffix)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
return hasSensitiveSuffix;
|
|
260
|
+
}
|
|
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
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 检测变量属于哪个平台
|
|
274
|
+
*/
|
|
275
|
+
function detectPlatform(varName) {
|
|
276
|
+
const upperName = varName.toUpperCase();
|
|
277
|
+
|
|
278
|
+
for (const [platform, keywords] of Object.entries(PLATFORM_KEYWORDS)) {
|
|
279
|
+
if (keywords.some(k => upperName.includes(k))) {
|
|
280
|
+
return platform;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return 'unknown';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 清理变量值(去除引号)
|
|
289
|
+
*/
|
|
290
|
+
function cleanValue(value) {
|
|
291
|
+
let cleaned = value.trim();
|
|
292
|
+
// 去除首尾引号
|
|
293
|
+
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
|
294
|
+
(cleaned.startsWith("'") && cleaned.endsWith("'"))) {
|
|
295
|
+
cleaned = cleaned.slice(1, -1);
|
|
296
|
+
}
|
|
297
|
+
return cleaned;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 判断值是否为非空
|
|
302
|
+
*/
|
|
303
|
+
function hasNonEmptyValue(value) {
|
|
304
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 遮蔽敏感值
|
|
309
|
+
*/
|
|
310
|
+
function maskSensitiveValue(value) {
|
|
311
|
+
if (!value) return '';
|
|
312
|
+
if (value.length <= 8) return '****';
|
|
313
|
+
return value.substring(0, 4) + '****' + value.substring(value.length - 4);
|
|
314
|
+
}
|
|
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
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 去重冲突列表
|
|
368
|
+
*/
|
|
369
|
+
function deduplicateConflicts(conflicts) {
|
|
370
|
+
const seen = new Map();
|
|
371
|
+
|
|
372
|
+
for (const conflict of conflicts) {
|
|
373
|
+
const key = `${conflict.varName}:${conflict.sourcePath}`;
|
|
374
|
+
if (!seen.has(key)) {
|
|
375
|
+
seen.set(key, conflict);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return Array.from(seen.values());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 获取冲突统计
|
|
384
|
+
*/
|
|
385
|
+
function getConflictStats(conflicts) {
|
|
386
|
+
const stats = {
|
|
387
|
+
total: conflicts.length,
|
|
388
|
+
byPlatform: {},
|
|
389
|
+
bySourceType: {}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
for (const conflict of conflicts) {
|
|
393
|
+
// 按平台统计
|
|
394
|
+
stats.byPlatform[conflict.platform] = (stats.byPlatform[conflict.platform] || 0) + 1;
|
|
395
|
+
// 按来源类型统计
|
|
396
|
+
stats.bySourceType[conflict.sourceType] = (stats.bySourceType[conflict.sourceType] || 0) + 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return stats;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
module.exports = {
|
|
403
|
+
checkEnvConflicts,
|
|
404
|
+
getConflictStats,
|
|
405
|
+
PLATFORM_KEYWORDS,
|
|
406
|
+
SHELL_CONFIG_FILES,
|
|
407
|
+
SENSITIVE_PATTERNS,
|
|
408
|
+
EXACT_SENSITIVE_VARS
|
|
409
|
+
};
|