clash-switcher 0.0.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/README.md +220 -0
- package/dist/api.d.ts +30 -0
- package/dist/api.js +43 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +27 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +51 -0
- package/dist/commands/profile.d.ts +16 -0
- package/dist/commands/profile.js +240 -0
- package/dist/commands/switch.d.ts +3 -0
- package/dist/commands/switch.js +48 -0
- package/dist/commands/test.d.ts +3 -0
- package/dist/commands/test.js +49 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +84 -0
- package/dist/core.d.ts +184 -0
- package/dist/core.js +677 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +243 -0
- package/dist/lib.d.ts +6 -0
- package/dist/lib.js +24 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +2 -0
- package/dist/utils/group-resolver.d.ts +8 -0
- package/dist/utils/group-resolver.js +38 -0
- package/dist/utils/proxy-resolver.d.ts +7 -0
- package/dist/utils/proxy-resolver.js +29 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# clash-switcher
|
|
2
|
+
|
|
3
|
+
Clash Verge 代理切换工具,提供 API 库和命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install clash-switcher
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## API 使用
|
|
12
|
+
|
|
13
|
+
### 初始化
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { ClashSwitcher } from 'clash-switcher';
|
|
17
|
+
|
|
18
|
+
const switcher = new ClashSwitcher({
|
|
19
|
+
host: '127.0.0.1', // 可选,默认 127.0.0.1
|
|
20
|
+
port: 9097, // 可选,默认 9097
|
|
21
|
+
secret: '', // 可选,API 密钥
|
|
22
|
+
testUrl: 'http://www.gstatic.com/generate_204', // 可选
|
|
23
|
+
timeout: 5000, // 可选,超时时间(ms)
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### 配置管理
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// 获取当前配置
|
|
31
|
+
const config = switcher.getConfig();
|
|
32
|
+
|
|
33
|
+
// 更新配置
|
|
34
|
+
switcher.setConfig({ port: 9090, timeout: 3000 });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 模式管理
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// 获取当前模式
|
|
41
|
+
const mode = await switcher.getMode(); // 'rule' | 'global' | 'direct'
|
|
42
|
+
|
|
43
|
+
// 设置模式
|
|
44
|
+
await switcher.setMode('rule');
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 代理组
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// 获取所有代理组
|
|
51
|
+
const groups = await switcher.getGroups();
|
|
52
|
+
|
|
53
|
+
// 获取单个代理组
|
|
54
|
+
const group = await switcher.getGroup(); // 主代理组
|
|
55
|
+
const group = await switcher.getGroup(undefined, 0); // 按索引
|
|
56
|
+
const group = await switcher.getGroup(undefined, '选择'); // 模糊匹配
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 节点操作
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
// 获取节点列表
|
|
63
|
+
const nodes = await switcher.getNodes(); // 主代理组的节点
|
|
64
|
+
const nodes = await switcher.getNodes(0); // 按索引
|
|
65
|
+
const nodes = await switcher.getNodes('选择'); // 模糊匹配
|
|
66
|
+
|
|
67
|
+
// 切换节点
|
|
68
|
+
await switcher.setNode(0, '香港'); // 按索引指定组
|
|
69
|
+
await switcher.setNode('选择', '香港'); // 模糊匹配组名
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 延迟测试
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// 测试单个节点
|
|
76
|
+
const result = await switcher.testNode('香港');
|
|
77
|
+
// { name: '🇭🇰 香港 01', delay: 120 }
|
|
78
|
+
|
|
79
|
+
// 测试代理组所有节点
|
|
80
|
+
const results = await switcher.testNodes(); // 主代理组
|
|
81
|
+
const results = await switcher.testNodes(0); // 按索引
|
|
82
|
+
const results = await switcher.testNodes('选择'); // 模糊匹配
|
|
83
|
+
|
|
84
|
+
// 测试指定节点列表
|
|
85
|
+
const nodes = await switcher.getNodes();
|
|
86
|
+
const results = await switcher.testNodes(nodes.filter(n => n !== 'DIRECT'));
|
|
87
|
+
|
|
88
|
+
// 自动选择最快节点
|
|
89
|
+
const best = await switcher.autoNode(); // 主代理组
|
|
90
|
+
const best = await switcher.autoNode(0); // 按索引
|
|
91
|
+
const best = await switcher.autoNode('选择'); // 模糊匹配
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 订阅管理
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// 获取所有订阅
|
|
98
|
+
const subs = switcher.getSubs();
|
|
99
|
+
|
|
100
|
+
// 获取当前订阅
|
|
101
|
+
const current = switcher.getSub();
|
|
102
|
+
|
|
103
|
+
// 按索引或名称获取
|
|
104
|
+
const sub = switcher.getSub(0);
|
|
105
|
+
const sub = switcher.getSub('机场名');
|
|
106
|
+
|
|
107
|
+
// 切换订阅(默认重启)
|
|
108
|
+
await switcher.setSub(0);
|
|
109
|
+
await switcher.setSub('机场名');
|
|
110
|
+
|
|
111
|
+
// 切换但不重启
|
|
112
|
+
await switcher.setSub('机场名', false);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 工具方法
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// 等待 API 就绪
|
|
119
|
+
await switcher.waitReady();
|
|
120
|
+
|
|
121
|
+
// 重启 Clash Verge
|
|
122
|
+
await switcher.restartClashVerge();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 命令行
|
|
126
|
+
|
|
127
|
+
全局安装后可使用 `clash-switcher` 命令。
|
|
128
|
+
|
|
129
|
+
### 模式
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 查看当前模式
|
|
133
|
+
clash-switcher mode
|
|
134
|
+
|
|
135
|
+
# 设置模式
|
|
136
|
+
clash-switcher mode rule
|
|
137
|
+
clash-switcher m global
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 代理组
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 列出所有代理组
|
|
144
|
+
clash-switcher groups
|
|
145
|
+
clash-switcher g
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 节点
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 列出节点(主代理组)
|
|
152
|
+
clash-switcher nodes
|
|
153
|
+
clash-switcher n
|
|
154
|
+
|
|
155
|
+
# 按索引或名称
|
|
156
|
+
clash-switcher n 0
|
|
157
|
+
clash-switcher n 选择
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 切换节点
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 切换节点(组 + 节点)
|
|
164
|
+
clash-switcher set 0 香港
|
|
165
|
+
clash-switcher s 选择 日本
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 测试延迟
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# 测试主代理组
|
|
172
|
+
clash-switcher test
|
|
173
|
+
clash-switcher t
|
|
174
|
+
|
|
175
|
+
# 按索引或名称
|
|
176
|
+
clash-switcher t 0
|
|
177
|
+
clash-switcher t 选择
|
|
178
|
+
|
|
179
|
+
# 自定义测试参数
|
|
180
|
+
clash-switcher t -u http://example.com -t 3000
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 自动选择
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# 自动选择最快节点
|
|
187
|
+
clash-switcher auto
|
|
188
|
+
clash-switcher a
|
|
189
|
+
|
|
190
|
+
# 指定代理组
|
|
191
|
+
clash-switcher a 0
|
|
192
|
+
clash-switcher a 选择
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 订阅
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
# 列出订阅
|
|
199
|
+
clash-switcher subs
|
|
200
|
+
|
|
201
|
+
# 切换订阅
|
|
202
|
+
clash-switcher sub 0
|
|
203
|
+
clash-switcher sub 机场名
|
|
204
|
+
|
|
205
|
+
# 切换但不重启
|
|
206
|
+
clash-switcher sub 机场名 --no-restart
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 配置
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# 查看配置
|
|
213
|
+
clash-switcher config
|
|
214
|
+
clash-switcher c
|
|
215
|
+
|
|
216
|
+
# 设置配置
|
|
217
|
+
clash-switcher c host 192.168.1.100
|
|
218
|
+
clash-switcher c port 9090
|
|
219
|
+
clash-switcher c timeout 3000
|
|
220
|
+
```
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ProxyGroup {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
now: string;
|
|
5
|
+
all: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface ProxyInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
history: {
|
|
11
|
+
delay: number;
|
|
12
|
+
}[];
|
|
13
|
+
}
|
|
14
|
+
export interface ClashConfig {
|
|
15
|
+
port: number;
|
|
16
|
+
'socks-port': number;
|
|
17
|
+
'mixed-port': number;
|
|
18
|
+
mode: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class ClashAPI {
|
|
21
|
+
private client;
|
|
22
|
+
private baseURL;
|
|
23
|
+
constructor(host?: string, port?: number, secret?: string);
|
|
24
|
+
getConfig(): Promise<ClashConfig>;
|
|
25
|
+
getProxies(): Promise<Record<string, ProxyInfo | ProxyGroup>>;
|
|
26
|
+
getProxyGroups(): Promise<ProxyGroup[]>;
|
|
27
|
+
switchProxy(group: string, proxy: string): Promise<void>;
|
|
28
|
+
testDelay(proxy: string, url?: string, timeout?: number): Promise<number>;
|
|
29
|
+
testGroupDelays(group: string, url?: string, timeout?: number): Promise<Record<string, number>>;
|
|
30
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ClashAPI = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class ClashAPI {
|
|
9
|
+
constructor(host = '127.0.0.1', port = 9097, secret) {
|
|
10
|
+
this.baseURL = `http://${host}:${port}`;
|
|
11
|
+
this.client = axios_1.default.create({
|
|
12
|
+
baseURL: this.baseURL,
|
|
13
|
+
timeout: 10000,
|
|
14
|
+
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async getConfig() {
|
|
18
|
+
const { data } = await this.client.get('/configs');
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
async getProxies() {
|
|
22
|
+
const { data } = await this.client.get('/proxies');
|
|
23
|
+
return data.proxies;
|
|
24
|
+
}
|
|
25
|
+
async getProxyGroups() {
|
|
26
|
+
const proxies = await this.getProxies();
|
|
27
|
+
return Object.values(proxies).filter((p) => 'all' in p && Array.isArray(p.all));
|
|
28
|
+
}
|
|
29
|
+
async switchProxy(group, proxy) {
|
|
30
|
+
await this.client.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy });
|
|
31
|
+
}
|
|
32
|
+
async testDelay(proxy, url, timeout) {
|
|
33
|
+
const testUrl = url || 'http://www.gstatic.com/generate_204';
|
|
34
|
+
const { data } = await this.client.get(`/proxies/${encodeURIComponent(proxy)}/delay`, { params: { url: testUrl, timeout: timeout || 5000 } });
|
|
35
|
+
return data.delay;
|
|
36
|
+
}
|
|
37
|
+
async testGroupDelays(group, url, timeout) {
|
|
38
|
+
const testUrl = url || 'http://www.gstatic.com/generate_204';
|
|
39
|
+
const { data } = await this.client.get(`/group/${encodeURIComponent(group)}/delay`, { params: { url: testUrl, timeout: timeout || 5000 } });
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.ClashAPI = ClashAPI;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.showConfig = showConfig;
|
|
7
|
+
exports.setConfig = setConfig;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
function showConfig() {
|
|
11
|
+
const config = (0, config_1.loadConfig)();
|
|
12
|
+
console.log(chalk_1.default.bold('\n当前配置:\n'));
|
|
13
|
+
console.log(` 配置文件: ${chalk_1.default.gray((0, config_1.getConfigPath)())}`);
|
|
14
|
+
console.log(` 主机 (host): ${chalk_1.default.cyan(config.host)}`);
|
|
15
|
+
console.log(` 端口 (port): ${chalk_1.default.cyan(config.port)}`);
|
|
16
|
+
console.log(` 密钥 (secret): ${chalk_1.default.cyan(config.secret || '(未设置)')}`);
|
|
17
|
+
console.log(` 测试URL (testUrl): ${chalk_1.default.cyan(config.testUrl)}`);
|
|
18
|
+
console.log(` 超时 (timeout): ${chalk_1.default.cyan(config.timeout + 'ms')}`);
|
|
19
|
+
console.log(` Verge配置目录 (vergeConfigDir): ${chalk_1.default.cyan(config.vergeConfigDir)}`);
|
|
20
|
+
console.log();
|
|
21
|
+
}
|
|
22
|
+
function setConfig(key, value) {
|
|
23
|
+
const numericKeys = ['port', 'timeout'];
|
|
24
|
+
const parsedValue = numericKeys.includes(key) ? parseInt(value, 10) : value;
|
|
25
|
+
(0, config_1.saveConfig)({ [key]: parsedValue });
|
|
26
|
+
console.log(chalk_1.default.green(`已设置 ${key} = ${value}`));
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.listGroups = listGroups;
|
|
7
|
+
exports.listProxies = listProxies;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
async function listGroups(api) {
|
|
11
|
+
const spinner = (0, ora_1.default)('获取代理组...').start();
|
|
12
|
+
try {
|
|
13
|
+
const groups = await api.getProxyGroups();
|
|
14
|
+
spinner.stop();
|
|
15
|
+
console.log(chalk_1.default.bold('\n代理组列表:\n'));
|
|
16
|
+
for (const group of groups) {
|
|
17
|
+
console.log(chalk_1.default.cyan(` ${group.name}`));
|
|
18
|
+
console.log(chalk_1.default.gray(` 类型: ${group.type}`));
|
|
19
|
+
console.log(chalk_1.default.green(` 当前: ${group.now}`));
|
|
20
|
+
console.log(chalk_1.default.gray(` 节点数: ${group.all.length}`));
|
|
21
|
+
console.log();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
spinner.fail('获取代理组失败');
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function listProxies(api, groupName) {
|
|
30
|
+
const spinner = (0, ora_1.default)('获取节点列表...').start();
|
|
31
|
+
try {
|
|
32
|
+
const groups = await api.getProxyGroups();
|
|
33
|
+
const group = groups.find((g) => g.name === groupName);
|
|
34
|
+
if (!group) {
|
|
35
|
+
spinner.fail(`未找到代理组: ${groupName}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
spinner.stop();
|
|
39
|
+
console.log(chalk_1.default.bold(`\n${groupName} 节点列表:\n`));
|
|
40
|
+
for (const proxy of group.all) {
|
|
41
|
+
const isCurrent = proxy === group.now;
|
|
42
|
+
const prefix = isCurrent ? chalk_1.default.green('* ') : ' ';
|
|
43
|
+
console.log(`${prefix}${isCurrent ? chalk_1.default.green(proxy) : proxy}`);
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
spinner.fail('获取节点列表失败');
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface Profile {
|
|
2
|
+
uid: string;
|
|
3
|
+
type: 'remote' | 'local' | 'merge' | 'script';
|
|
4
|
+
name?: string;
|
|
5
|
+
desc?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
updated?: number;
|
|
8
|
+
}
|
|
9
|
+
interface ProfilesConfig {
|
|
10
|
+
current?: string[];
|
|
11
|
+
items?: Profile[];
|
|
12
|
+
}
|
|
13
|
+
export declare function loadProfiles(): ProfilesConfig;
|
|
14
|
+
export declare function listProfiles(): Promise<void>;
|
|
15
|
+
export declare function switchProfile(nameOrUid: string, restart?: boolean): Promise<void>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.loadProfiles = loadProfiles;
|
|
40
|
+
exports.listProfiles = listProfiles;
|
|
41
|
+
exports.switchProfile = switchProfile;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const util_1 = require("util");
|
|
47
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
48
|
+
const ora_1 = __importDefault(require("ora"));
|
|
49
|
+
const config_1 = require("../config");
|
|
50
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
51
|
+
function getProfilesPath() {
|
|
52
|
+
const config = (0, config_1.loadConfig)();
|
|
53
|
+
return path.join(config.vergeConfigDir, 'profiles.yaml');
|
|
54
|
+
}
|
|
55
|
+
function parseYamlSimple(content) {
|
|
56
|
+
const result = { items: [] };
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
let inItems = false;
|
|
59
|
+
let currentItem = null;
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
// 解析 current 字段(单个值或数组)
|
|
62
|
+
if (line.startsWith('current:')) {
|
|
63
|
+
const value = line.slice(8).trim();
|
|
64
|
+
if (value) {
|
|
65
|
+
result.current = [value];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
result.current = [];
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// current 数组项
|
|
73
|
+
if (result.current && line.startsWith('- ') && !inItems) {
|
|
74
|
+
result.current.push(line.slice(2).trim());
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// items 开始
|
|
78
|
+
if (line.trim() === 'items:') {
|
|
79
|
+
inItems = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (inItems) {
|
|
83
|
+
// 新的 item 开始
|
|
84
|
+
if (line.startsWith('- ')) {
|
|
85
|
+
if (currentItem && currentItem.uid) {
|
|
86
|
+
result.items?.push(currentItem);
|
|
87
|
+
}
|
|
88
|
+
currentItem = {};
|
|
89
|
+
const match = line.slice(2).match(/^(\w+):\s*(.*)$/);
|
|
90
|
+
if (match) {
|
|
91
|
+
const value = match[2].trim();
|
|
92
|
+
if (value && value !== 'null') {
|
|
93
|
+
currentItem[match[1]] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// item 的属性(顶级属性,2空格缩进)
|
|
98
|
+
else if (line.startsWith(' ') && !line.startsWith(' ') && currentItem) {
|
|
99
|
+
const match = line.trim().match(/^(\w+):\s*(.*)$/);
|
|
100
|
+
if (match) {
|
|
101
|
+
const key = match[1];
|
|
102
|
+
let value = match[2].trim();
|
|
103
|
+
if (value === 'null' || value === '') {
|
|
104
|
+
value = undefined;
|
|
105
|
+
}
|
|
106
|
+
else if (value === 'true') {
|
|
107
|
+
value = true;
|
|
108
|
+
}
|
|
109
|
+
else if (value === 'false') {
|
|
110
|
+
value = false;
|
|
111
|
+
}
|
|
112
|
+
else if (/^\d+$/.test(value)) {
|
|
113
|
+
value = parseInt(value);
|
|
114
|
+
}
|
|
115
|
+
if (value !== undefined) {
|
|
116
|
+
currentItem[key] = value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 添加最后一个 item
|
|
123
|
+
if (currentItem && currentItem.uid) {
|
|
124
|
+
result.items?.push(currentItem);
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function loadProfiles() {
|
|
129
|
+
const profilesPath = getProfilesPath();
|
|
130
|
+
if (!fs.existsSync(profilesPath)) {
|
|
131
|
+
throw new Error(`未找到 Clash Verge 配置文件: ${profilesPath}`);
|
|
132
|
+
}
|
|
133
|
+
const content = fs.readFileSync(profilesPath, 'utf-8');
|
|
134
|
+
return parseYamlSimple(content);
|
|
135
|
+
}
|
|
136
|
+
async function listProfiles() {
|
|
137
|
+
const spinner = (0, ora_1.default)('读取订阅列表...').start();
|
|
138
|
+
try {
|
|
139
|
+
const config = loadProfiles();
|
|
140
|
+
spinner.stop();
|
|
141
|
+
if (!config.items || config.items.length === 0) {
|
|
142
|
+
console.log(chalk_1.default.yellow('\n没有找到任何订阅配置'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log(chalk_1.default.bold('\n订阅列表:\n'));
|
|
146
|
+
// 只显示 remote 类型的订阅
|
|
147
|
+
const remoteProfiles = config.items.filter(item => item.type === 'remote');
|
|
148
|
+
if (remoteProfiles.length === 0) {
|
|
149
|
+
console.log(chalk_1.default.yellow('没有找到远程订阅'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const item of remoteProfiles) {
|
|
153
|
+
const isCurrent = config.current?.includes(item.uid);
|
|
154
|
+
const prefix = isCurrent ? chalk_1.default.green('* ') : ' ';
|
|
155
|
+
const name = item.name || item.uid;
|
|
156
|
+
console.log(`${prefix}${isCurrent ? chalk_1.default.green(name) : name}`);
|
|
157
|
+
console.log(chalk_1.default.gray(` UID: ${item.uid}`));
|
|
158
|
+
console.log(chalk_1.default.gray(` 类型: ${item.type}`));
|
|
159
|
+
if (item.desc) {
|
|
160
|
+
console.log(chalk_1.default.gray(` 描述: ${item.desc}`));
|
|
161
|
+
}
|
|
162
|
+
if (item.url) {
|
|
163
|
+
console.log(chalk_1.default.gray(` URL: ${item.url.slice(0, 50)}...`));
|
|
164
|
+
}
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
spinner.fail('读取订阅失败');
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function restartClashVerge() {
|
|
174
|
+
const platform = os.platform();
|
|
175
|
+
if (platform === 'win32') {
|
|
176
|
+
// Windows: 先获取进程路径,再关闭并重启
|
|
177
|
+
const { stdout } = await execAsync('powershell -Command "Get-Process clash-verge -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Path"');
|
|
178
|
+
const appPath = stdout.trim();
|
|
179
|
+
if (!appPath) {
|
|
180
|
+
throw new Error('未找到运行中的 Clash Verge 进程');
|
|
181
|
+
}
|
|
182
|
+
await execAsync('taskkill /IM "clash-verge.exe" /F').catch(() => { });
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
184
|
+
(0, child_process_1.exec)(`start "" "${appPath}"`);
|
|
185
|
+
}
|
|
186
|
+
else if (platform === 'darwin') {
|
|
187
|
+
// macOS
|
|
188
|
+
await execAsync('pkill -x "Clash Verge"').catch(() => { });
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
190
|
+
(0, child_process_1.exec)('open -a "Clash Verge"');
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// Linux
|
|
194
|
+
await execAsync('pkill -x clash-verge').catch(() => { });
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
196
|
+
(0, child_process_1.exec)('clash-verge &');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function switchProfile(nameOrUid, restart = false) {
|
|
200
|
+
const spinner = (0, ora_1.default)('切换订阅...').start();
|
|
201
|
+
try {
|
|
202
|
+
const config = loadProfiles();
|
|
203
|
+
const profilesPath = getProfilesPath();
|
|
204
|
+
// 只在 remote 类型中查找,支持模糊匹配
|
|
205
|
+
const remoteProfiles = config.items?.filter(p => p.type === 'remote') || [];
|
|
206
|
+
const keyword = nameOrUid.toLowerCase();
|
|
207
|
+
const profile = remoteProfiles.find(p => p.uid === nameOrUid ||
|
|
208
|
+
p.name?.toLowerCase().includes(keyword));
|
|
209
|
+
if (!profile) {
|
|
210
|
+
spinner.fail(`未找到订阅: ${nameOrUid}`);
|
|
211
|
+
console.log(chalk_1.default.gray('\n可用的订阅:'));
|
|
212
|
+
remoteProfiles.forEach(p => {
|
|
213
|
+
console.log(chalk_1.default.gray(` - ${p.name || p.uid} (${p.uid})`));
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
// 读取原始文件内容并替换 current 字段
|
|
218
|
+
let content = fs.readFileSync(profilesPath, 'utf-8');
|
|
219
|
+
content = content.replace(/^current:.*$/m, `current: ${profile.uid}`);
|
|
220
|
+
fs.writeFileSync(profilesPath, content, 'utf-8');
|
|
221
|
+
spinner.succeed(chalk_1.default.green(`已切换到: ${profile.name || profile.uid}`));
|
|
222
|
+
if (restart) {
|
|
223
|
+
const restartSpinner = (0, ora_1.default)('重启 Clash Verge...').start();
|
|
224
|
+
try {
|
|
225
|
+
await restartClashVerge();
|
|
226
|
+
restartSpinner.succeed(chalk_1.default.green('Clash Verge 已重启'));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
restartSpinner.fail('重启失败,请手动重启 Clash Verge');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.log(chalk_1.default.yellow('\n提示: 使用 -r 参数可自动重启 Clash Verge'));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
spinner.fail('切换订阅失败');
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.switchProxy = switchProxy;
|
|
7
|
+
exports.testAndSwitch = testAndSwitch;
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
async function switchProxy(api, groupName, proxyName) {
|
|
11
|
+
const spinner = (0, ora_1.default)(`切换到 ${proxyName}...`).start();
|
|
12
|
+
try {
|
|
13
|
+
await api.switchProxy(groupName, proxyName);
|
|
14
|
+
spinner.succeed(chalk_1.default.green(`已切换到: ${proxyName}`));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
spinner.fail('切换失败');
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function testAndSwitch(api, groupName, testUrl, timeout) {
|
|
22
|
+
const spinner = (0, ora_1.default)('测试所有节点延迟...').start();
|
|
23
|
+
try {
|
|
24
|
+
const delays = await api.testGroupDelays(groupName, testUrl, timeout);
|
|
25
|
+
spinner.stop();
|
|
26
|
+
const sorted = Object.entries(delays)
|
|
27
|
+
.filter(([_, delay]) => delay > 0)
|
|
28
|
+
.sort((a, b) => a[1] - b[1]);
|
|
29
|
+
if (sorted.length === 0) {
|
|
30
|
+
console.log(chalk_1.default.red('没有可用的节点'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(chalk_1.default.bold('\n延迟测试结果:\n'));
|
|
34
|
+
sorted.slice(0, 10).forEach(([name, delay], index) => {
|
|
35
|
+
const color = delay < 200 ? chalk_1.default.green : delay < 500 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
36
|
+
console.log(` ${index + 1}. ${name}: ${color(delay + 'ms')}`);
|
|
37
|
+
});
|
|
38
|
+
const [bestProxy, bestDelay] = sorted[0];
|
|
39
|
+
console.log(chalk_1.default.bold(`\n最快节点: ${chalk_1.default.green(bestProxy)} (${bestDelay}ms)`));
|
|
40
|
+
const switchSpinner = (0, ora_1.default)(`切换到 ${bestProxy}...`).start();
|
|
41
|
+
await api.switchProxy(groupName, bestProxy);
|
|
42
|
+
switchSpinner.succeed(chalk_1.default.green(`已切换到最快节点: ${bestProxy}`));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
spinner.fail('测试失败');
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|