clash-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Clash CLI
2
+
3
+ 一个基于 Node.js 的 Clash 命令行管理工具,旨在简化 Clash 的配置管理、订阅切换和节点测速等操作。
4
+
5
+ ## 特性
6
+
7
+ - 🔄 **订阅管理**:支持添加、切换多个订阅源。
8
+ - 🌐 **节点切换**:交互式选择并切换当前使用的代理节点。
9
+ - 🔥 **热重载**:切换订阅后立即生效,无需重启服务。
10
+ - ⚡ **节点测速**:支持多线程并发测速,彩色高亮显示延迟结果。
11
+ - 📊 **状态监控**:实时查看服务运行状态、当前节点及延迟。
12
+ - 🛠 **自动初始化**:自动处理二进制文件权限问题。
13
+
14
+ - 💻 **系统代理**:一键开启/关闭 macOS 系统 HTTP 代理。
15
+ - 🛡 **TUN 模式**:高级网络模式,接管系统所有流量(类 VPN 体验)。
16
+
17
+ ## 使用
18
+
19
+ ### 1. 安装
20
+
21
+ ```bash
22
+ npm install -g clash-kit
23
+ # 或者
24
+ pnpm add -g clash-kit
25
+ ```
26
+
27
+ ### 2. 初始化
28
+
29
+ 首次安装后,需要先初始化 Clash 内核与权限:
30
+
31
+ ```bash
32
+ clash init
33
+ ```
34
+
35
+ ### 3. 启动服务
36
+
37
+ 启动 Clash 核心服务(建议在一个单独的终端窗口运行):
38
+
39
+ ```bash
40
+ # 启动 Clash 代理服务
41
+ clash start
42
+
43
+ # 启动并自动开启系统代理
44
+ clash start -s
45
+ ```
46
+
47
+ ### 4. 添加订阅
48
+
49
+ ```bash
50
+ # 交互式管理订阅(添加、切换、删除等)
51
+ clash sub
52
+
53
+ # 手动添加订阅
54
+ clash sub -a "https://example.com/subscribe?token=xxx" -n "abcName"
55
+ ```
56
+
57
+ ### 4. 节点测速与切换
58
+
59
+ ```bash
60
+ # 测速
61
+ clash test
62
+
63
+ # 切换节点
64
+ clash proxy
65
+ ```
66
+
67
+ ### 5. 更多功能
68
+
69
+ ```bash
70
+ # 查看状态
71
+ clash status
72
+
73
+ # 开启 TUN 模式 (需要 sudo 权限)
74
+ sudo clash tun on
75
+ ```
76
+
77
+ ## 命令详解
78
+
79
+ | 命令 | 说明 | 示例 |
80
+ | --------------------- | -------------------------- | ------------------------------------- |
81
+ | `clash init` | 初始化内核及权限 | `clash init` |
82
+ | `clash start` | 启动 Clash 服务 | `clash start -s` (启动并设置系统代理) |
83
+ | `clash stop` | 停止服务并关闭代理 | `clash stop` |
84
+ | `clash status` | 查看运行状态及当前节点延迟 | `clash status` |
85
+ | `clash sub`(推荐) | 管理订阅(交互式) | `clash sub` |
86
+ | `clash sub -a <url>` | 添加订阅 | `clash sub -a "http..." -n "pro"` |
87
+ | `clash sub -u <name>` | 切换订阅 | `clash sub -u "pro"` |
88
+ | `clash sub -l` | 列出所有订阅 | `clash sub -l` |
89
+ | `clash proxy` | 切换节点(交互式) | `clash proxy` |
90
+ | `clash test` | 节点并发测速 | `clash test` |
91
+ | `clash sysproxy` | 设置系统代理 (on/off) | `clash sysproxy on` |
92
+ | `clash tun` | 设置 TUN 模式 (on/off) | `sudo clash tun on` |
93
+
94
+ ## 目录结构
95
+
96
+ - `bin/`: CLI 入口文件
97
+ - `lib/`: 核心逻辑库
98
+ - `profiles/`: 存放下载的订阅配置文件
99
+ - `clash-meta`: Clash 核心二进制文件
100
+ - `config.yaml`: 当前生效的配置文件
101
+
102
+ ## 注意事项
103
+
104
+ - 本工具依赖 `clash-meta` 二进制文件,请确保其与本工具在同一目录下(安装包已包含)。
105
+ - 测速功能依赖 Clash API,请确保 `clash start` 正在运行。
106
+ - 默认 API 地址为 `http://127.0.0.1:9090`。
107
+
108
+ ## License
109
+
110
+ ISC
package/bin/index.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander'
4
+ import { init } from '../lib/commands/init.js'
5
+ import { start } from '../lib/commands/start.js'
6
+ import { stop } from '../lib/commands/stop.js'
7
+ import { setSysProxy } from '../lib/commands/sysproxy.js'
8
+ import { setTun } from '../lib/commands/tun.js'
9
+ import { status } from '../lib/commands/status.js'
10
+ import { manageSub } from '../lib/commands/sub.js'
11
+ import { proxy } from '../lib/commands/proxy.js'
12
+ import { test } from '../lib/commands/test.js'
13
+
14
+ const program = new Command()
15
+
16
+ program.name('clash').description('Clash CLI 管理工具').version('1.0.0')
17
+
18
+ // 初始化 clash 内核
19
+ program
20
+ .command('init')
21
+ .description('初始化 Clash 内核 (下载、解压并设置权限)')
22
+ .option('-f, --force', '强制重新下载内核')
23
+ .action(init)
24
+
25
+ // 启动 clash 服务
26
+ program.command('start').description('启动 Clash 服务').option('-s, --sysproxy', '启动后自动开启系统代理').action(start)
27
+
28
+ // 停止 clash 服务
29
+ program.command('stop').description('停止 Clash 服务').action(stop)
30
+
31
+ // 设置系统代理
32
+ program.command('sysproxy').description('设置系统代理').argument('[action]', 'on 或 off').action(setSysProxy)
33
+
34
+ // 设置 TUN 模式(真正的全局代理,所有流量都会被代理)
35
+ program.command('tun').description('设置 TUN 模式 (可能需要提权)').argument('[action]', 'on 或 off').action(setTun)
36
+
37
+ // 查看 clash 状态
38
+ program.command('status').description('查看 Clash 运行状态').action(status)
39
+
40
+ // 管理订阅
41
+ program
42
+ .command('sub')
43
+ .description('管理订阅')
44
+ .option('-a, --add <url>', '添加订阅链接')
45
+ .option('-n, --name <name>', '订阅名称')
46
+ .option('-l, --list', '列出所有订阅')
47
+ .option('-u, --use <name>', '切换使用的订阅')
48
+ .action(manageSub)
49
+
50
+ // 切换节点
51
+ program.command('proxy').description('切换节点').action(proxy)
52
+
53
+ // 节点测速
54
+ program.command('test').description('节点测速').action(test)
55
+
56
+ program.parse(process.argv)
package/index.js ADDED
@@ -0,0 +1,135 @@
1
+ import { spawn, execSync } from 'child_process'
2
+ import path from 'path'
3
+ import fs from 'fs'
4
+ import chalk from 'chalk'
5
+ import { fileURLToPath } from 'url'
6
+ import { getApiBase, getProxyPort } from './lib/api.js'
7
+ import * as sysproxy from './lib/sysproxy.js'
8
+ import * as tun from './lib/tun.js'
9
+
10
+ const __filename = fileURLToPath(import.meta.url)
11
+ const __dirname = path.dirname(__filename)
12
+
13
+ // ---------------- 1. 配置项 ----------------
14
+ const CLASH_BIN_PATH = path.join(__dirname, 'clash-meta') // 解压后的二进制文件路径
15
+ const CLASH_CONFIG_PATH = path.join(__dirname, 'config.yaml') // 配置文件路径
16
+
17
+ // ---------------- 2. 启动 Clash.Meta 进程 ----------------
18
+ function startClash() {
19
+ // 尝试停止已存在的进程
20
+ try {
21
+ execSync('pkill -f clash-meta')
22
+ } catch (e) {
23
+ // 忽略错误,说明没有运行中的进程
24
+ }
25
+
26
+ const logPath = path.join(__dirname, 'clash.log')
27
+ const logFd = fs.openSync(logPath, 'a')
28
+
29
+ const clashProcess = spawn(CLASH_BIN_PATH, ['-f', CLASH_CONFIG_PATH, '-d', __dirname], {
30
+ cwd: __dirname,
31
+ detached: true,
32
+ stdio: ['ignore', logFd, logFd], // 重定向 stdout 和 stderr 到日志文件
33
+ })
34
+
35
+ clashProcess.on('error', err => {
36
+ console.error(`启动 Clash.Meta 失败: ${err.message}`)
37
+ process.exit(1)
38
+ })
39
+
40
+ // 监听子进程退出,方便调试
41
+ clashProcess.on('exit', (code, signal) => {
42
+ if (code !== 0) {
43
+ console.log(`Clash 进程异常退出,代码: ${code}, 信号: ${signal}。请查看 clash.log 获取详情。`)
44
+ }
45
+ })
46
+
47
+ // 解除与父进程的引用,让子进程在后台独立运行
48
+ clashProcess.unref()
49
+
50
+ return clashProcess
51
+ }
52
+
53
+ // ---------------- 3. 清理函数 ----------------
54
+ async function cleanup() {
55
+ try {
56
+ // 关闭系统代理
57
+ await sysproxy.disableSystemProxy()
58
+
59
+ // 检查并关闭 TUN 模式
60
+ const tunEnabled = await tun.isTunEnabled()
61
+ if (tunEnabled) {
62
+ await tun.disableTun()
63
+ console.log('TUN 模式已关闭')
64
+ }
65
+
66
+ // 停止 Clash 进程
67
+ try {
68
+ execSync('pkill -f clash-meta')
69
+ console.log('Clash 服务已停止')
70
+ } catch (e) {
71
+ // 进程可能已经停止
72
+ }
73
+ } catch (error) {
74
+ console.error('清理过程出错:', error.message)
75
+ }
76
+ }
77
+
78
+ // ---------------- 4. 注册进程退出处理 ----------------
79
+ function setupExitHandlers() {
80
+ // 处理正常退出 (Ctrl+C)
81
+ process.on('SIGINT', async () => {
82
+ console.log('\n\n正在清理配置并退出...')
83
+ await cleanup()
84
+ process.exit(0)
85
+ })
86
+
87
+ // 处理终止信号
88
+ process.on('SIGTERM', async () => {
89
+ console.log('\n正在清理配置并退出...')
90
+ await cleanup()
91
+ process.exit(0)
92
+ })
93
+
94
+ // 处理未捕获的异常
95
+ process.on('uncaughtException', async err => {
96
+ console.error('未捕获的异常:', err)
97
+ await cleanup()
98
+ process.exit(1)
99
+ })
100
+ }
101
+
102
+ // ---------------- 5. 执行流程 ----------------
103
+ export function main() {
104
+ // 检查 clash-meta 二进制文件是否存在
105
+ if (!fs.existsSync(CLASH_BIN_PATH)) {
106
+ return console.error(chalk.red('\n找不到 Clash.Meta 内核文件,请先运行 clash init 命令初始化内核!\n'))
107
+ }
108
+ // 检查配置文件是否存在
109
+ if (!fs.existsSync(CLASH_CONFIG_PATH)) {
110
+ return console.error(chalk.red('\n找不到配置文件 config.yaml,请先通过 clash sub 命令添加或选择订阅配置!\n'))
111
+ }
112
+
113
+ // 设置退出处理
114
+ setupExitHandlers()
115
+
116
+ const clashProcess = startClash()
117
+ const { http, socks } = getProxyPort()
118
+
119
+ console.log(chalk.green('\n代理服务已在后台启动✅'))
120
+ if (clashProcess.pid) {
121
+ console.log(`PID: ${chalk.yellow(clashProcess.pid)}`)
122
+ }
123
+
124
+ console.log(``)
125
+ console.log(`HTTP Proxy: ${chalk.cyan(`127.0.0.1:${http}`)}`)
126
+ console.log(`SOCKS5 Proxy: ${chalk.cyan(`127.0.0.1:${socks}`)}`)
127
+ console.log(`API: ${chalk.cyan.underline(getApiBase())}`)
128
+ console.log(``)
129
+ console.log(chalk.gray('提示: 如需停止代理可使用 clash stop 命令'))
130
+ }
131
+
132
+ // 运行脚本
133
+ if (process.argv[1] === __filename) {
134
+ main()
135
+ }
package/lib/api.js ADDED
@@ -0,0 +1,106 @@
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
+ export async function getProxies() {
53
+ try {
54
+ const res = await axios.get(`${getApiBase()}/proxies`, { headers })
55
+ return res.data.proxies
56
+ } catch (err) {
57
+ throw new Error(`
58
+ 无法连接到 Clash API: ${err.message}
59
+
60
+ 请确保 Clash 正在运行并且 API 密钥正确设置。
61
+ 你可以通过 clash status 命令检查状态。
62
+ `)
63
+ }
64
+ }
65
+
66
+ export async function switchProxy(groupName, proxyName) {
67
+ try {
68
+ await axios.put(`${getApiBase()}/proxies/${encodeURIComponent(groupName)}`, { name: proxyName }, { headers })
69
+ } catch (err) {
70
+ throw new Error(`切换节点失败: ${err.message}`)
71
+ }
72
+ }
73
+
74
+ export async function getProxyDelay(proxyName, testUrl = 'http://www.gstatic.com/generate_204') {
75
+ try {
76
+ const res = await axios.get(`${getApiBase()}/proxies/${encodeURIComponent(proxyName)}/delay`, {
77
+ params: {
78
+ timeout: 5000,
79
+ url: testUrl,
80
+ },
81
+ headers,
82
+ })
83
+ return res.data.delay
84
+ } catch (err) {
85
+ return -1 // 超时或失败
86
+ }
87
+ }
88
+
89
+ export async function getConfig() {
90
+ try {
91
+ const res = await axios.get(`${getApiBase()}/configs`, { headers })
92
+ return res.data
93
+ } catch (err) {
94
+ throw new Error(`获取配置失败: ${err.message}`)
95
+ }
96
+ }
97
+
98
+ export async function reloadConfig(configPath) {
99
+ try {
100
+ // Clash API: PUT /configs
101
+ // payload: { path: '/absolute/path/to/config.yaml' }
102
+ await axios.put(`${getApiBase()}/configs?force=true`, { path: configPath }, { headers })
103
+ } catch (err) {
104
+ throw new Error(`重载配置失败: ${err.message}`)
105
+ }
106
+ }
@@ -0,0 +1,58 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import { fileURLToPath } from 'url'
4
+ import { downloadClash } from '../kernel.js'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(__filename)
8
+
9
+ const DEFAULT_CONFIG = `mixed-port: 7890
10
+ `
11
+
12
+ export async function init(options) {
13
+ const rootDir = path.join(__dirname, '../..')
14
+ const binName = process.platform === 'win32' ? 'clash-meta.exe' : 'clash-meta'
15
+ const binPath = path.join(rootDir, binName)
16
+ const configPath = path.join(rootDir, 'config.yaml')
17
+
18
+ try {
19
+ // 创建默认配置文件(如果不存在)
20
+ if (!fs.existsSync(configPath)) {
21
+ fs.writeFileSync(configPath, DEFAULT_CONFIG, 'utf8')
22
+ console.log(`已创建默认配置文件: ${configPath}`)
23
+ }
24
+
25
+ if (fs.existsSync(binPath) && !options.force) {
26
+ console.log(`Clash 内核已存在: ${binPath}`)
27
+ console.log('正在检查权限...')
28
+ if (process.platform !== 'win32') {
29
+ // 检查是否已有 SUID 权限,如果有则不再重置为 755
30
+ const stats = fs.statSync(binPath)
31
+ const hasSuid = (stats.mode & 0o4000) === 0o4000
32
+
33
+ if (!hasSuid) {
34
+ fs.chmodSync(binPath, 0o755)
35
+ console.log('权限已设置为 755 (普通执行权限)。')
36
+ } else {
37
+ console.log('检测到 SUID 权限,保持不变。')
38
+ }
39
+ }
40
+ console.log('权限检查通过!')
41
+ return
42
+ }
43
+
44
+ if (options.force && fs.existsSync(binPath)) {
45
+ console.log('强制更新模式,正在移除旧内核...')
46
+ try {
47
+ fs.unlinkSync(binPath)
48
+ } catch (e) {}
49
+ }
50
+
51
+ console.log('正在初始化 Clash 内核...')
52
+ await downloadClash(rootDir)
53
+ console.log('Clash 内核初始化成功!')
54
+ } catch (err) {
55
+ console.error(`初始化失败: ${err.message}`)
56
+ process.exit(1)
57
+ }
58
+ }
@@ -0,0 +1,40 @@
1
+ import { select } from '@inquirer/prompts'
2
+ import ora from 'ora'
3
+ import * as api from '../api.js'
4
+
5
+ export async function proxy() {
6
+ let spinner = ora('正在获取最新代理列表...').start()
7
+ try {
8
+ const proxies = await api.getProxies()
9
+ spinner.stop()
10
+
11
+ // 通常我们只关心 Proxy 组或者 Selector 类型的组
12
+ const groups = Object.values(proxies).filter(p => p.type === 'Selector')
13
+
14
+ if (groups.length === 0) {
15
+ console.log('没有找到可选的节点组')
16
+ return
17
+ }
18
+
19
+ // 选择组
20
+ const groupName = await select({
21
+ message: '请选择节点组:',
22
+ choices: groups.map(g => ({ name: g.name, value: g.name })),
23
+ })
24
+
25
+ const group = proxies[groupName]
26
+
27
+ // 选择节点
28
+ const proxyName = await select({
29
+ message: `[${groupName}] 当前: ${group.now}, 请选择节点:`,
30
+ choices: group.all.map(n => ({ name: n, value: n })),
31
+ })
32
+
33
+ spinner = ora(`正在切换到 ${proxyName}...`).start()
34
+ await api.switchProxy(groupName, proxyName)
35
+ spinner.succeed(`已切换 ${groupName} -> ${proxyName}`)
36
+ } catch (err) {
37
+ if (spinner && spinner.isSpinning) spinner.fail(err.message)
38
+ else console.error(err.message)
39
+ }
40
+ }
@@ -0,0 +1,22 @@
1
+ import * as sysproxy from '../sysproxy.js'
2
+ import { main as startClashService } from '../../index.js'
3
+
4
+ export async function start(options) {
5
+ startClashService()
6
+
7
+ if (options.sysproxy) {
8
+ console.log('正在等待 Clash API 就绪以设置系统代理...')
9
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
10
+
11
+ // 尝试 5 次,每次间隔 1 秒
12
+ for (let i = 0; i < 5; i++) {
13
+ await sleep(1000)
14
+ try {
15
+ const success = await sysproxy.enableSystemProxy()
16
+ if (success) break
17
+ } catch (e) {
18
+ if (i === 4) console.error('设置系统代理超时,请稍后手动设置: clash sysproxy on')
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,85 @@
1
+ import ora from 'ora'
2
+ import * as api from '../api.js'
3
+ import * as sub from '../subscription.js'
4
+ import * as tun from '../tun.js'
5
+ import * as sysproxy from '../sysproxy.js'
6
+
7
+ export async function status() {
8
+ const spinner = ora('正在获取 Clash 状态...').start()
9
+ try {
10
+ const config = await api.getConfig()
11
+ spinner.stop()
12
+ const apiBase = api.getApiBase()
13
+ const currentProfile = sub.getCurrentProfile()
14
+
15
+ // 获取 TUN 和系统代理状态
16
+ const tunEnabled = await tun.isTunEnabled()
17
+ const sysProxyStatus = await sysproxy.getSystemProxyStatus()
18
+
19
+ console.log('\n=== Clash 状态 ===')
20
+ console.log(`状态: \x1b[32m运行中\x1b[0m`)
21
+ console.log(`当前配置: ${currentProfile || '未知 (默认或手动修改)'}`)
22
+ console.log(`API 地址: ${apiBase}`)
23
+ console.log(`运行模式: ${config.mode}`)
24
+ console.log(`HTTP 端口: ${config['port'] || '未设置'}`)
25
+ console.log(`Socks5 端口: ${config['socks-port'] || '未设置'}`)
26
+ if (config['mixed-port']) {
27
+ console.log(`Mixed 端口: ${config['mixed-port']}`)
28
+ }
29
+
30
+ console.log('\n=== 代理模式 ===')
31
+ // TUN 状态
32
+ if (tunEnabled) {
33
+ console.log(`TUN 模式: \x1b[32m已开启\x1b[0m`)
34
+ } else {
35
+ console.log(`TUN 模式: \x1b[90m未开启\x1b[0m`)
36
+ }
37
+ // 系统代理状态
38
+ if (sysProxyStatus.enabled) {
39
+ console.log(`系统代理: \x1b[32m已开启\x1b[0m (${sysProxyStatus.server}:${sysProxyStatus.port})`)
40
+ } else {
41
+ console.log(`系统代理: \x1b[90m未开启\x1b[0m`)
42
+ }
43
+
44
+ const proxies = await api.getProxies()
45
+
46
+ // 过滤出 Selector 类型的代理组
47
+ const selectors = Object.entries(proxies)
48
+ .filter(([name, proxy]) => proxy.type === 'Selector')
49
+ .map(([name, proxy]) => ({ name, now: proxy.now }))
50
+
51
+ console.log('\n=== 节点信息 ===')
52
+
53
+ if (selectors.length === 0) {
54
+ console.log('未找到节点选择组。')
55
+ } else {
56
+ for (const { name, now } of selectors) {
57
+ process.stdout.write(`组 [${name}] 当前节点: ${now} ... 测速中`)
58
+ const testUrl = now === 'DIRECT' ? 'http://connect.rom.miui.com/generate_204' : undefined
59
+ const delay = await api.getProxyDelay(now, testUrl)
60
+
61
+ let delayStr = ''
62
+ if (delay > 0) {
63
+ if (delay < 200) delayStr = `\x1b[32m${delay}ms\x1b[0m`
64
+ else if (delay < 500) delayStr = `\x1b[33m${delay}ms\x1b[0m`
65
+ else delayStr = `\x1b[31m${delay}ms\x1b[0m`
66
+ } else {
67
+ delayStr = `\x1b[31m超时/失败\x1b[0m`
68
+ }
69
+
70
+ process.stdout.clearLine(0)
71
+ process.stdout.cursorTo(0)
72
+ console.log(`组 [${name}] 当前节点: ${now} - 延迟: ${delayStr}`)
73
+ }
74
+ }
75
+ } catch (err) {
76
+ if (spinner.isSpinning) spinner.stop()
77
+ if (err.message && (err.message.includes('ECONNREFUSED') || err.message.includes('无法连接'))) {
78
+ console.log('\n=== Clash 状态 ===')
79
+ console.log(`状态: \x1b[31m未运行\x1b[0m`)
80
+ console.log('提示: 请使用 `clash start` 启动服务')
81
+ } else {
82
+ console.error(`获取状态失败: ${err.message}`)
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,28 @@
1
+ import { execSync } from 'child_process'
2
+ import ora from 'ora'
3
+ import * as sysproxy from '../sysproxy.js'
4
+ import * as tun from '../tun.js'
5
+
6
+ export async function stop() {
7
+ const spinner = ora('正在停止 Clash 服务...').start()
8
+ try {
9
+ // 停止前先关闭系统代理
10
+ await sysproxy.disableSystemProxy()
11
+
12
+ // 检查并关闭 TUN 模式
13
+ const tunEnabled = await tun.isTunEnabled()
14
+ if (tunEnabled) {
15
+ spinner.text = '正在关闭 TUN 模式...'
16
+ await tun.disableTun()
17
+ console.log('TUN 模式已关闭')
18
+ }
19
+
20
+ // 使用 pkill 匹配进程名包含 clash-meta 的进程
21
+ spinner.text = '正在停止 Clash 服务...'
22
+ execSync('pkill -f clash-meta')
23
+ spinner.succeed('Clash 服务已停止')
24
+ } catch (err) {
25
+ // pkill 如果没找到进程会抛出错误,这里捕获并提示
26
+ spinner.warn('未找到运行中的 Clash 服务,或已停止')
27
+ }
28
+ }