clash-kit 1.1.0 → 1.1.2
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 +133 -124
- package/bin/dns-helper +0 -0
- package/bin/index.js +96 -85
- package/default.yaml +46 -46
- package/index.js +204 -218
- package/lib/api.js +178 -116
- package/lib/commands/init.js +127 -127
- package/lib/commands/proxy.js +73 -73
- package/lib/commands/restart.js +10 -0
- package/lib/commands/start.js +30 -30
- package/lib/commands/status.js +105 -85
- package/lib/commands/stop.js +67 -30
- package/lib/commands/sub.js +81 -81
- package/lib/commands/sysproxy.js +60 -24
- package/lib/commands/test.js +70 -70
- package/lib/commands/tun.js +123 -95
- package/lib/kernel.js +197 -178
- package/lib/port.js +115 -115
- package/lib/subscription.js +128 -125
- package/lib/sysnet.js +73 -52
- package/lib/sysproxy.js +133 -145
- package/lib/tun.js +150 -126
- package/package.json +50 -48
package/lib/api.js
CHANGED
|
@@ -1,116 +1,178 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import YAML from 'yaml'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
-
const __dirname = path.dirname(__filename)
|
|
9
|
-
const CONFIG_PATH = path.join(__dirname, '../config.yaml')
|
|
10
|
-
|
|
11
|
-
export function getProxyPort() {
|
|
12
|
-
try {
|
|
13
|
-
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8')
|
|
14
|
-
const config = YAML.parse(configContent)
|
|
15
|
-
const httpPort = config['port'] || config['mixed-port']
|
|
16
|
-
const socksPort = config['socks-port']
|
|
17
|
-
return { http: httpPort, socks: socksPort }
|
|
18
|
-
} catch (error) {
|
|
19
|
-
throw new Error(`读取配置文件失败: ${error.message}`)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getApiBase() {
|
|
24
|
-
let apiBase = 'http://127.0.0.1:9090'
|
|
25
|
-
try {
|
|
26
|
-
if (fs.existsSync(CONFIG_PATH)) {
|
|
27
|
-
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8')
|
|
28
|
-
const config = YAML.parse(configContent)
|
|
29
|
-
if (config['external-controller']) {
|
|
30
|
-
let host = config['external-controller']
|
|
31
|
-
// 处理 :9090 这种情况
|
|
32
|
-
if (host.startsWith(':')) {
|
|
33
|
-
host = '127.0.0.1' + host
|
|
34
|
-
}
|
|
35
|
-
// 处理 0.0.0.0
|
|
36
|
-
host = host.replace('0.0.0.0', '127.0.0.1')
|
|
37
|
-
apiBase = `http://${host}`
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
} catch (e) {
|
|
41
|
-
console.error('读取配置文件失败,使用默认 API 地址', e)
|
|
42
|
-
}
|
|
43
|
-
return apiBase
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const API_SECRET = 'your-strong-secret-key' // 实际项目中应该从 config 读取
|
|
47
|
-
|
|
48
|
-
const headers = {
|
|
49
|
-
Authorization: `Bearer ${API_SECRET}`,
|
|
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
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
export async function
|
|
109
|
-
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import YAML from 'yaml'
|
|
5
|
+
import { fileURLToPath } from 'url'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = path.dirname(__filename)
|
|
9
|
+
const CONFIG_PATH = path.join(__dirname, '../config.yaml')
|
|
10
|
+
|
|
11
|
+
export function getProxyPort() {
|
|
12
|
+
try {
|
|
13
|
+
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8')
|
|
14
|
+
const config = YAML.parse(configContent)
|
|
15
|
+
const httpPort = config['port'] || config['mixed-port']
|
|
16
|
+
const socksPort = config['socks-port']
|
|
17
|
+
return { http: httpPort, socks: socksPort }
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(`读取配置文件失败: ${error.message}`)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getApiBase() {
|
|
24
|
+
let apiBase = 'http://127.0.0.1:9090'
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
27
|
+
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8')
|
|
28
|
+
const config = YAML.parse(configContent)
|
|
29
|
+
if (config['external-controller']) {
|
|
30
|
+
let host = config['external-controller']
|
|
31
|
+
// 处理 :9090 这种情况
|
|
32
|
+
if (host.startsWith(':')) {
|
|
33
|
+
host = '127.0.0.1' + host
|
|
34
|
+
}
|
|
35
|
+
// 处理 0.0.0.0
|
|
36
|
+
host = host.replace('0.0.0.0', '127.0.0.1')
|
|
37
|
+
apiBase = `http://${host}`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('读取配置文件失败,使用默认 API 地址', e)
|
|
42
|
+
}
|
|
43
|
+
return apiBase
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const API_SECRET = 'your-strong-secret-key' // 实际项目中应该从 config 读取
|
|
47
|
+
|
|
48
|
+
const headers = {
|
|
49
|
+
Authorization: `Bearer ${API_SECRET}`,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 获取所有代理节点信息
|
|
53
|
+
export async function getProxies() {
|
|
54
|
+
try {
|
|
55
|
+
const res = await axios.get(`${getApiBase()}/proxies`, { headers })
|
|
56
|
+
return res.data.proxies
|
|
57
|
+
} catch (err) {
|
|
58
|
+
throw new Error(`
|
|
59
|
+
无法连接到 Clash API: ${err.message}
|
|
60
|
+
|
|
61
|
+
请确保 Clash 正在运行,并且配置文件中的 external-controller 已正确设置。
|
|
62
|
+
你可以通过 ck status 命令检查状态。
|
|
63
|
+
`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function switchProxy(groupName, proxyName) {
|
|
68
|
+
try {
|
|
69
|
+
await axios.put(`${getApiBase()}/proxies/${encodeURIComponent(groupName)}`, { name: proxyName }, { headers })
|
|
70
|
+
} catch (err) {
|
|
71
|
+
throw new Error(`切换节点失败: ${err.message}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function getProxyDelay(proxyName, testUrl = 'http://www.gstatic.com/generate_204') {
|
|
76
|
+
try {
|
|
77
|
+
const res = await axios.get(`${getApiBase()}/proxies/${encodeURIComponent(proxyName)}/delay`, {
|
|
78
|
+
params: {
|
|
79
|
+
timeout: 5000,
|
|
80
|
+
url: testUrl,
|
|
81
|
+
},
|
|
82
|
+
headers,
|
|
83
|
+
})
|
|
84
|
+
return res.data.delay
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return -1 // 超时或失败
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function getConfig() {
|
|
91
|
+
try {
|
|
92
|
+
const res = await axios.get(`${getApiBase()}/configs`, { headers })
|
|
93
|
+
return res.data
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`获取配置失败: ${err.message}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 重新加载基本配置,必须发送数据,URL 需携带 ?force=true 强制执行
|
|
99
|
+
export async function reloadBaseConfig() {
|
|
100
|
+
try {
|
|
101
|
+
const res = await axios.put(`${getApiBase()}/configs?force=true`, {}, { headers })
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new Error(`重新加载配置失败: ${err.message}`)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 获取策略组信息
|
|
108
|
+
export async function getProxyGroups() {
|
|
109
|
+
try {
|
|
110
|
+
const res = await axios.get(`${getApiBase()}/group`, { headers })
|
|
111
|
+
return res.data
|
|
112
|
+
} catch (err) {
|
|
113
|
+
throw new Error(`获取策略组失败: ${err.message}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 获取所有代理集合的所有信息
|
|
118
|
+
export async function getProxyProviders() {
|
|
119
|
+
try {
|
|
120
|
+
const res = await axios.get(`${getApiBase()}/providers/proxies`, { headers })
|
|
121
|
+
return res.data
|
|
122
|
+
} catch (err) {
|
|
123
|
+
throw new Error(`获取代理提供者失败: ${err.message}`)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 获取规则信息
|
|
128
|
+
export async function getRules() {
|
|
129
|
+
try {
|
|
130
|
+
const res = await axios.get(`${getApiBase()}/rules`, { headers })
|
|
131
|
+
return res.data
|
|
132
|
+
} catch (err) {
|
|
133
|
+
throw new Error(`获取规则失败: ${err.message}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 获取连接信息 /connections
|
|
138
|
+
export async function getConnections() {
|
|
139
|
+
try {
|
|
140
|
+
const res = await axios.get(`${getApiBase()}/connections`, { headers })
|
|
141
|
+
return res.data
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`获取连接信息失败: ${err.message}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 域名解析信息 /dns/query
|
|
148
|
+
export async function getDnsQueries(name, type = 'A') {
|
|
149
|
+
try {
|
|
150
|
+
const res = await axios.get(`${getApiBase()}/dns/query`, {
|
|
151
|
+
params: { name, type },
|
|
152
|
+
headers,
|
|
153
|
+
})
|
|
154
|
+
return res.data
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new Error(`获取域名解析信息失败: ${err.message}`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function reloadConfig(configPath) {
|
|
161
|
+
try {
|
|
162
|
+
// Clash API: PUT /configs
|
|
163
|
+
// payload: { path: '/absolute/path/to/config.yaml' }
|
|
164
|
+
await axios.put(`${getApiBase()}/configs?force=true`, { path: configPath }, { headers })
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new Error(`重载配置失败: ${err.message}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function isClashRunning() {
|
|
171
|
+
try {
|
|
172
|
+
// 使用较短的超时时间快速检查
|
|
173
|
+
await axios.get(`${getApiBase()}/version`, { timeout: 200 })
|
|
174
|
+
return true
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
import path from 'path'
|
|
2
|
-
import fs from 'fs'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
import { downloadClash } from '../kernel.js'
|
|
5
|
-
import axios from 'axios'
|
|
6
|
-
import ora from 'ora'
|
|
7
|
-
import chalk from 'chalk'
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
-
const __dirname = path.dirname(__filename)
|
|
11
|
-
|
|
12
|
-
const DEFAULT_CONFIG = `mixed-port: 7890\n`
|
|
13
|
-
|
|
14
|
-
const RESOURCES = [
|
|
15
|
-
{
|
|
16
|
-
filename: 'country.mmdb',
|
|
17
|
-
url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb',
|
|
18
|
-
},
|
|
19
|
-
// {
|
|
20
|
-
// filename: 'geoip.metadb',
|
|
21
|
-
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
|
22
|
-
// },
|
|
23
|
-
// {
|
|
24
|
-
// filename: 'geosite.dat',
|
|
25
|
-
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
|
26
|
-
// },
|
|
27
|
-
// {
|
|
28
|
-
// filename: 'geoip.dat',
|
|
29
|
-
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',
|
|
30
|
-
// },
|
|
31
|
-
// {
|
|
32
|
-
// filename: 'ASN.mmdb',
|
|
33
|
-
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb',
|
|
34
|
-
// },
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
async function downloadResource(resource, targetDir) {
|
|
38
|
-
const filePath = path.join(targetDir, resource.filename)
|
|
39
|
-
const spinner = ora(`正在下载 ${resource.filename}...`).start()
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
const response = await axios({
|
|
43
|
-
url: resource.url,
|
|
44
|
-
method: 'GET',
|
|
45
|
-
responseType: 'arraybuffer',
|
|
46
|
-
timeout: 30 * 1000, // 30s timeout
|
|
47
|
-
})
|
|
48
|
-
fs.writeFileSync(filePath, response.data)
|
|
49
|
-
spinner.succeed(`${resource.filename} 下载完成`)
|
|
50
|
-
} catch (e) {
|
|
51
|
-
spinner.fail(`${resource.filename} 下载失败: ${e.message}`)
|
|
52
|
-
// 不要抛出错误,让其他资源继续下载
|
|
53
|
-
// throw e
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function init(options) {
|
|
58
|
-
const rootDir = path.join(__dirname, '../..')
|
|
59
|
-
const binName = process.platform === 'win32' ? 'clash-kit.exe' : 'clash-kit'
|
|
60
|
-
const binPath = path.join(rootDir, binName)
|
|
61
|
-
const configPath = path.join(rootDir, 'config.yaml')
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
// 创建默认配置文件(如果不存在)
|
|
65
|
-
if (!fs.existsSync(configPath)) {
|
|
66
|
-
const defaultConfigPath = path.join(rootDir, 'default.yaml')
|
|
67
|
-
if (fs.existsSync(defaultConfigPath)) {
|
|
68
|
-
fs.copyFileSync(defaultConfigPath, configPath)
|
|
69
|
-
console.log(`已从 default.yaml 创建配置文件: ${configPath}`)
|
|
70
|
-
} else {
|
|
71
|
-
console.warn(chalk.yellow('警告: 未找到 default.yaml,将创建最小配置文件'))
|
|
72
|
-
fs.writeFileSync(configPath, DEFAULT_CONFIG, 'utf8')
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 检查并下载资源文件
|
|
77
|
-
console.log(chalk.blue('\n正在检查资源文件...'))
|
|
78
|
-
for (const resource of RESOURCES) {
|
|
79
|
-
const filePath = path.join(rootDir, resource.filename)
|
|
80
|
-
if (options.force && fs.existsSync(filePath)) {
|
|
81
|
-
try {
|
|
82
|
-
fs.unlinkSync(filePath)
|
|
83
|
-
} catch (e) {}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!fs.existsSync(filePath)) {
|
|
87
|
-
await downloadResource(resource, rootDir)
|
|
88
|
-
} else {
|
|
89
|
-
console.log(chalk.gray(`资源 ${resource.filename} 已存在`))
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
console.log(chalk.green('资源检查完成\n'))
|
|
93
|
-
|
|
94
|
-
if (fs.existsSync(binPath) && !options.force) {
|
|
95
|
-
console.log(`Clash 内核已存在: ${binPath}`)
|
|
96
|
-
console.log('正在检查权限...')
|
|
97
|
-
if (process.platform !== 'win32') {
|
|
98
|
-
// 检查是否已有 SUID 权限,如果有则不再重置为 755
|
|
99
|
-
const stats = fs.statSync(binPath)
|
|
100
|
-
const hasSuid = (stats.mode & 0o4000) === 0o4000
|
|
101
|
-
|
|
102
|
-
if (!hasSuid) {
|
|
103
|
-
fs.chmodSync(binPath, 0o755)
|
|
104
|
-
console.log('权限已设置为 755 (普通执行权限)。')
|
|
105
|
-
} else {
|
|
106
|
-
console.log('检测到 SUID 权限,保持不变。')
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
console.log('权限检查通过!')
|
|
110
|
-
return
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (options.force && fs.existsSync(binPath)) {
|
|
114
|
-
console.log('强制更新模式,正在移除旧内核...')
|
|
115
|
-
try {
|
|
116
|
-
fs.unlinkSync(binPath)
|
|
117
|
-
} catch (e) {}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
console.log('正在初始化 Clash 内核...')
|
|
121
|
-
await downloadClash(rootDir)
|
|
122
|
-
console.log('Clash 内核初始化成功!')
|
|
123
|
-
} catch (err) {
|
|
124
|
-
console.error(`初始化失败: ${err.message}`)
|
|
125
|
-
process.exit(1)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { downloadClash } from '../kernel.js'
|
|
5
|
+
import axios from 'axios'
|
|
6
|
+
import ora from 'ora'
|
|
7
|
+
import chalk from 'chalk'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = `mixed-port: 7890\n`
|
|
13
|
+
|
|
14
|
+
const RESOURCES = [
|
|
15
|
+
{
|
|
16
|
+
filename: 'country.mmdb',
|
|
17
|
+
url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country-lite.mmdb',
|
|
18
|
+
},
|
|
19
|
+
// {
|
|
20
|
+
// filename: 'geoip.metadb',
|
|
21
|
+
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
|
22
|
+
// },
|
|
23
|
+
// {
|
|
24
|
+
// filename: 'geosite.dat',
|
|
25
|
+
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
|
26
|
+
// },
|
|
27
|
+
// {
|
|
28
|
+
// filename: 'geoip.dat',
|
|
29
|
+
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat',
|
|
30
|
+
// },
|
|
31
|
+
// {
|
|
32
|
+
// filename: 'ASN.mmdb',
|
|
33
|
+
// url: 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb',
|
|
34
|
+
// },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
async function downloadResource(resource, targetDir) {
|
|
38
|
+
const filePath = path.join(targetDir, resource.filename)
|
|
39
|
+
const spinner = ora(`正在下载 ${resource.filename}...`).start()
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await axios({
|
|
43
|
+
url: resource.url,
|
|
44
|
+
method: 'GET',
|
|
45
|
+
responseType: 'arraybuffer',
|
|
46
|
+
timeout: 30 * 1000, // 30s timeout
|
|
47
|
+
})
|
|
48
|
+
fs.writeFileSync(filePath, response.data)
|
|
49
|
+
spinner.succeed(`${resource.filename} 下载完成`)
|
|
50
|
+
} catch (e) {
|
|
51
|
+
spinner.fail(`${resource.filename} 下载失败: ${e.message}`)
|
|
52
|
+
// 不要抛出错误,让其他资源继续下载
|
|
53
|
+
// throw e
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function init(options) {
|
|
58
|
+
const rootDir = path.join(__dirname, '../..')
|
|
59
|
+
const binName = process.platform === 'win32' ? 'clash-kit.exe' : 'clash-kit'
|
|
60
|
+
const binPath = path.join(rootDir, binName)
|
|
61
|
+
const configPath = path.join(rootDir, 'config.yaml')
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// 创建默认配置文件(如果不存在)
|
|
65
|
+
if (!fs.existsSync(configPath)) {
|
|
66
|
+
const defaultConfigPath = path.join(rootDir, 'default.yaml')
|
|
67
|
+
if (fs.existsSync(defaultConfigPath)) {
|
|
68
|
+
fs.copyFileSync(defaultConfigPath, configPath)
|
|
69
|
+
console.log(`已从 default.yaml 创建配置文件: ${configPath}`)
|
|
70
|
+
} else {
|
|
71
|
+
console.warn(chalk.yellow('警告: 未找到 default.yaml,将创建最小配置文件'))
|
|
72
|
+
fs.writeFileSync(configPath, DEFAULT_CONFIG, 'utf8')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 检查并下载资源文件
|
|
77
|
+
console.log(chalk.blue('\n正在检查资源文件...'))
|
|
78
|
+
for (const resource of RESOURCES) {
|
|
79
|
+
const filePath = path.join(rootDir, resource.filename)
|
|
80
|
+
if (options.force && fs.existsSync(filePath)) {
|
|
81
|
+
try {
|
|
82
|
+
fs.unlinkSync(filePath)
|
|
83
|
+
} catch (e) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(filePath)) {
|
|
87
|
+
await downloadResource(resource, rootDir)
|
|
88
|
+
} else {
|
|
89
|
+
console.log(chalk.gray(`资源 ${resource.filename} 已存在`))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
console.log(chalk.green('资源检查完成\n'))
|
|
93
|
+
|
|
94
|
+
if (fs.existsSync(binPath) && !options.force) {
|
|
95
|
+
console.log(`Clash 内核已存在: ${binPath}`)
|
|
96
|
+
console.log('正在检查权限...')
|
|
97
|
+
if (process.platform !== 'win32') {
|
|
98
|
+
// 检查是否已有 SUID 权限,如果有则不再重置为 755
|
|
99
|
+
const stats = fs.statSync(binPath)
|
|
100
|
+
const hasSuid = (stats.mode & 0o4000) === 0o4000
|
|
101
|
+
|
|
102
|
+
if (!hasSuid) {
|
|
103
|
+
fs.chmodSync(binPath, 0o755)
|
|
104
|
+
console.log('权限已设置为 755 (普通执行权限)。')
|
|
105
|
+
} else {
|
|
106
|
+
console.log('检测到 SUID 权限,保持不变。')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
console.log('权限检查通过!')
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (options.force && fs.existsSync(binPath)) {
|
|
114
|
+
console.log('强制更新模式,正在移除旧内核...')
|
|
115
|
+
try {
|
|
116
|
+
fs.unlinkSync(binPath)
|
|
117
|
+
} catch (e) {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('正在初始化 Clash 内核...')
|
|
121
|
+
await downloadClash(rootDir)
|
|
122
|
+
console.log('Clash 内核初始化成功!')
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(`初始化失败: ${err.message}`)
|
|
125
|
+
process.exit(1)
|
|
126
|
+
}
|
|
127
|
+
}
|