whistle.easy-proxy 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.
@@ -0,0 +1,222 @@
1
+ const { CONFIG_PATH } = require('../../config');
2
+ const {
3
+ readConfig,
4
+ writeConfig,
5
+ promptConfirm,
6
+ info,
7
+ ok,
8
+ err,
9
+ warn,
10
+ title,
11
+ sep,
12
+ c,
13
+ } = require('../helpers');
14
+
15
+ function registerIntercept(program) {
16
+ const interceptCmd = program.command('intercept').description('拦截规则管理');
17
+
18
+ interceptCmd
19
+ .command('status')
20
+ .description('查看拦截规则状态')
21
+ .action(() => {
22
+ const cfg = readConfig();
23
+ const enabled = cfg.interceptEnabled !== false;
24
+ const rules = cfg.interceptRules || {};
25
+ const hostCount = Object.keys(rules).length;
26
+ let pathCount = 0;
27
+ for (const h of Object.keys(rules)) {
28
+ pathCount += Object.keys(rules[h] || {}).length;
29
+ }
30
+ title('拦截规则状态');
31
+ sep();
32
+ info(`拦截开关: ${enabled ? `${c.green}✓ 已启用` : `${c.red}✗ 已关闭`}${c.reset}`);
33
+ info(`域名规则数: ${hostCount} 个域名, ${pathCount} 条路径规则`);
34
+ info(`配置文件: ${CONFIG_PATH}`);
35
+ sep();
36
+ info('使用 easy-proxy intercept list 查看所有规则');
37
+ info('使用 easy-proxy intercept search <关键词> 搜索规则');
38
+ });
39
+
40
+ interceptCmd
41
+ .command('enable')
42
+ .description('启用拦截规则')
43
+ .action(() => {
44
+ const cfg = readConfig();
45
+ cfg.interceptEnabled = true;
46
+ writeConfig(cfg);
47
+ ok('拦截规则已启用');
48
+ });
49
+
50
+ interceptCmd
51
+ .command('disable')
52
+ .description('关闭拦截规则')
53
+ .action(() => {
54
+ const cfg = readConfig();
55
+ cfg.interceptEnabled = false;
56
+ writeConfig(cfg);
57
+ ok('拦截规则已关闭');
58
+ });
59
+
60
+ interceptCmd
61
+ .command('list')
62
+ .description('列出所有拦截规则')
63
+ .option('-d, --domain <domain>', '只显示指定域名的规则')
64
+ .option('--json', '以 JSON 格式输出')
65
+ .action((opts) => {
66
+ const cfg = readConfig();
67
+ const rules = cfg.interceptRules || {};
68
+ const hosts = Object.keys(rules).sort();
69
+
70
+ if (opts.json) {
71
+ const result = {};
72
+ for (const host of hosts) {
73
+ if (opts.domain && host !== opts.domain && host.indexOf(opts.domain) < 0) continue;
74
+ result[host] = rules[host];
75
+ }
76
+ console.log(JSON.stringify(result, null, 2));
77
+ return;
78
+ }
79
+
80
+ title('拦截规则列表');
81
+ sep();
82
+ let count = 0;
83
+ for (const host of hosts) {
84
+ if (opts.domain && host !== opts.domain && host.indexOf(opts.domain) < 0) continue;
85
+ const pathRules = rules[host];
86
+ console.log(c.bold + c.blue + host + c.reset);
87
+ for (const pathPattern of Object.keys(pathRules)) {
88
+ const actions = pathRules[pathPattern];
89
+ const actionKeys = Object.keys(actions).filter(
90
+ (k) => ['desc', 'remark', 'test'].indexOf(k) < 0
91
+ );
92
+ const desc = actions.desc || '';
93
+ const actionStr =
94
+ actionKeys.length > 0
95
+ ? actionKeys
96
+ .map((k) => {
97
+ const v = actions[k];
98
+ if (typeof v === 'object') return `${k}:{...}`;
99
+ return `${k}:${v}`;
100
+ })
101
+ .join(', ')
102
+ : `${c.gray}(仅文档)${c.reset}`;
103
+ console.log(` ${c.gray}${pathPattern}${c.reset} → ${actionStr}`);
104
+ if (desc) console.log(` ${c.gray}${desc}${c.reset}`);
105
+ count++;
106
+ }
107
+ console.log('');
108
+ }
109
+ sep();
110
+ info(`共 ${hosts.length} 个域名, ${count} 条路径规则`);
111
+ });
112
+
113
+ interceptCmd
114
+ .command('search <keyword>')
115
+ .description('搜索拦截规则')
116
+ .action((keyword) => {
117
+ const cfg = readConfig();
118
+ const rules = cfg.interceptRules || {};
119
+ const hosts = Object.keys(rules).filter((h) => h.indexOf(keyword) >= 0);
120
+ title(`搜索: "${keyword}"`);
121
+ sep();
122
+ if (hosts.length === 0) {
123
+ info('未找到匹配的规则');
124
+ return;
125
+ }
126
+ for (const host of hosts) {
127
+ const pathRules = rules[host];
128
+ console.log(c.bold + c.blue + host + c.reset);
129
+ for (const pathPattern of Object.keys(pathRules)) {
130
+ const actions = pathRules[pathPattern];
131
+ const actionKeys = Object.keys(actions).filter(
132
+ (k) => ['desc', 'remark', 'test'].indexOf(k) < 0
133
+ );
134
+ const actionStr =
135
+ actionKeys.length > 0
136
+ ? actionKeys
137
+ .map((k) => {
138
+ const v = actions[k];
139
+ if (typeof v === 'object') return `${k}:{...}`;
140
+ return `${k}:${v}`;
141
+ })
142
+ .join(', ')
143
+ : `${c.gray}(仅文档)${c.reset}`;
144
+ console.log(` ${c.gray}${pathPattern}${c.reset} → ${actionStr}`);
145
+ }
146
+ console.log('');
147
+ }
148
+ sep();
149
+ info(`找到 ${hosts.length} 个匹配域名`);
150
+ });
151
+
152
+ interceptCmd
153
+ .command('add <host> <path> <actionJson>')
154
+ .description('添加拦截规则 (actionJson 为 JSON 字符串)')
155
+ .action((host, pathPattern, actionJson) => {
156
+ let actions;
157
+ try {
158
+ actions = JSON.parse(actionJson);
159
+ } catch (e) {
160
+ err(`actionJson 格式错误: ${e.message}`);
161
+ info('示例: easy-proxy intercept add example.com ".*" \'{"sni":"baidu.com"}\'');
162
+ return;
163
+ }
164
+ const cfg = readConfig();
165
+ if (!cfg.interceptRules || typeof cfg.interceptRules !== 'object') {
166
+ cfg.interceptRules = {};
167
+ }
168
+ if (!cfg.interceptRules[host]) {
169
+ cfg.interceptRules[host] = {};
170
+ }
171
+ cfg.interceptRules[host][pathPattern] = actions;
172
+ writeConfig(cfg);
173
+ ok(`已添加规则: ${host} ${pathPattern}`);
174
+ info(`动作: ${JSON.stringify(actions)}`);
175
+ });
176
+
177
+ interceptCmd
178
+ .command('remove <host> [path]')
179
+ .description('删除拦截规则 (不指定 path 则删除整个域名)')
180
+ .action((host, pathPattern) => {
181
+ const cfg = readConfig();
182
+ if (!cfg.interceptRules?.[host]) {
183
+ err(`未找到域名: ${host}`);
184
+ return;
185
+ }
186
+ if (pathPattern) {
187
+ if (!cfg.interceptRules[host][pathPattern]) {
188
+ err(`未找到路径规则: ${pathPattern}`);
189
+ return;
190
+ }
191
+ delete cfg.interceptRules[host][pathPattern];
192
+ if (Object.keys(cfg.interceptRules[host]).length === 0) {
193
+ delete cfg.interceptRules[host];
194
+ }
195
+ writeConfig(cfg);
196
+ ok(`已删除: ${host} ${pathPattern}`);
197
+ } else {
198
+ delete cfg.interceptRules[host];
199
+ writeConfig(cfg);
200
+ ok(`已删除域名: ${host}`);
201
+ }
202
+ });
203
+
204
+ interceptCmd
205
+ .command('reset')
206
+ .description('重置为默认拦截规则')
207
+ .action(async () => {
208
+ const confirmed = await promptConfirm('重置为默认拦截规则? 这将丢失自定义规则 (y/N) ');
209
+ if (!confirmed) {
210
+ info('已取消');
211
+ return;
212
+ }
213
+ const cfg = readConfig();
214
+ cfg.interceptRules = require('../../plugin/default-rules');
215
+ writeConfig(cfg);
216
+ ok('已重置为默认拦截规则');
217
+ });
218
+ }
219
+
220
+ module.exports = {
221
+ registerIntercept,
222
+ };
@@ -0,0 +1,32 @@
1
+ const { info, ok, err } = require('../helpers');
2
+
3
+ function registerNetwork(program) {
4
+ const netCmd = program.command('network').description('网络工具');
5
+
6
+ netCmd
7
+ .command('ip')
8
+ .description('显示本机 IP')
9
+ .action(() => {
10
+ const { getLocalIp } = require('../../network-utils');
11
+ info(`本机 IP: ${getLocalIp()}`);
12
+ });
13
+
14
+ netCmd
15
+ .command('delay')
16
+ .description('检测网络延迟')
17
+ .option('-p, --port <port>', '通过代理端口检测(不指定则直连)')
18
+ .action(async (opts) => {
19
+ const { checkDelay } = require('../../network-utils');
20
+ const port = opts.port ? parseInt(opts.port, 10) : undefined;
21
+ try {
22
+ const ms = await checkDelay(port);
23
+ ok(`延迟: ${ms}ms${port ? `(通过端口 ${port})` : '(直连)'}`);
24
+ } catch (e) {
25
+ err(`检测失败: ${e.message}`);
26
+ }
27
+ });
28
+ }
29
+
30
+ module.exports = {
31
+ registerNetwork,
32
+ };
@@ -0,0 +1,279 @@
1
+ const { localW2Path, getWhistleStatus, info, ok, warn, err, title, sep, c, editInEditor } = require('../helpers');
2
+
3
+ function enableSystemProxy() {
4
+ const { execSync } = require('node:child_process');
5
+ const proxyConfig = require('../proxy-config');
6
+ const bypass = proxyConfig.getEffectiveBypass();
7
+ const bypassArg = bypass.join(', ');
8
+ info(`开启系统代理(bypass: ${bypass.length} 个域名)...`);
9
+ execSync(`"${localW2Path}" proxy -x "${bypassArg}"`, { stdio: 'inherit' });
10
+ ok('系统代理已开启');
11
+ }
12
+
13
+ function disableSystemProxy() {
14
+ const { execSync } = require('node:child_process');
15
+ execSync(`"${localW2Path}" proxy 0`, { stdio: 'inherit' });
16
+ ok('系统代理已关闭');
17
+ }
18
+
19
+ // ---- 尝试同步上游代理规则到 whistle(静默失败) ----
20
+ async function trySyncUpstream(upstream, proxies) {
21
+ try {
22
+ const ruleApi = require('../rule-api');
23
+ await upstream.syncToWhistle(ruleApi, proxies);
24
+ info(`已同步上游代理规则到 whistle 规则集 "${upstream.RULESET_NAME}"`);
25
+ } catch (_e) {
26
+ // whistle 未运行时静默跳过,下次 start 后可手动 sync
27
+ info(
28
+ c.gray +
29
+ '(whistle 未运行,规则未同步 — 启动后可用 easy-proxy proxy upstream sync 同步)' +
30
+ c.reset
31
+ );
32
+ }
33
+ }
34
+
35
+ function registerProxy(program) {
36
+ const proxyCmd = program.command('proxy').description('系统代理管理');
37
+
38
+ // proxy on — 开启系统代理(调用 w2 proxy,自动带上 bypass 列表)
39
+ proxyCmd
40
+ .command('on')
41
+ .description('开启系统代理(自动带上 bypass 域名)')
42
+ .action(async () => {
43
+ try {
44
+ enableSystemProxy();
45
+ } catch (e) {
46
+ err(`开启系统代理失败: ${e.message}`);
47
+ info('请确保 whistle 已运行: easy-proxy start --init');
48
+ process.exit(1);
49
+ }
50
+ });
51
+
52
+ // proxy off — 关闭系统代理
53
+ proxyCmd
54
+ .command('off')
55
+ .description('关闭系统代理')
56
+ .action(() => {
57
+ try {
58
+ disableSystemProxy();
59
+ } catch (e) {
60
+ err(`关闭系统代理失败: ${e.message}`);
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ // proxy status — 检查系统代理是否指向 whistle
66
+ proxyCmd
67
+ .command('status')
68
+ .description('检查系统代理状态')
69
+ .action(async () => {
70
+ const { getLocalIp } = require('../../network-utils');
71
+ info(`本机 IP: ${getLocalIp()}`);
72
+ // 检查 whistle 是否运行
73
+ const ws = getWhistleStatus();
74
+ if (!ws.running) {
75
+ warn('whistle 代理未运行');
76
+ info('运行 easy-proxy start --init 启动代理');
77
+ return;
78
+ }
79
+ if (ws.port) info(`whistle 端口: ${ws.port}`);
80
+ // 检查系统代理
81
+ const { checkSystemProxy } = require('../../proxy-check');
82
+ const proxyOn = await checkSystemProxy(ws.port);
83
+ if (proxyOn) {
84
+ ok('系统代理: 已开启(指向 whistle)');
85
+ } else {
86
+ warn('系统代理: 未指向 whistle');
87
+ info('运行 easy-proxy proxy on 开启系统代理');
88
+ }
89
+ });
90
+
91
+ // ---- proxy upstream ----
92
+ const upstreamCmd = proxyCmd.command('upstream').description('上游代理管理');
93
+
94
+ upstreamCmd
95
+ .command('edit')
96
+ .description('编辑上游代理配置(打开 $EDITOR)')
97
+ .action(async () => {
98
+ const upstream = require('../upstream-proxy');
99
+ const { parse, buildEditContent } = require('../upstream-editor');
100
+ const oldProxies = upstream.loadProxies();
101
+ let content = buildEditContent(oldProxies);
102
+
103
+ let newProxies;
104
+ while (true) {
105
+ let editedText;
106
+ try {
107
+ editedText = editInEditor(content, 'upstream');
108
+ } catch (_e) {
109
+ // 编辑器非零退出码(如 vi :q!)视为用户取消
110
+ info('已取消');
111
+ return;
112
+ }
113
+ try {
114
+ newProxies = parse(editedText);
115
+ break;
116
+ } catch (e) {
117
+ err(e.message);
118
+ warn('解析失败,请修正后重新保存...');
119
+ content = editedText;
120
+ }
121
+ }
122
+
123
+ if (JSON.stringify(oldProxies) === JSON.stringify(newProxies)) {
124
+ info('无改动');
125
+ return;
126
+ }
127
+
128
+ upstream.saveProxies(newProxies);
129
+ await trySyncUpstream(upstream, newProxies);
130
+ ok('上游代理配置已更新');
131
+ });
132
+
133
+ upstreamCmd
134
+ .command('status')
135
+ .description('查看上游代理配置')
136
+ .action(() => {
137
+ const upstream = require('../upstream-proxy');
138
+ const proxies = upstream.loadProxies();
139
+ title('上游代理配置');
140
+ sep();
141
+ if (proxies.length === 0) {
142
+ info('未配置代理');
143
+ } else {
144
+ proxies.forEach((p) => {
145
+ info(`${p.name}: ${p.url}`);
146
+ if (p.domains.length > 0) {
147
+ p.domains.forEach((d) => {
148
+ info(` ${d}`);
149
+ });
150
+ } else {
151
+ info(' (无域名)');
152
+ }
153
+ });
154
+ }
155
+ info('默认: 直连');
156
+ sep();
157
+ info(`规则集名: ${upstream.RULESET_NAME}`);
158
+ });
159
+
160
+ // proxy upstream sync — 手动同步规则到 whistle
161
+ upstreamCmd
162
+ .command('sync')
163
+ .description('手动同步上游代理规则到 whistle')
164
+ .action(async () => {
165
+ const upstream = require('../upstream-proxy');
166
+ const proxies = upstream.loadProxies();
167
+ try {
168
+ const ruleApi = require('../rule-api');
169
+ await upstream.syncToWhistle(ruleApi, proxies);
170
+ ok(`已同步 ${proxies.length} 个上游代理规则到 whistle`);
171
+ } catch (e) {
172
+ err(`同步失败: ${e.message}`);
173
+ info('请确保 whistle 已运行: easy-proxy start --init');
174
+ process.exit(1);
175
+ }
176
+ });
177
+
178
+ // ---- proxy bypass ----
179
+ const bypassCmd = proxyCmd.command('bypass').description('管理跳过代理的域名');
180
+
181
+ bypassCmd
182
+ .command('show')
183
+ .description('查看当前跳过列表')
184
+ .action(() => {
185
+ const proxyConfig = require('../proxy-config');
186
+ const config = proxyConfig.loadConfig();
187
+ const domains = config.bypassDomains || proxyConfig.getDefaultBypass();
188
+ title('跳过代理的域名');
189
+ sep();
190
+ if (domains.length === 0) {
191
+ info('(空)');
192
+ } else {
193
+ domains.forEach((d) => {
194
+ info(` ${d}`);
195
+ });
196
+ }
197
+ if (!config.bypassDomains) {
198
+ info(`${c.gray}(使用默认列表)${c.reset}`);
199
+ }
200
+ sep();
201
+ });
202
+
203
+ bypassCmd
204
+ .command('set <domains>')
205
+ .description('设置跳过列表(逗号分隔,如: 127.0.0.1,localhost,*.local)')
206
+ .action((domains) => {
207
+ const proxyConfig = require('../proxy-config');
208
+ const list = domains
209
+ .split(',')
210
+ .map((d) => d.trim())
211
+ .filter(Boolean);
212
+ if (list.length === 0) {
213
+ err('请提供至少一个域名');
214
+ return;
215
+ }
216
+ proxyConfig.saveConfig({ bypassDomains: list });
217
+ ok(`已设置跳过列表: ${list.join(', ')}`);
218
+ info('重新开启系统代理后生效: easy-proxy proxy on');
219
+ });
220
+
221
+ bypassCmd
222
+ .command('add <domain>')
223
+ .description('添加一个跳过域名')
224
+ .action((domain) => {
225
+ const proxyConfig = require('../proxy-config');
226
+ const config = proxyConfig.loadConfig();
227
+ const list = config.bypassDomains
228
+ ? config.bypassDomains.slice()
229
+ : proxyConfig.getDefaultBypass().slice();
230
+ if (list.indexOf(domain) >= 0) {
231
+ warn(`已存在: ${domain}`);
232
+ return;
233
+ }
234
+ list.push(domain);
235
+ proxyConfig.saveConfig({ bypassDomains: list });
236
+ ok(`已添加: ${domain}`);
237
+ info('重新开启系统代理后生效: easy-proxy proxy on');
238
+ });
239
+
240
+ bypassCmd
241
+ .command('remove <domain>')
242
+ .description('移除一个跳过域名')
243
+ .action((domain) => {
244
+ const proxyConfig = require('../proxy-config');
245
+ const config = proxyConfig.loadConfig();
246
+ if (!config.bypassDomains) {
247
+ warn('未自定义跳过列表,当前使用默认列表');
248
+ return;
249
+ }
250
+ const index = config.bypassDomains.indexOf(domain);
251
+ if (index < 0) {
252
+ warn(`未找到: ${domain}`);
253
+ return;
254
+ }
255
+ config.bypassDomains.splice(index, 1);
256
+ proxyConfig.saveConfig({ bypassDomains: config.bypassDomains });
257
+ ok(`已移除: ${domain}`);
258
+ info('重新开启系统代理后生效: easy-proxy proxy on');
259
+ });
260
+
261
+ bypassCmd
262
+ .command('reset')
263
+ .description('重置为默认跳过列表')
264
+ .action(() => {
265
+ const proxyConfig = require('../proxy-config');
266
+ proxyConfig.saveConfig({ bypassDomains: null });
267
+ ok('已重置为默认列表');
268
+ proxyConfig.getDefaultBypass().forEach((d) => {
269
+ info(` ${d}`);
270
+ });
271
+ info('重新开启系统代理后生效: easy-proxy proxy on');
272
+ });
273
+ }
274
+
275
+ module.exports = {
276
+ registerProxy,
277
+ enableSystemProxy,
278
+ disableSystemProxy,
279
+ };