@adversity/coding-tool-x 2.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.
Files changed (125) hide show
  1. package/CHANGELOG.md +333 -0
  2. package/LICENSE +21 -0
  3. package/README.md +404 -0
  4. package/bin/ctx.js +8 -0
  5. package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
  6. package/dist/web/assets/index-aL3cKxSK.css +41 -0
  7. package/dist/web/favicon.ico +0 -0
  8. package/dist/web/index.html +14 -0
  9. package/dist/web/logo.png +0 -0
  10. package/docs/CHANGELOG.md +582 -0
  11. package/docs/DIRECTORY_MIGRATION.md +112 -0
  12. package/docs/PROJECT_STRUCTURE.md +396 -0
  13. package/docs/bannel.png +0 -0
  14. package/docs/home.png +0 -0
  15. package/docs/logo.png +0 -0
  16. package/docs/multi-channel-load-balancing.md +249 -0
  17. package/package.json +73 -0
  18. package/src/commands/channels.js +504 -0
  19. package/src/commands/cli-type.js +99 -0
  20. package/src/commands/daemon.js +286 -0
  21. package/src/commands/doctor.js +332 -0
  22. package/src/commands/list.js +222 -0
  23. package/src/commands/logs.js +259 -0
  24. package/src/commands/port-config.js +115 -0
  25. package/src/commands/proxy-control.js +258 -0
  26. package/src/commands/proxy.js +152 -0
  27. package/src/commands/resume.js +137 -0
  28. package/src/commands/search.js +190 -0
  29. package/src/commands/stats.js +224 -0
  30. package/src/commands/switch.js +48 -0
  31. package/src/commands/toggle-proxy.js +222 -0
  32. package/src/commands/ui.js +92 -0
  33. package/src/commands/workspace.js +454 -0
  34. package/src/config/default.js +40 -0
  35. package/src/config/loader.js +75 -0
  36. package/src/config/paths.js +121 -0
  37. package/src/index.js +373 -0
  38. package/src/reset-config.js +92 -0
  39. package/src/server/api/agents.js +248 -0
  40. package/src/server/api/aliases.js +36 -0
  41. package/src/server/api/channels.js +258 -0
  42. package/src/server/api/claude-hooks.js +480 -0
  43. package/src/server/api/codex-channels.js +312 -0
  44. package/src/server/api/codex-projects.js +91 -0
  45. package/src/server/api/codex-proxy.js +182 -0
  46. package/src/server/api/codex-sessions.js +491 -0
  47. package/src/server/api/codex-statistics.js +57 -0
  48. package/src/server/api/commands.js +245 -0
  49. package/src/server/api/config-templates.js +182 -0
  50. package/src/server/api/config.js +147 -0
  51. package/src/server/api/convert.js +127 -0
  52. package/src/server/api/dashboard.js +125 -0
  53. package/src/server/api/env.js +144 -0
  54. package/src/server/api/favorites.js +77 -0
  55. package/src/server/api/gemini-channels.js +261 -0
  56. package/src/server/api/gemini-projects.js +91 -0
  57. package/src/server/api/gemini-proxy.js +160 -0
  58. package/src/server/api/gemini-sessions.js +397 -0
  59. package/src/server/api/gemini-statistics.js +57 -0
  60. package/src/server/api/health-check.js +118 -0
  61. package/src/server/api/mcp.js +336 -0
  62. package/src/server/api/pm2-autostart.js +269 -0
  63. package/src/server/api/projects.js +124 -0
  64. package/src/server/api/prompts.js +279 -0
  65. package/src/server/api/proxy.js +235 -0
  66. package/src/server/api/rules.js +271 -0
  67. package/src/server/api/sessions.js +595 -0
  68. package/src/server/api/settings.js +61 -0
  69. package/src/server/api/skills.js +305 -0
  70. package/src/server/api/statistics.js +91 -0
  71. package/src/server/api/terminal.js +202 -0
  72. package/src/server/api/ui-config.js +64 -0
  73. package/src/server/api/workspaces.js +407 -0
  74. package/src/server/codex-proxy-server.js +538 -0
  75. package/src/server/dev-server.js +26 -0
  76. package/src/server/gemini-proxy-server.js +518 -0
  77. package/src/server/index.js +305 -0
  78. package/src/server/proxy-server.js +469 -0
  79. package/src/server/services/agents-service.js +354 -0
  80. package/src/server/services/alias.js +71 -0
  81. package/src/server/services/channel-health.js +234 -0
  82. package/src/server/services/channel-scheduler.js +234 -0
  83. package/src/server/services/channels.js +347 -0
  84. package/src/server/services/codex-channels.js +625 -0
  85. package/src/server/services/codex-config.js +90 -0
  86. package/src/server/services/codex-parser.js +322 -0
  87. package/src/server/services/codex-sessions.js +665 -0
  88. package/src/server/services/codex-settings-manager.js +397 -0
  89. package/src/server/services/codex-speed-test-template.json +24 -0
  90. package/src/server/services/codex-statistics-service.js +255 -0
  91. package/src/server/services/commands-service.js +360 -0
  92. package/src/server/services/config-templates-service.js +732 -0
  93. package/src/server/services/env-checker.js +307 -0
  94. package/src/server/services/env-manager.js +300 -0
  95. package/src/server/services/favorites.js +163 -0
  96. package/src/server/services/gemini-channels.js +333 -0
  97. package/src/server/services/gemini-config.js +73 -0
  98. package/src/server/services/gemini-sessions.js +689 -0
  99. package/src/server/services/gemini-settings-manager.js +263 -0
  100. package/src/server/services/gemini-statistics-service.js +253 -0
  101. package/src/server/services/health-check.js +399 -0
  102. package/src/server/services/mcp-service.js +1188 -0
  103. package/src/server/services/prompts-service.js +492 -0
  104. package/src/server/services/proxy-runtime.js +79 -0
  105. package/src/server/services/pty-manager.js +435 -0
  106. package/src/server/services/rules-service.js +401 -0
  107. package/src/server/services/session-cache.js +127 -0
  108. package/src/server/services/session-converter.js +577 -0
  109. package/src/server/services/sessions.js +757 -0
  110. package/src/server/services/settings-manager.js +163 -0
  111. package/src/server/services/skill-service.js +965 -0
  112. package/src/server/services/speed-test.js +545 -0
  113. package/src/server/services/statistics-service.js +386 -0
  114. package/src/server/services/terminal-commands.js +155 -0
  115. package/src/server/services/terminal-config.js +140 -0
  116. package/src/server/services/terminal-detector.js +306 -0
  117. package/src/server/services/ui-config.js +130 -0
  118. package/src/server/services/workspace-service.js +662 -0
  119. package/src/server/utils/pricing.js +41 -0
  120. package/src/server/websocket-server.js +557 -0
  121. package/src/ui/menu.js +129 -0
  122. package/src/ui/prompts.js +100 -0
  123. package/src/utils/format.js +43 -0
  124. package/src/utils/port-helper.js +94 -0
  125. package/src/utils/session.js +239 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * 环境变量检测服务
