clash-kit 1.1.1 → 1.1.5
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/.gitignore +30 -21
- package/LICENSE.md +8 -8
- package/README.md +159 -126
- package/bin/index.js +113 -85
- package/default.yaml +46 -46
- package/lib/api.js +178 -116
- package/lib/commands/init.js +133 -127
- package/lib/commands/proxy.js +81 -73
- package/lib/commands/restart.js +10 -0
- package/lib/commands/start.js +38 -30
- package/lib/commands/status.js +126 -85
- package/lib/commands/stop.js +67 -30
- package/lib/commands/sub.js +124 -81
- package/lib/commands/sysproxy.js +60 -24
- package/lib/commands/test.js +74 -70
- package/lib/commands/tun.js +123 -95
- package/lib/kernel.js +197 -178
- package/lib/port.js +115 -115
- package/{index.js → lib/service.js} +200 -218
- package/lib/subscription.js +152 -125
- package/lib/sysnet.js +50 -52
- package/lib/sysproxy.js +133 -145
- package/lib/tun.js +126 -126
- package/package.json +49 -49
package/lib/commands/tun.js
CHANGED
|
@@ -1,95 +1,123 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts'
|
|
2
|
-
import chalk from 'chalk'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
1
|
+
import { select } from '@inquirer/prompts'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import boxen from 'boxen'
|
|
5
|
+
import * as tun from '../tun.js'
|
|
6
|
+
import * as sysnet from '../sysnet.js'
|
|
7
|
+
import { main as startClashService } from '../service.js'
|
|
8
|
+
|
|
9
|
+
async function turnOn() {
|
|
10
|
+
const spinner = ora('正在配置 TUN 模式...').start()
|
|
11
|
+
let shouldRestart = false
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
if (process.platform !== 'win32') {
|
|
15
|
+
spinner.text = '正在检查权限...'
|
|
16
|
+
const hasPerm = tun.checkTunPermissions()
|
|
17
|
+
const isRoot = process.getuid && process.getuid() === 0
|
|
18
|
+
|
|
19
|
+
if (!hasPerm && !isRoot) {
|
|
20
|
+
spinner.stop() // Stop spinner for user interaction
|
|
21
|
+
console.log(chalk.yellow('检测到内核缺少 SUID 权限,TUN 模式可能无法启动。'))
|
|
22
|
+
const confirm = await select({
|
|
23
|
+
message: '是否自动授予内核 SUID 权限 (推荐)?',
|
|
24
|
+
choices: [
|
|
25
|
+
{ name: '是 (需要 sudo 密码)', value: true },
|
|
26
|
+
{ name: '否 (之后可能需要 sudo 启动)', value: false },
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
if (confirm) {
|
|
30
|
+
tun.setupPermissions() // This is noisy
|
|
31
|
+
shouldRestart = true
|
|
32
|
+
}
|
|
33
|
+
spinner.start('正在继续配置...')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spinner.text = '正在更新配置文件...'
|
|
38
|
+
await tun.enableTun()
|
|
39
|
+
|
|
40
|
+
spinner.text = '正在设置系统 DNS...'
|
|
41
|
+
sysnet.setDNS(['223.5.5.5', '114.114.114.114']) // This is noisy
|
|
42
|
+
|
|
43
|
+
spinner.stop()
|
|
44
|
+
|
|
45
|
+
const content = [`TUN 模式: ${chalk.green('已开启')}`, `DNS 设置: ${chalk.cyan('223.5.5.5, 114.114.114.114')}`]
|
|
46
|
+
console.log(
|
|
47
|
+
boxen(content.join('\n'), {
|
|
48
|
+
title: 'TUN 配置成功',
|
|
49
|
+
padding: 1,
|
|
50
|
+
margin: 1,
|
|
51
|
+
borderStyle: 'round',
|
|
52
|
+
borderColor: 'green',
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if (shouldRestart) {
|
|
57
|
+
console.log(chalk.yellow('权限已变更,正在重启 Clash 服务以应用...'))
|
|
58
|
+
await startClashService()
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.gray('提示: 配置已热重载。如 TUN 未生效, 可尝试 `clash restart`。'))
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (spinner.isSpinning) spinner.fail(`设置 TUN 失败: ${err.message}`)
|
|
64
|
+
else console.error(chalk.red(`设置 TUN 失败: ${err.message}`))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function turnOff(options = {}) {
|
|
69
|
+
const silent = options.silent || false
|
|
70
|
+
const spinner = silent ? null : ora('正在关闭 TUN 模式...').start()
|
|
71
|
+
try {
|
|
72
|
+
if (spinner) spinner.text = '正在更新配置文件...'
|
|
73
|
+
await tun.disableTun()
|
|
74
|
+
|
|
75
|
+
if (spinner) spinner.text = '正在恢复系统 DNS...'
|
|
76
|
+
const dnsResult = sysnet.setDNS([])
|
|
77
|
+
if (!dnsResult.success) {
|
|
78
|
+
console.warn(chalk.yellow(`\n DNS 恢复失败 (可忽略,DNS 将在重启后自动恢复): ${dnsResult.error}`))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (spinner) spinner.stop()
|
|
82
|
+
if (!silent) {
|
|
83
|
+
console.log(
|
|
84
|
+
boxen('TUN 模式: ' + chalk.yellow('已关闭'), {
|
|
85
|
+
title: 'TUN 配置成功',
|
|
86
|
+
padding: 1,
|
|
87
|
+
margin: 1,
|
|
88
|
+
borderStyle: 'round',
|
|
89
|
+
borderColor: 'yellow',
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
console.log(chalk.gray('提示: 配置已热重载。'))
|
|
93
|
+
}
|
|
94
|
+
return { success: true }
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (spinner) spinner.fail(`关闭 TUN 失败: ${err.message}`)
|
|
97
|
+
return { success: false, error: err.message }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function setTun(action, options = {}) {
|
|
102
|
+
try {
|
|
103
|
+
if (action === 'on') {
|
|
104
|
+
await turnOn()
|
|
105
|
+
} else if (action === 'off') {
|
|
106
|
+
return await turnOff(options)
|
|
107
|
+
} else {
|
|
108
|
+
const isEnabled = await tun.isTunEnabled()
|
|
109
|
+
const answer = await select({
|
|
110
|
+
message: `请选择 TUN 模式操作 (当前状态: ${isEnabled ? chalk.green('开启') : chalk.gray('关闭')}):`,
|
|
111
|
+
choices: [
|
|
112
|
+
{ name: '开启 TUN 模式', value: 'on' },
|
|
113
|
+
{ name: '关闭 TUN 模式', value: 'off' },
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
if (answer === 'on') await turnOn()
|
|
117
|
+
else await turnOff(options)
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// Catch errors from select/prompts
|
|
121
|
+
console.error(chalk.red(`操作失败: ${err.message}`))
|
|
122
|
+
}
|
|
123
|
+
}
|
package/lib/kernel.js
CHANGED
|
@@ -1,178 +1,197 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import os from 'os'
|
|
5
|
-
import zlib from 'zlib'
|
|
6
|
-
import AdmZip from 'adm-zip'
|
|
7
|
-
import { fileURLToPath } from 'url'
|
|
8
|
-
import ora from 'ora'
|
|
9
|
-
import cliProgress from 'cli-progress'
|
|
10
|
-
import chalk from 'chalk'
|
|
11
|
-
import { execSync } from 'child_process'
|
|
12
|
-
|
|
13
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
-
const __dirname = path.dirname(__filename)
|
|
15
|
-
|
|
16
|
-
const MIHOMO_VERSION_URL = 'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
|
|
17
|
-
|
|
18
|
-
const PLATFORM_MAP = {
|
|
19
|
-
'win32-x64': 'mihomo-windows-amd64-compatible',
|
|
20
|
-
'win32-ia32': 'mihomo-windows-386',
|
|
21
|
-
'win32-arm64': 'mihomo-windows-arm64',
|
|
22
|
-
'darwin-x64': 'mihomo-darwin-amd64-compatible',
|
|
23
|
-
'darwin-arm64': 'mihomo-darwin-arm64',
|
|
24
|
-
'linux-x64': 'mihomo-linux-amd64-compatible',
|
|
25
|
-
'linux-arm64': 'mihomo-linux-arm64',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function downloadClash(targetDir) {
|
|
29
|
-
const platform = os.platform()
|
|
30
|
-
const arch = os.arch()
|
|
31
|
-
const key = `${platform}-${arch}`
|
|
32
|
-
const name = PLATFORM_MAP[key]
|
|
33
|
-
|
|
34
|
-
if (!name) {
|
|
35
|
-
throw new Error(`不支持的平台: ${platform}-${arch}`)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// 1. 获取最新版本
|
|
39
|
-
const spinner = ora('正在获取最新 Mihomo 版本信息...').start()
|
|
40
|
-
let version
|
|
41
|
-
try {
|
|
42
|
-
const { data } = await axios.get(MIHOMO_VERSION_URL)
|
|
43
|
-
version = data.trim()
|
|
44
|
-
spinner.succeed(`检测到最新版本: ${version}`)
|
|
45
|
-
} catch (e) {
|
|
46
|
-
spinner.fail(`获取版本信息失败: ${e.message}`)
|
|
47
|
-
throw new Error(`获取版本信息失败: ${e.message}`)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. 构建下载 URL
|
|
51
|
-
const isWin = platform === 'win32'
|
|
52
|
-
const urlExt = isWin ? 'zip' : 'gz'
|
|
53
|
-
const downloadUrl = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
|
|
54
|
-
const tempFileName = `mihomo-temp.${urlExt}`
|
|
55
|
-
const tempPath = path.join(targetDir, tempFileName)
|
|
56
|
-
const targetBinName = `clash-kit${isWin ? '.exe' : ''}`
|
|
57
|
-
const targetBinPath = path.join(targetDir, targetBinName)
|
|
58
|
-
|
|
59
|
-
// 3. 下载文件
|
|
60
|
-
console.log(`正在下载: ${downloadUrl}`)
|
|
61
|
-
const bar = new cliProgress.SingleBar(
|
|
62
|
-
{
|
|
63
|
-
format: '下载进度 | {bar} | {percentage}% | {valueFormatted}/{totalFormatted} MB',
|
|
64
|
-
hideCursor: true,
|
|
65
|
-
},
|
|
66
|
-
cliProgress.Presets.shades_classic
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
let started = false
|
|
71
|
-
let totalMB = '0.0'
|
|
72
|
-
const response = await axios({
|
|
73
|
-
url: downloadUrl,
|
|
74
|
-
method: 'GET',
|
|
75
|
-
responseType: 'arraybuffer',
|
|
76
|
-
onDownloadProgress: progressEvent => {
|
|
77
|
-
if (progressEvent.total) {
|
|
78
|
-
const loadedMB = (progressEvent.loaded / 1024 / 1024).toFixed(1)
|
|
79
|
-
if (!started) {
|
|
80
|
-
totalMB = (progressEvent.total / 1024 / 1024).toFixed(1)
|
|
81
|
-
bar.start(progressEvent.total, 0, {
|
|
82
|
-
valueFormatted: '0.0',
|
|
83
|
-
totalFormatted: totalMB,
|
|
84
|
-
})
|
|
85
|
-
started = true
|
|
86
|
-
}
|
|
87
|
-
bar.update(progressEvent.loaded, {
|
|
88
|
-
valueFormatted: loadedMB,
|
|
89
|
-
totalFormatted: totalMB,
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
bar.stop()
|
|
95
|
-
|
|
96
|
-
fs.writeFileSync(tempPath, response.data)
|
|
97
|
-
spinner.start(chalk.green('下载完成,正在解压...'))
|
|
98
|
-
} catch (e) {
|
|
99
|
-
bar.stop()
|
|
100
|
-
throw new Error(`下载失败: ${e.message}`)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 4. 解压文件
|
|
104
|
-
try {
|
|
105
|
-
if (isWin) {
|
|
106
|
-
// ZIP 解压
|
|
107
|
-
const zip = new AdmZip(tempPath)
|
|
108
|
-
const zipEntries = zip.getEntries()
|
|
109
|
-
// 查找可执行文件(通常名字包含 mihoo 或 name)
|
|
110
|
-
const entry = zipEntries.find(e => e.entryName.includes(name) || e.entryName.endsWith('.exe'))
|
|
111
|
-
if (!entry) {
|
|
112
|
-
throw new Error('压缩包中未找到可执行文件')
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 1. 解压文件 (使用原文件名)
|
|
116
|
-
zip.extractEntryTo(entry, targetDir, false, true)
|
|
117
|
-
|
|
118
|
-
// 2. 重命名为目标文件名
|
|
119
|
-
const extractedPath = path.join(targetDir, entry.entryName)
|
|
120
|
-
if (targetBinName !== entry.entryName) {
|
|
121
|
-
// 确保目标文件不存在
|
|
122
|
-
if (fs.existsSync(targetBinPath)) {
|
|
123
|
-
fs.unlinkSync(targetBinPath)
|
|
124
|
-
}
|
|
125
|
-
fs.renameSync(extractedPath, targetBinPath)
|
|
126
|
-
}
|
|
127
|
-
} else {
|
|
128
|
-
// GZ 解压
|
|
129
|
-
const fileContents = fs.readFileSync(tempPath)
|
|
130
|
-
const unzipped = zlib.gunzipSync(fileContents)
|
|
131
|
-
fs.writeFileSync(targetBinPath, unzipped)
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {
|
|
134
|
-
spinner.fail(chalk.red(`解压失败: ${e.message}`))
|
|
135
|
-
throw new Error(`解压失败: ${e.message}`)
|
|
136
|
-
} finally {
|
|
137
|
-
// 清理临时文件
|
|
138
|
-
fs.unlinkSync(tempPath)
|
|
139
|
-
spinner.succeed(chalk.green(`解压完成: ${targetBinPath}`))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 5. 设置权限
|
|
143
|
-
if (!isWin) {
|
|
144
|
-
spinner.start('正在设置执行权限...')
|
|
145
|
-
fs.chmodSync(targetBinPath, 0o755)
|
|
146
|
-
spinner.succeed(chalk.green('设置执行权限完成'))
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return targetBinPath
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function
|
|
153
|
-
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
execSync(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import zlib from 'zlib'
|
|
6
|
+
import AdmZip from 'adm-zip'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import ora from 'ora'
|
|
9
|
+
import cliProgress from 'cli-progress'
|
|
10
|
+
import chalk from 'chalk'
|
|
11
|
+
import { execSync } from 'child_process'
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
14
|
+
const __dirname = path.dirname(__filename)
|
|
15
|
+
|
|
16
|
+
const MIHOMO_VERSION_URL = 'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
|
|
17
|
+
|
|
18
|
+
const PLATFORM_MAP = {
|
|
19
|
+
'win32-x64': 'mihomo-windows-amd64-compatible',
|
|
20
|
+
'win32-ia32': 'mihomo-windows-386',
|
|
21
|
+
'win32-arm64': 'mihomo-windows-arm64',
|
|
22
|
+
'darwin-x64': 'mihomo-darwin-amd64-compatible',
|
|
23
|
+
'darwin-arm64': 'mihomo-darwin-arm64',
|
|
24
|
+
'linux-x64': 'mihomo-linux-amd64-compatible',
|
|
25
|
+
'linux-arm64': 'mihomo-linux-arm64',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function downloadClash(targetDir) {
|
|
29
|
+
const platform = os.platform()
|
|
30
|
+
const arch = os.arch()
|
|
31
|
+
const key = `${platform}-${arch}`
|
|
32
|
+
const name = PLATFORM_MAP[key]
|
|
33
|
+
|
|
34
|
+
if (!name) {
|
|
35
|
+
throw new Error(`不支持的平台: ${platform}-${arch}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. 获取最新版本
|
|
39
|
+
const spinner = ora('正在获取最新 Mihomo 版本信息...').start()
|
|
40
|
+
let version
|
|
41
|
+
try {
|
|
42
|
+
const { data } = await axios.get(MIHOMO_VERSION_URL)
|
|
43
|
+
version = data.trim()
|
|
44
|
+
spinner.succeed(`检测到最新版本: ${version}`)
|
|
45
|
+
} catch (e) {
|
|
46
|
+
spinner.fail(`获取版本信息失败: ${e.message}`)
|
|
47
|
+
throw new Error(`获取版本信息失败: ${e.message}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. 构建下载 URL
|
|
51
|
+
const isWin = platform === 'win32'
|
|
52
|
+
const urlExt = isWin ? 'zip' : 'gz'
|
|
53
|
+
const downloadUrl = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
|
|
54
|
+
const tempFileName = `mihomo-temp.${urlExt}`
|
|
55
|
+
const tempPath = path.join(targetDir, tempFileName)
|
|
56
|
+
const targetBinName = `clash-kit${isWin ? '.exe' : ''}`
|
|
57
|
+
const targetBinPath = path.join(targetDir, targetBinName)
|
|
58
|
+
|
|
59
|
+
// 3. 下载文件
|
|
60
|
+
console.log(`正在下载: ${downloadUrl}`)
|
|
61
|
+
const bar = new cliProgress.SingleBar(
|
|
62
|
+
{
|
|
63
|
+
format: '下载进度 | {bar} | {percentage}% | {valueFormatted}/{totalFormatted} MB',
|
|
64
|
+
hideCursor: true,
|
|
65
|
+
},
|
|
66
|
+
cliProgress.Presets.shades_classic
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
let started = false
|
|
71
|
+
let totalMB = '0.0'
|
|
72
|
+
const response = await axios({
|
|
73
|
+
url: downloadUrl,
|
|
74
|
+
method: 'GET',
|
|
75
|
+
responseType: 'arraybuffer',
|
|
76
|
+
onDownloadProgress: progressEvent => {
|
|
77
|
+
if (progressEvent.total) {
|
|
78
|
+
const loadedMB = (progressEvent.loaded / 1024 / 1024).toFixed(1)
|
|
79
|
+
if (!started) {
|
|
80
|
+
totalMB = (progressEvent.total / 1024 / 1024).toFixed(1)
|
|
81
|
+
bar.start(progressEvent.total, 0, {
|
|
82
|
+
valueFormatted: '0.0',
|
|
83
|
+
totalFormatted: totalMB,
|
|
84
|
+
})
|
|
85
|
+
started = true
|
|
86
|
+
}
|
|
87
|
+
bar.update(progressEvent.loaded, {
|
|
88
|
+
valueFormatted: loadedMB,
|
|
89
|
+
totalFormatted: totalMB,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
bar.stop()
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(tempPath, response.data)
|
|
97
|
+
spinner.start(chalk.green('下载完成,正在解压...'))
|
|
98
|
+
} catch (e) {
|
|
99
|
+
bar.stop()
|
|
100
|
+
throw new Error(`下载失败: ${e.message}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. 解压文件
|
|
104
|
+
try {
|
|
105
|
+
if (isWin) {
|
|
106
|
+
// ZIP 解压
|
|
107
|
+
const zip = new AdmZip(tempPath)
|
|
108
|
+
const zipEntries = zip.getEntries()
|
|
109
|
+
// 查找可执行文件(通常名字包含 mihoo 或 name)
|
|
110
|
+
const entry = zipEntries.find(e => e.entryName.includes(name) || e.entryName.endsWith('.exe'))
|
|
111
|
+
if (!entry) {
|
|
112
|
+
throw new Error('压缩包中未找到可执行文件')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 1. 解压文件 (使用原文件名)
|
|
116
|
+
zip.extractEntryTo(entry, targetDir, false, true)
|
|
117
|
+
|
|
118
|
+
// 2. 重命名为目标文件名
|
|
119
|
+
const extractedPath = path.join(targetDir, entry.entryName)
|
|
120
|
+
if (targetBinName !== entry.entryName) {
|
|
121
|
+
// 确保目标文件不存在
|
|
122
|
+
if (fs.existsSync(targetBinPath)) {
|
|
123
|
+
fs.unlinkSync(targetBinPath)
|
|
124
|
+
}
|
|
125
|
+
fs.renameSync(extractedPath, targetBinPath)
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// GZ 解压
|
|
129
|
+
const fileContents = fs.readFileSync(tempPath)
|
|
130
|
+
const unzipped = zlib.gunzipSync(fileContents)
|
|
131
|
+
fs.writeFileSync(targetBinPath, unzipped)
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
spinner.fail(chalk.red(`解压失败: ${e.message}`))
|
|
135
|
+
throw new Error(`解压失败: ${e.message}`)
|
|
136
|
+
} finally {
|
|
137
|
+
// 清理临时文件
|
|
138
|
+
fs.unlinkSync(tempPath)
|
|
139
|
+
spinner.succeed(chalk.green(`解压完成: ${targetBinPath}`))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 5. 设置权限
|
|
143
|
+
if (!isWin) {
|
|
144
|
+
spinner.start('正在设置执行权限...')
|
|
145
|
+
fs.chmodSync(targetBinPath, 0o755)
|
|
146
|
+
spinner.succeed(chalk.green('设置执行权限完成'))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return targetBinPath
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function getClashProcessInfo() {
|
|
153
|
+
try {
|
|
154
|
+
let pid
|
|
155
|
+
if (process.platform === 'win32') {
|
|
156
|
+
const command = "tasklist | findstr clash-kit.exe"
|
|
157
|
+
const output = execSync(command, { encoding: 'utf-8' })
|
|
158
|
+
const match = output.match(/(\d+)/)
|
|
159
|
+
pid = match ? match[0] : null
|
|
160
|
+
} else {
|
|
161
|
+
const command = "pgrep -f clash-kit"
|
|
162
|
+
pid = execSync(command, { encoding: 'utf-8' }).trim()
|
|
163
|
+
}
|
|
164
|
+
return pid ? { pid } : null
|
|
165
|
+
} catch (e) {
|
|
166
|
+
// pgrep/findstr throws an error if no process is found
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function killClashProcess() {
|
|
172
|
+
try {
|
|
173
|
+
if (process.platform === 'win32') {
|
|
174
|
+
execSync('taskkill /F /IM clash-kit.exe', { stdio: 'ignore' })
|
|
175
|
+
} else {
|
|
176
|
+
execSync('pkill -f clash-kit', { stdio: 'ignore' })
|
|
177
|
+
}
|
|
178
|
+
return true
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function testDownload() {
|
|
185
|
+
// 测试下载功能
|
|
186
|
+
const rootDir = path.join(__dirname, '..')
|
|
187
|
+
downloadClash(rootDir)
|
|
188
|
+
.then(binPath => {
|
|
189
|
+
console.log(`Mihomo 内核已下载并解压到: ${binPath}`)
|
|
190
|
+
})
|
|
191
|
+
.catch(err => {
|
|
192
|
+
console.error(`下载失败: ${err.message}`)
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 如果直接运行此文件,则执行测试
|
|
197
|
+
if (import.meta.url === `file://${process.argv[1]}`) testDownload()
|