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/commands/sub.js
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
import { select, input } from '@inquirer/prompts'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import fs from 'fs'
|
|
4
|
-
import { fileURLToPath } from 'url'
|
|
5
|
-
import chalk from 'chalk'
|
|
6
|
-
import * as sub from '../subscription.js'
|
|
7
|
-
import { CLASH_BIN_PATH } from '../../index.js'
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
-
const __dirname = path.dirname(__filename)
|
|
11
|
-
|
|
12
|
-
async function handleAddSubscription(url, name) {
|
|
13
|
-
const profiles = sub.listProfiles()
|
|
14
|
-
// 没有找到可选的订阅,或没找到 config.yaml
|
|
15
|
-
// __dirname here is lib/commands. Config is at ROOT/config.yaml. so join ../../config.yaml
|
|
16
|
-
const isFirst = profiles.length === 0 || !fs.existsSync(path.join(__dirname, '../../config.yaml'))
|
|
17
|
-
|
|
18
|
-
await sub.downloadSubscription(url, name)
|
|
19
|
-
|
|
20
|
-
if (!isFirst) return
|
|
21
|
-
console.log(`检测到这是第一个订阅,正在自动切换到 ${name}...`)
|
|
22
|
-
await sub.useProfile(name)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function manageSub(options) {
|
|
26
|
-
// 检查 clash-kit 二进制文件是否存在
|
|
27
|
-
if (!fs.existsSync(CLASH_BIN_PATH)) {
|
|
28
|
-
return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (options.add) {
|
|
32
|
-
if (!options.name) return console.error('错误: 添加订阅时必须指定名称 (-n)')
|
|
33
|
-
try {
|
|
34
|
-
await handleAddSubscription(options.add, options.name)
|
|
35
|
-
} catch (err) {
|
|
36
|
-
console.error(err.message)
|
|
37
|
-
}
|
|
38
|
-
} else if (options.list) {
|
|
39
|
-
const profiles = sub.listProfiles()
|
|
40
|
-
console.log(`${profiles.length ? '已添加的订阅:' : '暂无已添加的订阅'}`)
|
|
41
|
-
profiles.forEach(p => console.log(`- ${p}`))
|
|
42
|
-
} else if (options.use) {
|
|
43
|
-
try {
|
|
44
|
-
await sub.useProfile(options.use)
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error(err.message)
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
// 交互式模式
|
|
50
|
-
const profiles = sub.listProfiles()
|
|
51
|
-
|
|
52
|
-
const action = await select({
|
|
53
|
-
message: '请选择操作:',
|
|
54
|
-
choices: [
|
|
55
|
-
{ name: '切换订阅', value: 'switch' },
|
|
56
|
-
{ name: '添加订阅', value: 'add' },
|
|
57
|
-
],
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
if (action === 'switch') {
|
|
61
|
-
if (profiles.length === 0) {
|
|
62
|
-
console.log('暂无订阅,请先添加')
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
const profile = await select({
|
|
66
|
-
message: '选择要使用的订阅:',
|
|
67
|
-
choices: profiles.map(p => ({ name: p, value: p })),
|
|
68
|
-
})
|
|
69
|
-
await sub.useProfile(profile)
|
|
70
|
-
} else if (action === 'add') {
|
|
71
|
-
const url = await input({ message: '请输入订阅链接:' })
|
|
72
|
-
const name = await input({ message: '请输入订阅名称:' })
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await handleAddSubscription(url, name)
|
|
76
|
-
} catch (err) {
|
|
77
|
-
console.error(err.message)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
import { select, input } from '@inquirer/prompts'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import * as sub from '../subscription.js'
|
|
7
|
+
import { CLASH_BIN_PATH } from '../../index.js'
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
10
|
+
const __dirname = path.dirname(__filename)
|
|
11
|
+
|
|
12
|
+
async function handleAddSubscription(url, name) {
|
|
13
|
+
const profiles = sub.listProfiles()
|
|
14
|
+
// 没有找到可选的订阅,或没找到 config.yaml
|
|
15
|
+
// __dirname here is lib/commands. Config is at ROOT/config.yaml. so join ../../config.yaml
|
|
16
|
+
const isFirst = profiles.length === 0 || !fs.existsSync(path.join(__dirname, '../../config.yaml'))
|
|
17
|
+
|
|
18
|
+
await sub.downloadSubscription(url, name)
|
|
19
|
+
|
|
20
|
+
if (!isFirst) return
|
|
21
|
+
console.log(`检测到这是第一个订阅,正在自动切换到 ${name}...`)
|
|
22
|
+
await sub.useProfile(name)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function manageSub(options) {
|
|
26
|
+
// 检查 clash-kit 二进制文件是否存在
|
|
27
|
+
if (!fs.existsSync(CLASH_BIN_PATH)) {
|
|
28
|
+
return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.add) {
|
|
32
|
+
if (!options.name) return console.error('错误: 添加订阅时必须指定名称 (-n)')
|
|
33
|
+
try {
|
|
34
|
+
await handleAddSubscription(options.add, options.name)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err.message)
|
|
37
|
+
}
|
|
38
|
+
} else if (options.list) {
|
|
39
|
+
const profiles = sub.listProfiles()
|
|
40
|
+
console.log(`${profiles.length ? '已添加的订阅:' : '暂无已添加的订阅'}`)
|
|
41
|
+
profiles.forEach(p => console.log(`- ${p}`))
|
|
42
|
+
} else if (options.use) {
|
|
43
|
+
try {
|
|
44
|
+
await sub.useProfile(options.use)
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(err.message)
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// 交互式模式
|
|
50
|
+
const profiles = sub.listProfiles()
|
|
51
|
+
|
|
52
|
+
const action = await select({
|
|
53
|
+
message: '请选择操作:',
|
|
54
|
+
choices: [
|
|
55
|
+
{ name: '切换订阅', value: 'switch' },
|
|
56
|
+
{ name: '添加订阅', value: 'add' },
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (action === 'switch') {
|
|
61
|
+
if (profiles.length === 0) {
|
|
62
|
+
console.log('暂无订阅,请先添加')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const profile = await select({
|
|
66
|
+
message: '选择要使用的订阅:',
|
|
67
|
+
choices: profiles.map(p => ({ name: p, value: p })),
|
|
68
|
+
})
|
|
69
|
+
await sub.useProfile(profile)
|
|
70
|
+
} else if (action === 'add') {
|
|
71
|
+
const url = await input({ message: '请输入订阅链接:' })
|
|
72
|
+
const name = await input({ message: '请输入订阅名称:' })
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await handleAddSubscription(url, name)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(err.message)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/lib/commands/sysproxy.js
CHANGED
|
@@ -1,24 +1,60 @@
|
|
|
1
|
-
import { select } from '@inquirer/prompts'
|
|
2
|
-
import * as sysproxy from '../sysproxy.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
1
|
+
import { select } from '@inquirer/prompts'
|
|
2
|
+
import * as sysproxy from '../sysproxy.js'
|
|
3
|
+
import ora from 'ora'
|
|
4
|
+
import boxen from 'boxen'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
|
|
7
|
+
async function handleAction(action) {
|
|
8
|
+
if (action === 'on') {
|
|
9
|
+
const spinner = ora('正在开启系统代理...').start()
|
|
10
|
+
const result = await sysproxy.enableSystemProxy()
|
|
11
|
+
if (result.success) {
|
|
12
|
+
spinner.stop()
|
|
13
|
+
const content = [`状态: ${chalk.green('已开启')}`, `地址: ${chalk.cyan(`${result.host}:${result.port}`)}`]
|
|
14
|
+
console.log(
|
|
15
|
+
boxen(content.join('\n'), {
|
|
16
|
+
title: '系统代理',
|
|
17
|
+
padding: 1,
|
|
18
|
+
margin: 1,
|
|
19
|
+
borderStyle: 'round',
|
|
20
|
+
borderColor: 'green',
|
|
21
|
+
}),
|
|
22
|
+
)
|
|
23
|
+
} else {
|
|
24
|
+
spinner.fail(`开启失败: ${result.error}`)
|
|
25
|
+
}
|
|
26
|
+
} else if (action === 'off') {
|
|
27
|
+
const spinner = ora('正在关闭系统代理...').start()
|
|
28
|
+
const result = await sysproxy.disableSystemProxy()
|
|
29
|
+
if (result.success) {
|
|
30
|
+
spinner.stop()
|
|
31
|
+
console.log(
|
|
32
|
+
boxen(chalk.yellow('状态: 已关闭'), {
|
|
33
|
+
title: '系统代理',
|
|
34
|
+
padding: 1,
|
|
35
|
+
margin: 1,
|
|
36
|
+
borderStyle: 'round',
|
|
37
|
+
borderColor: 'yellow',
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
} else {
|
|
41
|
+
spinner.fail(`关闭失败: ${result.error}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function setSysProxy(action) {
|
|
47
|
+
if (action === 'on' || action === 'off') {
|
|
48
|
+
await handleAction(action)
|
|
49
|
+
} else {
|
|
50
|
+
// 交互式选择
|
|
51
|
+
const answer = await select({
|
|
52
|
+
message: '请选择系统代理操作:',
|
|
53
|
+
choices: [
|
|
54
|
+
{ name: '开启系统代理', value: 'on' },
|
|
55
|
+
{ name: '关闭系统代理', value: 'off' },
|
|
56
|
+
],
|
|
57
|
+
})
|
|
58
|
+
await handleAction(answer)
|
|
59
|
+
}
|
|
60
|
+
}
|
package/lib/commands/test.js
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import chalk from 'chalk'
|
|
2
|
-
import * as api from '../api.js'
|
|
3
|
-
|
|
4
|
-
export async function test() {
|
|
5
|
-
try {
|
|
6
|
-
const proxies = await api.getProxies()
|
|
7
|
-
// 默认测速 Proxy 组的所有节点
|
|
8
|
-
const group = proxies['Proxy'] || Object.values(proxies).find(p => p.type === 'Selector')
|
|
9
|
-
|
|
10
|
-
if (!group) {
|
|
11
|
-
console.error('找不到 Proxy 组')
|
|
12
|
-
return
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
console.log(`\n
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
results.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
console.log(`${chalk.gray(i + 1 + '.')} ${
|
|
66
|
-
})
|
|
67
|
-
} catch (err) {
|
|
68
|
-
console.error(err.message)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import * as api from '../api.js'
|
|
3
|
+
|
|
4
|
+
export async function test() {
|
|
5
|
+
try {
|
|
6
|
+
const proxies = await api.getProxies()
|
|
7
|
+
// 默认测速 Proxy 组的所有节点
|
|
8
|
+
const group = proxies['Proxy'] || Object.values(proxies).find(p => p.type === 'Selector')
|
|
9
|
+
|
|
10
|
+
if (!group) {
|
|
11
|
+
console.error('找不到 Proxy 组')
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log(`\n[${group.name}]${group.all.length}个节点, 当前选中: ${group.now}\n`)
|
|
16
|
+
|
|
17
|
+
const results = []
|
|
18
|
+
const concurrency = 10 // 并发数量
|
|
19
|
+
const queue = [...group.all]
|
|
20
|
+
const total = group.all.length
|
|
21
|
+
let completed = 0
|
|
22
|
+
const current = group.now // 当前选中节点
|
|
23
|
+
|
|
24
|
+
const worker = async () => {
|
|
25
|
+
while (queue.length > 0) {
|
|
26
|
+
const name = queue.shift()
|
|
27
|
+
if (!name) break
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const testUrl = name === 'DIRECT' ? 'http://connect.rom.miui.com/generate_204' : undefined
|
|
31
|
+
const delay = await api.getProxyDelay(name, testUrl)
|
|
32
|
+
completed++
|
|
33
|
+
const progress = `[${completed}/${total}]`
|
|
34
|
+
const isCurrent = name === current
|
|
35
|
+
const nameDisplay = isCurrent ? chalk.bold.bgCyan(name) : chalk.cyan(name)
|
|
36
|
+
|
|
37
|
+
if (delay > 0) {
|
|
38
|
+
const color = delay < 200 ? chalk.green : delay < 800 ? chalk.yellow : chalk.red
|
|
39
|
+
console.log(`${chalk.gray(progress)} ${nameDisplay}: ${color(delay + 'ms')}`)
|
|
40
|
+
results.push({ name, delay, isCurrent })
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`${chalk.gray(progress)} ${nameDisplay}: ${chalk.red('超时')}`)
|
|
43
|
+
results.push({ name, delay: 99999, isCurrent })
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
completed++
|
|
47
|
+
results.push({ name, delay: 99999, isCurrent: name === current })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, total) }, () => worker()))
|
|
53
|
+
|
|
54
|
+
console.log(chalk.bold.blue('\n=== 测速结果 (Top 5) ==='))
|
|
55
|
+
results.sort((a, b) => a.delay - b.delay)
|
|
56
|
+
results.slice(0, 5).forEach((r, i) => {
|
|
57
|
+
let delayInfo
|
|
58
|
+
if (r.delay === 99999) {
|
|
59
|
+
delayInfo = chalk.red('超时')
|
|
60
|
+
} else {
|
|
61
|
+
const color = r.delay < 200 ? chalk.green : r.delay < 800 ? chalk.yellow : chalk.red
|
|
62
|
+
delayInfo = color(`${r.delay}ms`)
|
|
63
|
+
}
|
|
64
|
+
const nameDisplay = r.isCurrent ? chalk.bold.bgCyan(r.name) : chalk.cyan(r.name)
|
|
65
|
+
console.log(`${chalk.gray(i + 1 + '.')} ${nameDisplay}: ${delayInfo}`)
|
|
66
|
+
})
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(err.message)
|
|
69
|
+
}
|
|
70
|
+
}
|
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 '../../index.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([]) // This is silent now
|
|
77
|
+
if (!dnsResult.success) {
|
|
78
|
+
throw new Error(`恢复 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
|
+
}
|