3
+ *
4
+ * 检测系统中可能导致 API 配置冲突的环境变量
5
+ * 支持 macOS/Linux 的 shell 配置文件检测
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ // 各平台需要检测的环境变量关键词
13
+ const PLATFORM_KEYWORDS = {
14
+ claude: ['ANTHROPIC'],
15
+ codex: ['OPENAI'],
16
+ gemini: ['GEMINI', 'GOOGLE_GEMINI']
17
+ };
18
+
19
+ // 敏感变量后缀模式 - 只有同时匹配关键词和这些后缀的变量才会被检测
20
+ // 这样可以过滤掉 IDE 集成等无害变量(如 GEMINI_CLI_IDE_WORKSPACE_PATH)
21
+ const SENSITIVE_PATTERNS = [
22
+ '_API_KEY',
23
+ '_SECRET_KEY',
24
+ '_ACCESS_TOKEN',
25
+ '_AUTH_TOKEN',
26
+ '_BASE_URL',
27
+ '_API_BASE',
28
+ '_API_URL',
29
+ '_ENDPOINT',
30
+ '_API_ENDPOINT'
31
+ ];
32
+
33
+ // 精确匹配的敏感变量名(不需要后缀匹配)
34
+ const EXACT_SENSITIVE_VARS = [
35
+ 'ANTHROPIC_API_KEY',
36
+ 'ANTHROPIC_BASE_URL',
37
+ 'OPENAI_API_KEY',
38
+ 'OPENAI_BASE_URL',
39
+ 'OPENAI_API_BASE',
40
+ 'GEMINI_API_KEY',
41
+ 'GOOGLE_API_KEY',
42
+ 'GOOGLE_GEMINI_API_KEY'
43
+ ];
44
+
45
+ // 需要检测的 shell 配置文件
46
+ const SHELL_CONFIG_FILES = [
47
+ '.bashrc',
48
+ '.bash_profile',
49
+ '.zshrc',
50
+ '.zprofile',
51
+ '.profile'
52
+ ];
53
+
54
+ // 系统级配置文件
55
+ const SYSTEM_CONFIG_FILES = [
56
+ '/etc/profile',
57
+ '/etc/bashrc',
58
+ '/etc/zshrc'
59
+ ];
60
+
61
+ /**
62
+ * 检测环境变量冲突
63
+ * @param {string} platform - 平台名称: claude/codex/gemini,不传则检测所有
64
+ * @returns {Array} 冲突列表
65
+ */
66
+ function checkEnvConflicts(platform = null) {
67
+ const keywords = getKeywords(platform);
68
+ const conflicts = [];
69
+
70
+ // 1. 检测当前进程环境变量
71
+ conflicts.push(...checkProcessEnv(keywords));
72
+
73
+ // 2. 检测用户 shell 配置文件
74
+ conflicts.push(...checkShellConfigs(keywords));
75
+
76
+ // 3. 检测系统配置文件
77
+ conflicts.push(...checkSystemConfigs(keywords));
78
+
79
+ // 去重(同一变量可能在多处定义)
80
+ return deduplicateConflicts(conflicts);
81
+ }
82
+
83
+ /**
84
+ * 获取需要检测的关键词
85
+ */
86
+ function getKeywords(platform) {
87
+ if (platform && PLATFORM_KEYWORDS[platform]) {
88
+ return PLATFORM_KEYWORDS[platform];
89
+ }
90
+ // 返回所有关键词
91
+ return Object.values(PLATFORM_KEYWORDS).flat();
92
+ }
93
+
94
+ /**
95
+ * 检测当前进程环境变量
96
+ */
97
+ function checkProcessEnv(keywords) {
98
+ const conflicts = [];
99
+
100
+ for (const [key, value] of Object.entries(process.env)) {
101
+ if (matchesKeywords(key, keywords)) {
102
+ conflicts.push({
103
+ varName: key,
104
+ varValue: maskSensitiveValue(value),
105
+ sourceType: 'process',
106
+ sourcePath: 'Process Environment',
107
+ platform: detectPlatform(key)
108
+ });
109
+ }
110
+ }
111
+
112
+ return conflicts;
113
+ }
114
+
115
+ /**
116
+ * 检测用户 shell 配置文件
117
+ */
118
+ function checkShellConfigs(keywords) {
119
+ const conflicts = [];
120
+ const homeDir = os.homedir();
121
+
122
+ for (const fileName of SHELL_CONFIG_FILES) {
123
+ const filePath = path.join(homeDir, fileName);
124
+ const fileConflicts = parseConfigFile(filePath, keywords);
125
+ conflicts.push(...fileConflicts);
126
+ }
127
+
128
+ return conflicts;
129
+ }
130
+
131
+ /**
132
+ * 检测系统配置文件
133
+ */
134
+ function checkSystemConfigs(keywords) {
135
+ const conflicts = [];
136
+
137
+ for (const filePath of SYSTEM_CONFIG_FILES) {
138
+ const fileConflicts = parseConfigFile(filePath, keywords);
139
+ conflicts.push(...fileConflicts);
140
+ }
141
+
142
+ return conflicts;
143
+ }
144
+
145
+ /**
146
+ * 解析配置文件,查找环境变量定义
147
+ */
148
+ function parseConfigFile(filePath, keywords) {
149
+ const conflicts = [];
150
+
151
+ try {
152
+ if (!fs.existsSync(filePath)) {
153
+ return conflicts;
154
+ }
155
+
156
+ const content = fs.readFileSync(filePath, 'utf-8');
157
+ const lines = content.split('\n');
158
+
159
+ for (let i = 0; i < lines.length; i++) {
160
+ const line = lines[i];
161
+ const trimmed = line.trim();
162
+
163
+ // 跳过注释行
164
+ if (trimmed.startsWith('#')) {
165
+ continue;
166
+ }
167
+
168
+ // 匹配 export VAR=value 或 VAR=value 格式
169
+ const exportMatch = trimmed.match(/^(?:export\s+)?([A-Z_][A-Z0-9_]*)=(.*)$/);
170
+ if (exportMatch) {
171
+ const [, varName, varValue] = exportMatch;
172
+
173
+ if (matchesKeywords(varName, keywords)) {
174
+ conflicts.push({
175
+ varName,
176
+ varValue: maskSensitiveValue(cleanValue(varValue)),
177
+ sourceType: 'file',
178
+ sourcePath: `${filePath}:${i + 1}`,
179
+ filePath,
180
+ lineNumber: i + 1,
181
+ platform: detectPlatform(varName)
182
+ });
183
+ }
184
+ }
185
+ }
186
+ } catch (err) {
187
+ // 忽略无法读取的文件
188
+ console.debug(`[EnvChecker] Cannot read ${filePath}:`, err.message);
189
+ }
190
+
191
+ return conflicts;
192
+ }
193
+
194
+ /**
195
+ * 检查变量名是否匹配关键词且为敏感变量
196
+ *
197
+ * 双重过滤逻辑:
198
+ * 1. 变量名必须包含平台关键词(ANTHROPIC/OPENAI/GEMINI)
199
+ * 2. 同时满足以下条件之一:
200
+ * - 精确匹配已知敏感变量名
201
+ * - 变量名以敏感后缀结尾(如 _API_KEY, _BASE_URL)
202
+ *
203
+ * 这样可以过滤掉无害的 IDE 集成变量(如 GEMINI_CLI_IDE_WORKSPACE_PATH)
204
+ */
205
+ function matchesKeywords(varName, keywords) {
206
+ const upperName = varName.toUpperCase();
207
+
208
+ // 首先检查是否包含平台关键词
209
+ const hasKeyword = keywords.some(keyword => upperName.includes(keyword));
210
+ if (!hasKeyword) {
211
+ return false;
212
+ }
213
+
214
+ // 检查是否精确匹配已知敏感变量
215
+ if (EXACT_SENSITIVE_VARS.includes(upperName)) {
216
+ return true;
217
+ }
218
+
219
+ // 检查是否以敏感后缀结尾
220
+ const hasSensitiveSuffix = SENSITIVE_PATTERNS.some(suffix =>
221
+ upperName.endsWith(suffix)
222
+ );
223
+
224
+ return hasSensitiveSuffix;
225
+ }
226
+
227
+ /**
228
+ * 检测变量属于哪个平台
229
+ */
230
+ function detectPlatform(varName) {
231
+ const upperName = varName.toUpperCase();
232
+
233
+ for (const [platform, keywords] of Object.entries(PLATFORM_KEYWORDS)) {
234
+ if (keywords.some(k => upperName.includes(k))) {
235
+ return platform;
236
+ }
237
+ }
238
+
239
+ return 'unknown';
240
+ }
241
+
242
+ /**
243
+ * 清理变量值(去除引号)
244
+ */
245
+ function cleanValue(value) {
246
+ let cleaned = value.trim();
247
+ // 去除首尾引号
248
+ if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
249
+ (cleaned.startsWith("'") && cleaned.endsWith("'"))) {
250
+ cleaned = cleaned.slice(1, -1);
251
+ }
252
+ return cleaned;
253
+ }
254
+
255
+ /**
256
+ * 遮蔽敏感值
257
+ */
258
+ function maskSensitiveValue(value) {
259
+ if (!value) return '';
260
+ if (value.length <= 8) return '****';
261
+ return value.substring(0, 4) + '****' + value.substring(value.length - 4);
262
+ }
263
+
264
+ /**
265
+ * 去重冲突列表
266
+ */
267
+ function deduplicateConflicts(conflicts) {
268
+ const seen = new Map();
269
+
270
+ for (const conflict of conflicts) {
271
+ const key = `${conflict.varName}:${conflict.sourcePath}`;
272
+ if (!seen.has(key)) {
273
+ seen.set(key, conflict);
274
+ }
275
+ }
276
+
277
+ return Array.from(seen.values());
278
+ }
279
+
280
+ /**
281
+ * 获取冲突统计
282
+ */
283
+ function getConflictStats(conflicts) {
284
+ const stats = {
285
+ total: conflicts.length,
286
+ byPlatform: {},
287
+ bySourceType: {}
288
+ };
289
+
290
+ for (const conflict of conflicts) {
291
+ // 按平台统计
292
+ stats.byPlatform[conflict.platform] = (stats.byPlatform[conflict.platform] || 0) + 1;
293
+ // 按来源类型统计
294
+ stats.bySourceType[conflict.sourceType] = (stats.bySourceType[conflict.sourceType] || 0) + 1;
295
+ }
296
+
297
+ return stats;
298
+ }
299
+
300
+ module.exports = {
301
+ checkEnvConflicts,
302
+ getConflictStats,
303
+ PLATFORM_KEYWORDS,
304
+ SHELL_CONFIG_FILES,
305
+ SENSITIVE_PATTERNS,
306
+ EXACT_SENSITIVE_VARS
307
+ };
@@ -0,0 +1,300 @@
1
+ /**
2
+ * 环境变量管理服务
3
+ *
4
+ * 负责备份、删除和恢复环境变量
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ // 备份目录
12
+ const BACKUP_DIR = path.join(os.homedir(), '.claude', 'cc-tool', 'env-backups');
13
+
14
+ /**
15
+ * 确保备份目录存在
16
+ */
17
+ function ensureBackupDir() {
18
+ if (!fs.existsSync(BACKUP_DIR)) {
19
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * 删除环境变量(带自动备份)
25
+ * @param {Array} conflicts - 要删除的冲突列表
26
+ * @returns {Object} 备份信息
27
+ */
28
+ function deleteEnvVars(conflicts) {
29
+ if (!conflicts || conflicts.length === 0) {
30
+ throw new Error('没有选择要删除的环境变量');
31
+ }
32
+
33
+ // 只处理文件类型的环境变量(进程环境变量无法删除)
34
+ const fileConflicts = conflicts.filter(c => c.sourceType === 'file');
35
+ const processConflicts = conflicts.filter(c => c.sourceType === 'process');
36
+
37
+ if (fileConflicts.length === 0 && processConflicts.length > 0) {
38
+ throw new Error('进程环境变量无法直接删除,请手动从配置文件中移除');
39
+ }
40
+
41
+ // 1. 创建备份
42
+ const backupInfo = createBackup(conflicts);
43
+
44
+ // 2. 从文件中删除
45
+ const results = [];
46
+ const fileGroups = groupByFile(fileConflicts);
47
+
48
+ for (const [filePath, vars] of Object.entries(fileGroups)) {
49
+ try {
50
+ removeVarsFromFile(filePath, vars);
51
+ results.push({
52
+ filePath,
53
+ success: true,
54
+ removedVars: vars.map(v => v.varName)
55
+ });
56
+ } catch (err) {
57
+ results.push({
58
+ filePath,
59
+ success: false,
60
+ error: err.message
61
+ });
62
+ }
63
+ }
64
+
65
+ return {
66
+ backupPath: backupInfo.backupPath,
67
+ timestamp: backupInfo.timestamp,
68
+ results,
69
+ processConflictsSkipped: processConflicts.length
70
+ };
71
+ }
72
+
73
+ /**
74
+ * 创建备份
75
+ */
76
+ function createBackup(conflicts) {
77
+ ensureBackupDir();
78
+
79
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
80
+ const backupFile = path.join(BACKUP_DIR, `env-backup-${timestamp}.json`);
81
+
82
+ const backupData = {
83
+ timestamp,
84
+ createdAt: Date.now(),
85
+ conflicts: conflicts.map(c => ({
86
+ ...c,
87
+ // 存储完整值用于恢复(不遮蔽)
88
+ originalValue: getOriginalValue(c)
89
+ }))
90
+ };
91
+
92
+ fs.writeFileSync(backupFile, JSON.stringify(backupData, null, 2), 'utf-8');
93
+
94
+ return {
95
+ backupPath: backupFile,
96
+ timestamp
97
+ };
98
+ }
99
+
100
+ /**
101
+ * 获取原始值(从文件中重新读取)
102
+ */
103
+ function getOriginalValue(conflict) {
104
+ if (conflict.sourceType !== 'file' || !conflict.filePath || !conflict.lineNumber) {
105
+ return null;
106
+ }
107
+
108
+ try {
109
+ const content = fs.readFileSync(conflict.filePath, 'utf-8');
110
+ const lines = content.split('\n');
111
+ const line = lines[conflict.lineNumber - 1];
112
+
113
+ if (line) {
114
+ const match = line.match(/^(?:export\s+)?[A-Z_][A-Z0-9_]*=(.*)$/);
115
+ if (match) {
116
+ return cleanValue(match[1]);
117
+ }
118
+ }
119
+ } catch (err) {
120
+ // 忽略
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * 清理变量值
128
+ */
129
+ function cleanValue(value) {
130
+ let cleaned = value.trim();
131
+ if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
132
+ (cleaned.startsWith("'") && cleaned.endsWith("'"))) {
133
+ cleaned = cleaned.slice(1, -1);
134
+ }
135
+ return cleaned;
136
+ }
137
+
138
+ /**
139
+ * 按文件分组
140
+ */
141
+ function groupByFile(conflicts) {
142
+ const groups = {};
143
+
144
+ for (const conflict of conflicts) {
145
+ if (conflict.filePath) {
146
+ if (!groups[conflict.filePath]) {
147
+ groups[conflict.filePath] = [];
148
+ }
149
+ groups[conflict.filePath].push(conflict);
150
+ }
151
+ }
152
+
153
+ return groups;
154
+ }
155
+
156
+ /**
157
+ * 从文件中移除环境变量
158
+ */
159
+ function removeVarsFromFile(filePath, vars) {
160
+ if (!fs.existsSync(filePath)) {
161
+ throw new Error(`文件不存在: ${filePath}`);
162
+ }
163
+
164
+ const content = fs.readFileSync(filePath, 'utf-8');
165
+ const lines = content.split('\n');
166
+
167
+ // 收集要删除的行号
168
+ const lineNumbersToRemove = new Set(vars.map(v => v.lineNumber));
169
+
170
+ // 过滤掉要删除的行
171
+ const newLines = lines.filter((_, index) => !lineNumbersToRemove.has(index + 1));
172
+
173
+ // 写回文件
174
+ fs.writeFileSync(filePath, newLines.join('\n'), 'utf-8');
175
+
176
+ console.log(`[EnvManager] Removed ${vars.length} var(s) from ${filePath}`);
177
+ }
178
+
179
+ /**
180
+ * 获取备份列表
181
+ */
182
+ function getBackupList() {
183
+ ensureBackupDir();
184
+
185
+ const files = fs.readdirSync(BACKUP_DIR)
186
+ .filter(f => f.startsWith('env-backup-') && f.endsWith('.json'))
187
+ .sort()
188
+ .reverse(); // 最新的在前
189
+
190
+ return files.map(fileName => {
191
+ const filePath = path.join(BACKUP_DIR, fileName);
192
+ try {
193
+ const content = fs.readFileSync(filePath, 'utf-8');
194
+ const data = JSON.parse(content);
195
+ return {
196
+ fileName,
197
+ filePath,
198
+ timestamp: data.timestamp,
199
+ createdAt: data.createdAt,
200
+ conflictCount: data.conflicts?.length || 0
201
+ };
202
+ } catch (err) {
203
+ return {
204
+ fileName,
205
+ filePath,
206
+ error: err.message
207
+ };
208
+ }
209
+ });
210
+ }
211
+
212
+ /**
213
+ * 从备份恢复
214
+ */
215
+ function restoreFromBackup(backupPath) {
216
+ if (!fs.existsSync(backupPath)) {
217
+ throw new Error(`备份文件不存在: ${backupPath}`);
218
+ }
219
+
220
+ const content = fs.readFileSync(backupPath, 'utf-8');
221
+ const backupData = JSON.parse(content);
222
+
223
+ const results = [];
224
+
225
+ for (const conflict of backupData.conflicts) {
226
+ if (conflict.sourceType !== 'file' || !conflict.filePath || !conflict.originalValue) {
227
+ results.push({
228
+ varName: conflict.varName,
229
+ success: false,
230
+ error: '无法恢复非文件类型的环境变量'
231
+ });
232
+ continue;
233
+ }
234
+
235
+ try {
236
+ restoreVarToFile(conflict.filePath, conflict.varName, conflict.originalValue);
237
+ results.push({
238
+ varName: conflict.varName,
239
+ filePath: conflict.filePath,
240
+ success: true
241
+ });
242
+ } catch (err) {
243
+ results.push({
244
+ varName: conflict.varName,
245
+ success: false,
246
+ error: err.message
247
+ });
248
+ }
249
+ }
250
+
251
+ return { results };
252
+ }
253
+
254
+ /**
255
+ * 恢复环境变量到文件
256
+ */
257
+ function restoreVarToFile(filePath, varName, value) {
258
+ let content = '';
259
+
260
+ if (fs.existsSync(filePath)) {
261
+ content = fs.readFileSync(filePath, 'utf-8');
262
+ // 确保末尾有换行
263
+ if (!content.endsWith('\n')) {
264
+ content += '\n';
265
+ }
266
+ }
267
+
268
+ // 添加环境变量
269
+ const exportLine = `export ${varName}="${value}"`;
270
+ content += exportLine + '\n';
271
+
272
+ fs.writeFileSync(filePath, content, 'utf-8');
273
+
274
+ console.log(`[EnvManager] Restored ${varName} to ${filePath}`);
275
+ }
276
+
277
+ /**
278
+ * 删除备份文件
279
+ */
280
+ function deleteBackup(backupPath) {
281
+ if (!fs.existsSync(backupPath)) {
282
+ throw new Error(`备份文件不存在: ${backupPath}`);
283
+ }
284
+
285
+ // 安全检查:确保是备份目录下的文件
286
+ if (!backupPath.startsWith(BACKUP_DIR)) {
287
+ throw new Error('无效的备份文件路径');
288
+ }
289
+
290
+ fs.unlinkSync(backupPath);
291
+ return true;
292
+ }
293
+
294
+ module.exports = {
295
+ deleteEnvVars,
296
+ getBackupList,
297
+ restoreFromBackup,
298
+ deleteBackup,
299
+ BACKUP_DIR
300
+ };