aihezu 2.5.0 → 2.6.1
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/bin/aihezu.js +15 -6
- package/commands/backup.js +48 -0
- package/commands/clear.js +31 -2
- package/commands/recover.js +107 -0
- package/commands/reset.js +131 -0
- package/commands/usage.js +88 -0
- package/lib/backup.js +237 -0
- package/lib/cache.js +82 -13
- package/lib/permissions.js +105 -0
- package/package.json +1 -1
- package/services/claude.js +5 -1
- package/services/codex.js +4 -0
- package/services/gemini.js +6 -2
package/bin/aihezu.js
CHANGED
|
@@ -7,7 +7,10 @@ const readline = require('readline');
|
|
|
7
7
|
const commands = {
|
|
8
8
|
install: require('../commands/install'),
|
|
9
9
|
clear: require('../commands/clear'),
|
|
10
|
-
usage: require('../commands/usage')
|
|
10
|
+
usage: require('../commands/usage'),
|
|
11
|
+
backup: require('../commands/backup'),
|
|
12
|
+
recover: require('../commands/recover'),
|
|
13
|
+
reset: require('../commands/reset')
|
|
11
14
|
};
|
|
12
15
|
|
|
13
16
|
const services = {
|
|
@@ -22,7 +25,10 @@ function showHelp() {
|
|
|
22
25
|
console.log('用法: aihezu <command> [service]');
|
|
23
26
|
console.log('\n命令:');
|
|
24
27
|
console.log(' install <service> 配置服务 (API Key, URL, 网络设置)');
|
|
25
|
-
console.log(' clear <service> 清理缓存并刷新网络设置');
|
|
28
|
+
console.log(' clear <service> 清理缓存并刷新网络设置 (自动备份)');
|
|
29
|
+
console.log(' backup <service> 手动备份配置');
|
|
30
|
+
console.log(' recover <service> 从备份恢复配置');
|
|
31
|
+
console.log(' reset <service> 重置服务配置 (删除所有配置)');
|
|
26
32
|
console.log(' usage [service] 查看用量统计');
|
|
27
33
|
console.log('');
|
|
28
34
|
console.log('usage 命令可选参数:');
|
|
@@ -47,10 +53,13 @@ function showHelp() {
|
|
|
47
53
|
console.log('\n示例:');
|
|
48
54
|
console.log(' aihezu install claude');
|
|
49
55
|
console.log(' aihezu clear codex');
|
|
50
|
-
console.log(' aihezu
|
|
51
|
-
console.log(' aihezu
|
|
52
|
-
console.log(' aihezu
|
|
53
|
-
console.log(' aihezu
|
|
56
|
+
console.log(' aihezu backup claude (备份 Claude Code 配置)');
|
|
57
|
+
console.log(' aihezu recover claude (恢复 Claude Code 配置)');
|
|
58
|
+
console.log(' aihezu reset claude (重置 Claude Code 配置)');
|
|
59
|
+
console.log(' aihezu usage (查看所有服务用量)');
|
|
60
|
+
console.log(' aihezu usage cc (只查看 Claude Code 用量)');
|
|
61
|
+
console.log(' aihezu install (交互式安装)');
|
|
62
|
+
console.log(' aihezu help (显示帮助信息)');
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
async function askQuestion(rl, question) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { createBackup, cleanOldBackups } = require('../lib/backup');
|
|
2
|
+
|
|
3
|
+
async function backupCommand(service) {
|
|
4
|
+
console.log('');
|
|
5
|
+
console.log('💾 正在备份 ' + service.displayName + ' 配置...');
|
|
6
|
+
console.log('');
|
|
7
|
+
|
|
8
|
+
if (!service.cacheConfig) {
|
|
9
|
+
console.log('❌ 此服务未定义配置信息,无法备份。');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { dir: targetDir, items: itemsToBackup } = service.cacheConfig;
|
|
14
|
+
|
|
15
|
+
// Add settings.json to backup list if it exists
|
|
16
|
+
const allItems = [...itemsToBackup];
|
|
17
|
+
if (service.name === 'claude') {
|
|
18
|
+
allItems.push('settings.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Create backup
|
|
22
|
+
const result = createBackup({
|
|
23
|
+
targetDir: targetDir,
|
|
24
|
+
itemsToBackup: allItems,
|
|
25
|
+
description: service.displayName
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
console.log(`❌ ${result.error}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Clean old backups (keep 3 most recent)
|
|
34
|
+
console.log('');
|
|
35
|
+
const deletedCount = cleanOldBackups({
|
|
36
|
+
targetDir: targetDir,
|
|
37
|
+
keepCount: 3
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (deletedCount > 0) {
|
|
41
|
+
console.log(`\n🧹 已清理 ${deletedCount} 个旧备份`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('✅ 备份完成!');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = backupCommand;
|
package/commands/clear.js
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
1
|
const { modifyHostsFile } = require('../lib/hosts');
|
|
2
2
|
const { cleanCache } = require('../lib/cache');
|
|
3
|
+
const { createBackup, cleanOldBackups } = require('../lib/backup');
|
|
3
4
|
|
|
4
5
|
async function clearCommand(service) {
|
|
5
6
|
console.log('');
|
|
6
7
|
console.log('🧹 正在清理 ' + service.displayName + ' 环境...');
|
|
7
8
|
console.log('');
|
|
8
9
|
|
|
9
|
-
// 1.
|
|
10
|
+
// 1. Create backup before clearing
|
|
11
|
+
if (service.cacheConfig) {
|
|
12
|
+
const allItems = [...service.cacheConfig.items];
|
|
13
|
+
if (service.name === 'claude') {
|
|
14
|
+
allItems.push('settings.json');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const backupResult = createBackup({
|
|
18
|
+
targetDir: service.cacheConfig.dir,
|
|
19
|
+
itemsToBackup: allItems,
|
|
20
|
+
description: service.displayName
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (backupResult.success) {
|
|
24
|
+
console.log('');
|
|
25
|
+
} else {
|
|
26
|
+
console.log(`⚠️ 备份失败: ${backupResult.error}`);
|
|
27
|
+
console.log(' 继续清理缓存...');
|
|
28
|
+
console.log('');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Clean Cache
|
|
10
33
|
if (service.cacheConfig) {
|
|
11
34
|
cleanCache({
|
|
12
35
|
targetDir: service.cacheConfig.dir,
|
|
13
36
|
itemsToClean: service.cacheConfig.items,
|
|
14
37
|
showHeader: true
|
|
15
38
|
});
|
|
39
|
+
|
|
40
|
+
// Clean old backups (keep 3 most recent)
|
|
41
|
+
cleanOldBackups({
|
|
42
|
+
targetDir: service.cacheConfig.dir,
|
|
43
|
+
keepCount: 3
|
|
44
|
+
});
|
|
16
45
|
} else {
|
|
17
46
|
console.log('ℹ️ 此服务未定义缓存配置。');
|
|
18
47
|
}
|
|
19
48
|
|
|
20
|
-
//
|
|
49
|
+
// 3. Refresh Hosts (Ensure they are still set correctly)
|
|
21
50
|
if (service.hostsConfig && service.hostsConfig.length > 0) {
|
|
22
51
|
console.log('');
|
|
23
52
|
console.log('=== 刷新网络配置 ===');
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const { listBackups, restoreBackup, createBackup } = require('../lib/backup');
|
|
3
|
+
|
|
4
|
+
async function askQuestion(rl, question) {
|
|
5
|
+
return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function recoverCommand(service) {
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log('🔄 ' + service.displayName + ' 配置恢复');
|
|
11
|
+
console.log('');
|
|
12
|
+
|
|
13
|
+
if (!service.cacheConfig) {
|
|
14
|
+
console.log('❌ 此服务未定义配置信息,无法恢复。');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { dir: targetDir, items: itemsToBackup } = service.cacheConfig;
|
|
19
|
+
|
|
20
|
+
// List available backups
|
|
21
|
+
const backups = listBackups(targetDir);
|
|
22
|
+
|
|
23
|
+
if (backups.length === 0) {
|
|
24
|
+
console.log('❌ 没有找到可用的备份文件。');
|
|
25
|
+
console.log('');
|
|
26
|
+
console.log('提示: 使用 `aihezu backup ' + service.name + '` 创建备份。');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Display available backups
|
|
31
|
+
console.log('可用的备份:');
|
|
32
|
+
console.log('');
|
|
33
|
+
backups.forEach((backup, index) => {
|
|
34
|
+
console.log(` [${index + 1}] ${backup.formattedDate} (${backup.formattedSize})`);
|
|
35
|
+
});
|
|
36
|
+
console.log('');
|
|
37
|
+
|
|
38
|
+
const rl = readline.createInterface({
|
|
39
|
+
input: process.stdin,
|
|
40
|
+
output: process.stdout
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Ask user to select a backup
|
|
45
|
+
let selectedIndex = -1;
|
|
46
|
+
let validChoice = false;
|
|
47
|
+
|
|
48
|
+
while (!validChoice) {
|
|
49
|
+
const answer = await askQuestion(rl, '请选择要恢复的备份 (输入编号, 或输入 q 退出): ');
|
|
50
|
+
|
|
51
|
+
if (answer.toLowerCase() === 'q') {
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('已取消恢复操作。');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const index = parseInt(answer, 10) - 1;
|
|
58
|
+
if (index >= 0 && index < backups.length) {
|
|
59
|
+
selectedIndex = index;
|
|
60
|
+
validChoice = true;
|
|
61
|
+
} else {
|
|
62
|
+
console.log('无效的选择,请输入列表中的数字。');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const selectedBackup = backups[selectedIndex];
|
|
67
|
+
|
|
68
|
+
// Warn user about overwriting
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('⚠️ 警告: 恢复备份将覆盖当前配置!');
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(' 恢复的备份: ' + selectedBackup.formattedDate);
|
|
73
|
+
console.log(' 目标目录: ' + targetDir);
|
|
74
|
+
console.log('');
|
|
75
|
+
console.log('💡 建议: 在继续之前,你可以先运行 `aihezu backup ' + service.name + '` 备份当前配置。');
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
const confirm = await askQuestion(rl, '确认恢复? (输入 yes 确认): ');
|
|
79
|
+
|
|
80
|
+
if (confirm.toLowerCase() !== 'yes') {
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('已取消恢复操作。');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Restore the backup
|
|
87
|
+
const result = restoreBackup({
|
|
88
|
+
backupFile: selectedBackup.filePath,
|
|
89
|
+
targetDir: targetDir
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (!result.success) {
|
|
93
|
+
console.log(`❌ ${result.error}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log('✅ 恢复完成!');
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log('提示: 如果你使用的是 Claude Code,可能需要重启才能使配置生效。');
|
|
101
|
+
|
|
102
|
+
} finally {
|
|
103
|
+
rl.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = recoverCommand;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { createBackup } = require('../lib/backup');
|
|
5
|
+
|
|
6
|
+
async function askQuestion(rl, question) {
|
|
7
|
+
return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function resetCommand(service) {
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log('🔄 重置 ' + service.displayName + ' 配置');
|
|
13
|
+
console.log('');
|
|
14
|
+
|
|
15
|
+
if (!service.cacheConfig) {
|
|
16
|
+
console.log('❌ 此服务未定义配置信息,无法重置。');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { dir: targetDir, items: itemsToClean } = service.cacheConfig;
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(targetDir)) {
|
|
23
|
+
console.log('ℹ️ 配置目录不存在,无需重置。');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Collect items that exist
|
|
34
|
+
const existingItems = [];
|
|
35
|
+
for (const item of itemsToClean) {
|
|
36
|
+
const itemPath = path.join(targetDir, item);
|
|
37
|
+
if (fs.existsSync(itemPath)) {
|
|
38
|
+
existingItems.push(item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Also check for settings.json
|
|
43
|
+
if (service.name === 'claude') {
|
|
44
|
+
const settingsPath = path.join(targetDir, 'settings.json');
|
|
45
|
+
if (fs.existsSync(settingsPath)) {
|
|
46
|
+
existingItems.push('settings.json');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (existingItems.length === 0) {
|
|
51
|
+
console.log('ℹ️ 没有找到需要清理的配置文件。');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Show what will be deleted
|
|
56
|
+
console.log('⚠️ 警告: 以下配置将被永久删除:');
|
|
57
|
+
console.log('');
|
|
58
|
+
existingItems.forEach(item => {
|
|
59
|
+
const itemPath = path.join(targetDir, item);
|
|
60
|
+
const stat = fs.statSync(itemPath);
|
|
61
|
+
const type = stat.isDirectory() ? '目录' : '文件';
|
|
62
|
+
console.log(` - ${item} (${type})`);
|
|
63
|
+
});
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('💡 建议: 在继续之前,你可以先运行 `aihezu backup ' + service.name + '` 备份当前配置。');
|
|
66
|
+
console.log('');
|
|
67
|
+
|
|
68
|
+
const shouldBackup = await askQuestion(rl, '是否先创建备份? (yes/no, 默认 yes): ');
|
|
69
|
+
|
|
70
|
+
if (shouldBackup.toLowerCase() !== 'no') {
|
|
71
|
+
console.log('');
|
|
72
|
+
const backupResult = createBackup({
|
|
73
|
+
targetDir: targetDir,
|
|
74
|
+
itemsToBackup: existingItems,
|
|
75
|
+
description: service.displayName
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (backupResult.success) {
|
|
79
|
+
console.log('');
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`⚠️ 备份失败: ${backupResult.error}`);
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('⚠️ 最后确认: 确定要删除所有配置吗?此操作不可恢复!');
|
|
87
|
+
console.log('');
|
|
88
|
+
const confirm = await askQuestion(rl, '请输入 "DELETE" (大写) 确认删除: ');
|
|
89
|
+
|
|
90
|
+
if (confirm !== 'DELETE') {
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log('已取消重置操作。');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Delete all items
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log('🗑️ 正在删除配置...');
|
|
99
|
+
console.log('');
|
|
100
|
+
|
|
101
|
+
let deletedCount = 0;
|
|
102
|
+
for (const item of existingItems) {
|
|
103
|
+
const itemPath = path.join(targetDir, item);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const stat = fs.statSync(itemPath);
|
|
107
|
+
|
|
108
|
+
if (stat.isDirectory()) {
|
|
109
|
+
fs.rmSync(itemPath, { recursive: true, force: true });
|
|
110
|
+
console.log(` ✓ 已删除目录: ${item}`);
|
|
111
|
+
} else {
|
|
112
|
+
fs.unlinkSync(itemPath);
|
|
113
|
+
console.log(` ✓ 已删除文件: ${item}`);
|
|
114
|
+
}
|
|
115
|
+
deletedCount++;
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.log(` ✗ 删除失败: ${item} - ${e.message}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(`✅ 重置完成!已删除 ${deletedCount} 个项目。`);
|
|
123
|
+
console.log('');
|
|
124
|
+
console.log('提示: 使用 `aihezu install ' + service.name + '` 重新配置服务。');
|
|
125
|
+
|
|
126
|
+
} finally {
|
|
127
|
+
rl.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = resetCommand;
|
package/commands/usage.js
CHANGED
|
@@ -263,6 +263,54 @@ function formatPercent(current, limit) {
|
|
|
263
263
|
return `${((c / l) * 100).toFixed(1)}%`;
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
function normalizeEpochMs(value) {
|
|
267
|
+
const n = asNumber(value);
|
|
268
|
+
if (n === null) return null;
|
|
269
|
+
return n < 1e12 ? n * 1000 : n;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function formatDateTime(value) {
|
|
273
|
+
const ms = normalizeEpochMs(value);
|
|
274
|
+
if (ms === null) return '-';
|
|
275
|
+
const date = new Date(ms);
|
|
276
|
+
if (Number.isNaN(date.getTime())) return '-';
|
|
277
|
+
const year = date.getFullYear();
|
|
278
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
279
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
280
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
281
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
282
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
283
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function formatDurationSeconds(value) {
|
|
287
|
+
const total = asNumber(value);
|
|
288
|
+
if (total === null) return '-';
|
|
289
|
+
let remaining = Math.max(0, Math.floor(total));
|
|
290
|
+
const days = Math.floor(remaining / 86400);
|
|
291
|
+
remaining %= 86400;
|
|
292
|
+
const hours = Math.floor(remaining / 3600);
|
|
293
|
+
remaining %= 3600;
|
|
294
|
+
const minutes = Math.floor(remaining / 60);
|
|
295
|
+
const seconds = remaining % 60;
|
|
296
|
+
|
|
297
|
+
const parts = [];
|
|
298
|
+
if (days) parts.push(`${days} 天`);
|
|
299
|
+
if (hours) parts.push(`${hours} 小时`);
|
|
300
|
+
if (minutes) parts.push(`${minutes} 分`);
|
|
301
|
+
if (seconds || parts.length === 0) parts.push(`${seconds} 秒`);
|
|
302
|
+
return parts.join(' ');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function formatDurationMinutes(value) {
|
|
306
|
+
const minutes = asNumber(value);
|
|
307
|
+
if (minutes === null || minutes <= 0) return '-';
|
|
308
|
+
const human = formatDurationSeconds(minutes * 60);
|
|
309
|
+
if (human === '-') return '-';
|
|
310
|
+
const minuteText = Number.isInteger(minutes) ? String(minutes) : minutes.toFixed(2);
|
|
311
|
+
return `${human} (${minuteText} 分钟)`;
|
|
312
|
+
}
|
|
313
|
+
|
|
266
314
|
function renderBar(current, limit, width = 18) {
|
|
267
315
|
const c = asNumber(current);
|
|
268
316
|
const l = asNumber(limit);
|
|
@@ -326,6 +374,27 @@ function displayUsageStats(stats, origin, source) {
|
|
|
326
374
|
const dailyCostLimit = (stats.dailyCostLimit !== undefined) ? stats.dailyCostLimit : limits.dailyCostLimit;
|
|
327
375
|
const currentWindowCost = (stats.currentWindowCost !== undefined) ? stats.currentWindowCost : limits.currentWindowCost;
|
|
328
376
|
const rateLimitCost = (stats.rateLimitCost !== undefined) ? stats.rateLimitCost : limits.rateLimitCost;
|
|
377
|
+
const windowStartTime = (stats.windowStartTime !== undefined) ? stats.windowStartTime : limits.windowStartTime;
|
|
378
|
+
const windowEndTime = (stats.windowEndTime !== undefined) ? stats.windowEndTime : limits.windowEndTime;
|
|
379
|
+
const windowRemainingSeconds = (() => {
|
|
380
|
+
const raw = (stats.windowRemainingSeconds !== undefined)
|
|
381
|
+
? stats.windowRemainingSeconds
|
|
382
|
+
: limits.windowRemainingSeconds;
|
|
383
|
+
const provided = asNumber(raw);
|
|
384
|
+
if (provided !== null) return provided;
|
|
385
|
+
const endMs = normalizeEpochMs(windowEndTime);
|
|
386
|
+
if (endMs === null) return null;
|
|
387
|
+
return Math.max(0, Math.floor((endMs - Date.now()) / 1000));
|
|
388
|
+
})();
|
|
389
|
+
const windowDurationMinutes = (() => {
|
|
390
|
+
const raw = (stats.rateLimitWindow !== undefined) ? stats.rateLimitWindow : limits.rateLimitWindow;
|
|
391
|
+
const minutes = asNumber(raw);
|
|
392
|
+
if (minutes !== null) return minutes;
|
|
393
|
+
const startMs = normalizeEpochMs(windowStartTime);
|
|
394
|
+
const endMs = normalizeEpochMs(windowEndTime);
|
|
395
|
+
if (startMs === null || endMs === null || endMs <= startMs) return null;
|
|
396
|
+
return Math.round((endMs - startMs) / 60000);
|
|
397
|
+
})();
|
|
329
398
|
|
|
330
399
|
const dailyRemaining = (() => {
|
|
331
400
|
const c = asNumber(currentDailyCost);
|
|
@@ -360,6 +429,25 @@ function displayUsageStats(stats, origin, source) {
|
|
|
360
429
|
if (windowRemaining !== null) {
|
|
361
430
|
console.log(`窗口剩余: ${formatCost(windowRemaining)}`);
|
|
362
431
|
}
|
|
432
|
+
const windowStartText = formatDateTime(windowStartTime);
|
|
433
|
+
const windowEndText = formatDateTime(windowEndTime);
|
|
434
|
+
if (windowStartText !== '-' || windowEndText !== '-') {
|
|
435
|
+
if (windowStartText !== '-' && windowEndText !== '-') {
|
|
436
|
+
console.log(`时间窗口: ${windowStartText} ~ ${windowEndText}`);
|
|
437
|
+
} else if (windowStartText !== '-') {
|
|
438
|
+
console.log(`时间窗口开始: ${windowStartText}`);
|
|
439
|
+
} else {
|
|
440
|
+
console.log(`时间窗口结束: ${windowEndText}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const windowDurationText = formatDurationMinutes(windowDurationMinutes);
|
|
444
|
+
if (windowDurationText !== '-') {
|
|
445
|
+
console.log(`周时间窗口: ${windowDurationText}`);
|
|
446
|
+
}
|
|
447
|
+
const resetCountdownText = formatDurationSeconds(windowRemainingSeconds);
|
|
448
|
+
if (resetCountdownText !== '-') {
|
|
449
|
+
console.log(`距离重置: ${resetCountdownText}`);
|
|
450
|
+
}
|
|
363
451
|
console.log('');
|
|
364
452
|
}
|
|
365
453
|
|
package/lib/backup.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const { getLocalTimestamp } = require('./cache');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a tar.gz backup of files/directories
|
|
8
|
+
* @param {Object} options - Backup options
|
|
9
|
+
* @param {string} options.targetDir - The directory containing items to backup
|
|
10
|
+
* @param {Array<string>} options.itemsToBackup - Items to backup (files or directories)
|
|
11
|
+
* @param {string} options.description - Description for logging
|
|
12
|
+
* @returns {Object} - { success: boolean, backupFile: string, error: string }
|
|
13
|
+
*/
|
|
14
|
+
function createBackup(options = {}) {
|
|
15
|
+
const { targetDir, itemsToBackup = [], description = '配置' } = options;
|
|
16
|
+
|
|
17
|
+
if (!targetDir || !fs.existsSync(targetDir)) {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
error: `目录不存在: ${targetDir}`
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (itemsToBackup.length === 0) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: '没有指定要备份的项目'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timestamp = getLocalTimestamp();
|
|
32
|
+
const backupFileName = `backup-${timestamp}.tar.gz`;
|
|
33
|
+
const backupFilePath = path.join(targetDir, backupFileName);
|
|
34
|
+
|
|
35
|
+
console.log(`\n📦 正在创建备份...`);
|
|
36
|
+
console.log(` 备份目录: ${targetDir}`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Collect items that actually exist
|
|
40
|
+
const existingItems = [];
|
|
41
|
+
for (const item of itemsToBackup) {
|
|
42
|
+
const itemPath = path.join(targetDir, item);
|
|
43
|
+
if (fs.existsSync(itemPath)) {
|
|
44
|
+
existingItems.push(item);
|
|
45
|
+
const stat = fs.statSync(itemPath);
|
|
46
|
+
const type = stat.isDirectory() ? '目录' : '文件';
|
|
47
|
+
console.log(` - ${item} (${type})`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (existingItems.length === 0) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: '没有找到可以备份的项目'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create tar.gz with all items
|
|
59
|
+
const itemsArg = existingItems.map(item => `'${item}'`).join(' ');
|
|
60
|
+
const tarCommand = process.platform === 'win32'
|
|
61
|
+
? `tar -czf "${backupFilePath}" -C "${targetDir}" ${existingItems.map(i => `"${i}"`).join(' ')}`
|
|
62
|
+
: `tar -czf '${backupFilePath}' -C '${targetDir}' ${itemsArg}`;
|
|
63
|
+
|
|
64
|
+
execSync(tarCommand, { stdio: 'pipe' });
|
|
65
|
+
|
|
66
|
+
console.log(`\n✅ 备份创建成功: ${backupFileName}`);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
success: true,
|
|
70
|
+
backupFile: backupFilePath,
|
|
71
|
+
backupFileName: backupFileName
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
error: `备份失败: ${e.message}`
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* List all available backups in a directory
|
|
84
|
+
* @param {string} targetDir - Directory to search for backups
|
|
85
|
+
* @returns {Array} - Array of backup objects sorted by timestamp (newest first)
|
|
86
|
+
*/
|
|
87
|
+
function listBackups(targetDir) {
|
|
88
|
+
if (!targetDir || !fs.existsSync(targetDir)) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const items = fs.readdirSync(targetDir);
|
|
94
|
+
const backups = [];
|
|
95
|
+
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
// Match pattern: backup-YYYYMMDDHHMMSS.tar.gz
|
|
98
|
+
const match = item.match(/^backup-(\d{14})\.tar\.gz$/);
|
|
99
|
+
|
|
100
|
+
if (match) {
|
|
101
|
+
const timestamp = match[1];
|
|
102
|
+
const itemPath = path.join(targetDir, item);
|
|
103
|
+
const stat = fs.statSync(itemPath);
|
|
104
|
+
|
|
105
|
+
// Format timestamp for display: YYYY-MM-DD HH:MM:SS
|
|
106
|
+
const year = timestamp.substring(0, 4);
|
|
107
|
+
const month = timestamp.substring(4, 6);
|
|
108
|
+
const day = timestamp.substring(6, 8);
|
|
109
|
+
const hour = timestamp.substring(8, 10);
|
|
110
|
+
const minute = timestamp.substring(10, 12);
|
|
111
|
+
const second = timestamp.substring(12, 14);
|
|
112
|
+
const formattedDate = `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
113
|
+
|
|
114
|
+
backups.push({
|
|
115
|
+
fileName: item,
|
|
116
|
+
filePath: itemPath,
|
|
117
|
+
timestamp: timestamp,
|
|
118
|
+
formattedDate: formattedDate,
|
|
119
|
+
size: stat.size,
|
|
120
|
+
formattedSize: formatBytes(stat.size)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Sort by timestamp descending (newest first)
|
|
126
|
+
backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
127
|
+
|
|
128
|
+
return backups;
|
|
129
|
+
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Restore a backup file
|
|
137
|
+
* @param {Object} options - Restore options
|
|
138
|
+
* @param {string} options.backupFile - Path to the backup file
|
|
139
|
+
* @param {string} options.targetDir - Directory to restore to
|
|
140
|
+
* @returns {Object} - { success: boolean, error: string }
|
|
141
|
+
*/
|
|
142
|
+
function restoreBackup(options = {}) {
|
|
143
|
+
const { backupFile, targetDir } = options;
|
|
144
|
+
|
|
145
|
+
if (!backupFile || !fs.existsSync(backupFile)) {
|
|
146
|
+
return {
|
|
147
|
+
success: false,
|
|
148
|
+
error: `备份文件不存在: ${backupFile}`
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!targetDir || !fs.existsSync(targetDir)) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: `目标目录不存在: ${targetDir}`
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`\n🔄 正在恢复备份...`);
|
|
160
|
+
console.log(` 备份文件: ${path.basename(backupFile)}`);
|
|
161
|
+
console.log(` 恢复目录: ${targetDir}`);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Extract tar.gz to target directory
|
|
165
|
+
const tarCommand = process.platform === 'win32'
|
|
166
|
+
? `tar -xzf "${backupFile}" -C "${targetDir}"`
|
|
167
|
+
: `tar -xzf '${backupFile}' -C '${targetDir}'`;
|
|
168
|
+
|
|
169
|
+
execSync(tarCommand, { stdio: 'pipe' });
|
|
170
|
+
|
|
171
|
+
console.log(`\n✅ 备份恢复成功`);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return {
|
|
179
|
+
success: false,
|
|
180
|
+
error: `恢复失败: ${e.message}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Clean old backups, keeping only the most recent N backups
|
|
187
|
+
* @param {Object} options - Clean options
|
|
188
|
+
* @param {string} options.targetDir - Directory containing backups
|
|
189
|
+
* @param {number} options.keepCount - Number of recent backups to keep (default: 3)
|
|
190
|
+
* @returns {number} - Number of backups deleted
|
|
191
|
+
*/
|
|
192
|
+
function cleanOldBackups(options = {}) {
|
|
193
|
+
const { targetDir, keepCount = 3 } = options;
|
|
194
|
+
|
|
195
|
+
if (!targetDir || !fs.existsSync(targetDir)) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const backups = listBackups(targetDir);
|
|
200
|
+
|
|
201
|
+
if (backups.length <= keepCount) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const backupsToDelete = backups.slice(keepCount);
|
|
206
|
+
let deletedCount = 0;
|
|
207
|
+
|
|
208
|
+
for (const backup of backupsToDelete) {
|
|
209
|
+
try {
|
|
210
|
+
console.log(`🗑️ 删除旧备份: ${backup.fileName}`);
|
|
211
|
+
fs.unlinkSync(backup.filePath);
|
|
212
|
+
deletedCount++;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.log(`⚠️ 删除备份失败: ${e.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return deletedCount;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Format bytes to human readable format
|
|
223
|
+
*/
|
|
224
|
+
function formatBytes(bytes) {
|
|
225
|
+
if (bytes === 0) return '0 B';
|
|
226
|
+
const k = 1024;
|
|
227
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
228
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
229
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
createBackup,
|
|
234
|
+
listBackups,
|
|
235
|
+
restoreBackup,
|
|
236
|
+
cleanOldBackups
|
|
237
|
+
};
|
package/lib/cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
4
5
|
|
|
5
6
|
// Generate local timestamp: YYYYMMDDHHMMSS
|
|
6
7
|
function getLocalTimestamp() {
|
|
@@ -14,6 +15,27 @@ function getLocalTimestamp() {
|
|
|
14
15
|
return `${year}${month}${day}${hours}${minutes}${seconds}`;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
// Create a tar.gz archive of a file or directory
|
|
19
|
+
function createTarGzBackup(sourcePath, backupPath) {
|
|
20
|
+
const sourceDir = path.dirname(sourcePath);
|
|
21
|
+
const sourceName = path.basename(sourcePath);
|
|
22
|
+
const backupFile = `${backupPath}.tar.gz`;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Use tar command to create compressed archive
|
|
26
|
+
// -czf: create gzip compressed archive
|
|
27
|
+
// -C: change to directory before operation
|
|
28
|
+
const tarCommand = process.platform === 'win32'
|
|
29
|
+
? `tar -czf "${backupFile}" -C "${sourceDir}" "${sourceName}"`
|
|
30
|
+
: `tar -czf '${backupFile}' -C '${sourceDir}' '${sourceName}'`;
|
|
31
|
+
|
|
32
|
+
execSync(tarCommand, { stdio: 'pipe' });
|
|
33
|
+
return backupFile;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
throw new Error(`打包失败: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
function cleanCache(options = {}) {
|
|
18
40
|
const {
|
|
19
41
|
targetDir,
|
|
@@ -43,7 +65,7 @@ function cleanCache(options = {}) {
|
|
|
43
65
|
try {
|
|
44
66
|
if (fs.existsSync(itemPath)) {
|
|
45
67
|
const stat = fs.statSync(itemPath);
|
|
46
|
-
const
|
|
68
|
+
const backupBasePath = `${itemPath}-backup-${timestamp}`;
|
|
47
69
|
|
|
48
70
|
if (stat.isDirectory()) {
|
|
49
71
|
console.log(`📦 备份并清理目录: ${item}/`);
|
|
@@ -51,7 +73,17 @@ function cleanCache(options = {}) {
|
|
|
51
73
|
console.log(`📦 备份并清理文件: ${item}`);
|
|
52
74
|
}
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
// Create tar.gz backup
|
|
77
|
+
const backupFile = createTarGzBackup(itemPath, backupBasePath);
|
|
78
|
+
console.log(` ✓ 已打包为: ${path.basename(backupFile)}`);
|
|
79
|
+
|
|
80
|
+
// Remove original after successful backup
|
|
81
|
+
if (stat.isDirectory()) {
|
|
82
|
+
fs.rmSync(itemPath, { recursive: true, force: true });
|
|
83
|
+
} else {
|
|
84
|
+
fs.unlinkSync(itemPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
cleanedCount++;
|
|
56
88
|
}
|
|
57
89
|
} catch (e) {
|
|
@@ -59,24 +91,61 @@ function cleanCache(options = {}) {
|
|
|
59
91
|
}
|
|
60
92
|
}
|
|
61
93
|
|
|
62
|
-
// Clean old backups (
|
|
94
|
+
// Clean old backups (keep only the 3 most recent backups)
|
|
63
95
|
try {
|
|
64
96
|
const items = fs.readdirSync(targetDir);
|
|
97
|
+
const backupItems = [];
|
|
98
|
+
|
|
99
|
+
// Collect all backup items with their timestamps
|
|
65
100
|
for (const item of items) {
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
// Let's stick to generic 'backup-*' or if the file ends with '-backup-\d+'
|
|
69
|
-
|
|
70
|
-
const isBackup = item.startsWith('backup-') || /backup-\d{14}$/.test(item);
|
|
101
|
+
// Match pattern: *-backup-YYYYMMDDHHMMSS.tar.gz
|
|
102
|
+
const match = item.match(/^(.+)-backup-(\d{14})\.tar\.gz$/);
|
|
71
103
|
|
|
72
|
-
if (
|
|
104
|
+
if (match) {
|
|
105
|
+
const [, baseName, backupTimestamp] = match;
|
|
73
106
|
const itemPath = path.join(targetDir, item);
|
|
74
|
-
const stat = fs.statSync(itemPath);
|
|
75
107
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
try {
|
|
109
|
+
const stat = fs.statSync(itemPath);
|
|
110
|
+
backupItems.push({
|
|
111
|
+
name: item,
|
|
112
|
+
baseName: baseName,
|
|
113
|
+
timestamp: backupTimestamp,
|
|
114
|
+
path: itemPath,
|
|
115
|
+
isFile: stat.isFile()
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Skip if stat fails
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Group backups by base name
|
|
124
|
+
const backupGroups = {};
|
|
125
|
+
for (const backup of backupItems) {
|
|
126
|
+
if (!backupGroups[backup.baseName]) {
|
|
127
|
+
backupGroups[backup.baseName] = [];
|
|
128
|
+
}
|
|
129
|
+
backupGroups[backup.baseName].push(backup);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For each group, keep only the 3 most recent backups
|
|
133
|
+
for (const baseName in backupGroups) {
|
|
134
|
+
const group = backupGroups[baseName];
|
|
135
|
+
|
|
136
|
+
// Sort by timestamp descending (newest first)
|
|
137
|
+
group.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
138
|
+
|
|
139
|
+
// Delete backups beyond the 3 most recent
|
|
140
|
+
const backupsToDelete = group.slice(3);
|
|
141
|
+
|
|
142
|
+
for (const backup of backupsToDelete) {
|
|
143
|
+
try {
|
|
144
|
+
console.log(`🗑️ 删除旧备份: ${backup.name}`);
|
|
145
|
+
fs.unlinkSync(backup.path);
|
|
79
146
|
cleanedCount++;
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.log(`⚠️ 删除备份 ${backup.name} 时出错: ${e.message}`);
|
|
80
149
|
}
|
|
81
150
|
}
|
|
82
151
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fix file/directory permissions when running with sudo
|
|
6
|
+
* This ensures files are owned by the actual user, not root
|
|
7
|
+
*
|
|
8
|
+
* @param {string|string[]} paths - File or directory path(s) to fix
|
|
9
|
+
* @returns {boolean} - Whether permissions were fixed
|
|
10
|
+
*/
|
|
11
|
+
function fixPermissions(paths) {
|
|
12
|
+
// Check if running with sudo
|
|
13
|
+
const isSudo = process.getuid && process.getuid() === 0;
|
|
14
|
+
|
|
15
|
+
if (!isSudo) {
|
|
16
|
+
// Not running with sudo, no need to fix permissions
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Get the real user who invoked sudo
|
|
21
|
+
const realUser = process.env.SUDO_USER;
|
|
22
|
+
|
|
23
|
+
if (!realUser) {
|
|
24
|
+
console.log('⚠️ 警告: 以 root 权限运行,但无法确定真实用户。');
|
|
25
|
+
console.log(' 配置文件可能需要手动修改权限。');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get the real user's UID and GID
|
|
30
|
+
let uid, gid;
|
|
31
|
+
try {
|
|
32
|
+
const userInfo = execSync(`id -u ${realUser}`, { encoding: 'utf8' }).trim();
|
|
33
|
+
const groupInfo = execSync(`id -g ${realUser}`, { encoding: 'utf8' }).trim();
|
|
34
|
+
uid = parseInt(userInfo, 10);
|
|
35
|
+
gid = parseInt(groupInfo, 10);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.log(`⚠️ 警告: 无法获取用户 ${realUser} 的 UID/GID`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Convert single path to array for uniform processing
|
|
42
|
+
const pathArray = Array.isArray(paths) ? paths : [paths];
|
|
43
|
+
|
|
44
|
+
let fixedCount = 0;
|
|
45
|
+
|
|
46
|
+
for (const itemPath of pathArray) {
|
|
47
|
+
if (!fs.existsSync(itemPath)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Fix ownership recursively for directories
|
|
53
|
+
const stat = fs.statSync(itemPath);
|
|
54
|
+
|
|
55
|
+
if (stat.isDirectory()) {
|
|
56
|
+
// Recursively fix directory and all its contents
|
|
57
|
+
fixDirectoryOwnership(itemPath, uid, gid);
|
|
58
|
+
fixedCount++;
|
|
59
|
+
} else {
|
|
60
|
+
// Fix file ownership
|
|
61
|
+
fs.chownSync(itemPath, uid, gid);
|
|
62
|
+
fixedCount++;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.log(`⚠️ 无法修改 ${itemPath} 的权限: ${e.message}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (fixedCount > 0) {
|
|
70
|
+
console.log(`\n🔧 已修复文件权限 (所有者: ${realUser})`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fixedCount > 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Recursively fix directory ownership
|
|
78
|
+
*/
|
|
79
|
+
function fixDirectoryOwnership(dirPath, uid, gid) {
|
|
80
|
+
// Fix the directory itself
|
|
81
|
+
fs.chownSync(dirPath, uid, gid);
|
|
82
|
+
|
|
83
|
+
// Fix all contents
|
|
84
|
+
const items = fs.readdirSync(dirPath);
|
|
85
|
+
|
|
86
|
+
for (const item of items) {
|
|
87
|
+
const itemPath = require('path').join(dirPath, item);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const stat = fs.lstatSync(itemPath); // Use lstatSync to handle symlinks
|
|
91
|
+
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
fixDirectoryOwnership(itemPath, uid, gid);
|
|
94
|
+
} else {
|
|
95
|
+
fs.chownSync(itemPath, uid, gid);
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// Skip items that can't be accessed
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
fixPermissions
|
|
105
|
+
};
|
package/package.json
CHANGED
package/services/claude.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { fixPermissions } = require('../lib/permissions');
|
|
4
5
|
|
|
5
6
|
const homeDir = os.homedir();
|
|
6
7
|
const configDir = path.join(homeDir, '.claude');
|
|
@@ -58,7 +59,10 @@ module.exports = {
|
|
|
58
59
|
|
|
59
60
|
// Write file
|
|
60
61
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
61
|
-
|
|
62
|
+
|
|
63
|
+
// Fix permissions if running with sudo
|
|
64
|
+
fixPermissions([configDir, settingsPath]);
|
|
65
|
+
|
|
62
66
|
return [settingsPath];
|
|
63
67
|
}
|
|
64
68
|
};
|
package/services/codex.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { fixPermissions } = require('../lib/permissions');
|
|
4
5
|
|
|
5
6
|
const homeDir = os.homedir();
|
|
6
7
|
const configDir = path.join(homeDir, '.codex');
|
|
@@ -114,6 +115,9 @@ module.exports = {
|
|
|
114
115
|
authData.AIHEZU_OAI_KEY = apiKey;
|
|
115
116
|
fs.writeFileSync(authPath, JSON.stringify(authData, null, 2), 'utf8');
|
|
116
117
|
|
|
118
|
+
// Fix permissions if running with sudo
|
|
119
|
+
fixPermissions([configDir, configPath, authPath]);
|
|
120
|
+
|
|
117
121
|
return [configPath, authPath];
|
|
118
122
|
}
|
|
119
123
|
};
|
package/services/gemini.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const { fixPermissions } = require('../lib/permissions');
|
|
4
5
|
|
|
5
6
|
const homeDir = os.homedir();
|
|
6
7
|
const configDir = path.join(homeDir, '.gemini');
|
|
@@ -39,7 +40,7 @@ module.exports = {
|
|
|
39
40
|
fs.writeFileSync(envFilePath, envContent, 'utf8');
|
|
40
41
|
|
|
41
42
|
const configFiles = [envFilePath];
|
|
42
|
-
|
|
43
|
+
|
|
43
44
|
// 2. Conditionally write settings.json only if options are provided
|
|
44
45
|
if (Object.keys(options).length > 0) {
|
|
45
46
|
const settings = {
|
|
@@ -48,6 +49,9 @@ module.exports = {
|
|
|
48
49
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
|
|
49
50
|
configFiles.push(settingsPath);
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
|
|
53
|
+
// Fix permissions if running with sudo
|
|
54
|
+
fixPermissions([configDir, ...configFiles]);
|
|
55
|
+
|
|
52
56
|
return configFiles; }
|
|
53
57
|
};
|