pac-proxy-cli 1.1.13 → 1.2.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 +15 -0
- package/dist/assets/index-CFoGlW0m.css +1 -0
- package/dist/assets/index-DN6Yf8E7.js +28 -0
- package/dist/index.html +2 -2
- package/lib/config-cli.js +768 -0
- package/lib/index.js +84 -0
- package/lib/local-store.js +3 -3
- package/lib/pac-file.js +3 -2
- package/lib/pac-match.js +7 -5
- package/lib/proxy-server.js +12 -0
- package/lib/sslocal-manager.js +51 -14
- package/package.json +4 -2
- package/dist/assets/index-CmIDvBkA.js +0 -28
- package/dist/assets/index-QepWtMiX.css +0 -1
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { input, select, confirm, password } from '@inquirer/prompts';
|
|
4
|
+
import {
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
getStoreDir,
|
|
8
|
+
getStorePath,
|
|
9
|
+
getRemoteConfigPath,
|
|
10
|
+
getPacRules,
|
|
11
|
+
setPacRules,
|
|
12
|
+
getProxyConfig,
|
|
13
|
+
getSslocalConfig,
|
|
14
|
+
} from './local-store.js';
|
|
15
|
+
import { getSslocalCiphers } from './sslocal-manager.js';
|
|
16
|
+
|
|
17
|
+
/** @param {unknown} n */
|
|
18
|
+
export function validatePortNum(n) {
|
|
19
|
+
const p = Number(n);
|
|
20
|
+
if (!Number.isInteger(p) || p < 1 || p > 65535) {
|
|
21
|
+
return '请输入 1–65535 之间的整数端口';
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {string} s */
|
|
27
|
+
export function validateUpstreamUrl(s) {
|
|
28
|
+
const t = (s || '').trim();
|
|
29
|
+
if (!t) return true;
|
|
30
|
+
try {
|
|
31
|
+
const u = new URL(t);
|
|
32
|
+
if (!u.protocol || (u.protocol !== 'socks5:' && u.protocol !== 'socks4:' && u.protocol !== 'http:' && u.protocol !== 'https:')) {
|
|
33
|
+
return '上游地址需为 socks5://、socks4://、http:// 或 https:// 开头';
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return '请输入合法的上游代理 URL';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {Record<string, unknown>} config */
|
|
42
|
+
export function sanitizeConfigForPrint(config) {
|
|
43
|
+
const c = JSON.parse(JSON.stringify(config));
|
|
44
|
+
if (c.sslocal && typeof c.sslocal === 'object' && c.sslocal.password) {
|
|
45
|
+
c.sslocal = { ...c.sslocal, password: '***' };
|
|
46
|
+
}
|
|
47
|
+
return c;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* CLI / 脚本用:解析代理模式关键字,与 Web「代理设置」四档一致。
|
|
52
|
+
* @param {string} raw
|
|
53
|
+
* @returns {'direct'|'global'|'pac'|'mitm'|null}
|
|
54
|
+
*/
|
|
55
|
+
export function normalizeProxyModeCli(raw) {
|
|
56
|
+
const k = String(raw || '').trim().toLowerCase();
|
|
57
|
+
if (k === 'direct' || k === 'off' || k === 'clear' || k === 'none') return 'direct';
|
|
58
|
+
if (k === 'global' || k === 'system') return 'global';
|
|
59
|
+
if (k === 'pac') return 'pac';
|
|
60
|
+
if (k === 'mitm' || k === 'capture') return 'mitm';
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 写入 config.json 中的 proxy(与 Web 切换模式时保存的字段一致)。若需立即启停代理与系统设置,请使用带 `tryPushProxyToRunningServe` 的交互命令,或确保 `pac-proxy serve` 已运行。
|
|
66
|
+
* @param {string} modeInput
|
|
67
|
+
*/
|
|
68
|
+
export function runConfigSetProxyMode(modeInput) {
|
|
69
|
+
const mode = normalizeProxyModeCli(modeInput);
|
|
70
|
+
if (!mode) {
|
|
71
|
+
console.error(
|
|
72
|
+
`无效的代理模式「${modeInput}」。可选: direct(直连), global|system(系统/全局代理), pac, mitm(抓包)`,
|
|
73
|
+
);
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const cfg = loadConfig();
|
|
78
|
+
const proxy = { ...cfg.proxy };
|
|
79
|
+
if (mode === 'direct') {
|
|
80
|
+
proxy.enabled = false;
|
|
81
|
+
proxy.applySystemProxy = true;
|
|
82
|
+
} else {
|
|
83
|
+
proxy.enabled = true;
|
|
84
|
+
proxy.mode = mode;
|
|
85
|
+
proxy.applySystemProxy = true;
|
|
86
|
+
}
|
|
87
|
+
saveConfig({ ...cfg, proxy });
|
|
88
|
+
const labels = {
|
|
89
|
+
direct: '直连(无代理)',
|
|
90
|
+
global: '系统代理(全局 HTTP)',
|
|
91
|
+
pac: 'PAC 代理',
|
|
92
|
+
mitm: '抓包代理 (MITM)',
|
|
93
|
+
};
|
|
94
|
+
console.log(`已写入代理模式: ${labels[mode]}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 探测本机控制台 HTTP 端口(默认扫 5174–5184,可用 PAC_PROXY_CONSOLE_PORT 指定) */
|
|
98
|
+
function getConsoleHttpPortsToProbe() {
|
|
99
|
+
const raw = process.env.PAC_PROXY_CONSOLE_PORT;
|
|
100
|
+
if (raw != null && String(raw).trim()) {
|
|
101
|
+
const p = Number(String(raw).trim());
|
|
102
|
+
if (Number.isInteger(p) && p >= 1 && p <= 65535) return [p];
|
|
103
|
+
}
|
|
104
|
+
const ports = [];
|
|
105
|
+
for (let i = 5174; i <= 5184; i++) ports.push(i);
|
|
106
|
+
return ports;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 若本机已有 `pac-proxy serve` 在跑,对其发起 PUT /api/local/proxy,触发与 Web「代理设置」相同的 applyProxyService(启停本地代理、系统代理等)。
|
|
111
|
+
* @returns {Promise<boolean>} 是否成功命中并同步
|
|
112
|
+
*/
|
|
113
|
+
export async function tryPushProxyToRunningServe() {
|
|
114
|
+
const proxy = getProxyConfig();
|
|
115
|
+
const body = JSON.stringify(proxy);
|
|
116
|
+
for (const port of getConsoleHttpPortsToProbe()) {
|
|
117
|
+
const ctrl = new AbortController();
|
|
118
|
+
const tid = setTimeout(() => ctrl.abort(), 800);
|
|
119
|
+
try {
|
|
120
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/local/proxy`, {
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body,
|
|
124
|
+
signal: ctrl.signal,
|
|
125
|
+
});
|
|
126
|
+
if (r.ok) {
|
|
127
|
+
console.log('已向运行中的控制台同步代理设置(与 Web 保存一致)。');
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
/* 下一端口 */
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(tid);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 若本机已有 `pac-proxy serve` 在跑,对其发起 PUT /api/local/sslocal,与 Web「Shadowsocks」页保存一致(写盘、启停 sslocal、applyProxyService)。
|
|
141
|
+
* @returns {Promise<boolean>} 是否成功命中并同步
|
|
142
|
+
*/
|
|
143
|
+
export async function tryPushSslocalToRunningServe() {
|
|
144
|
+
const sslocal = getSslocalConfig();
|
|
145
|
+
const body = JSON.stringify(sslocal);
|
|
146
|
+
for (const port of getConsoleHttpPortsToProbe()) {
|
|
147
|
+
const ctrl = new AbortController();
|
|
148
|
+
const tid = setTimeout(() => ctrl.abort(), 800);
|
|
149
|
+
try {
|
|
150
|
+
const r = await fetch(`http://127.0.0.1:${port}/api/local/sslocal`, {
|
|
151
|
+
method: 'PUT',
|
|
152
|
+
headers: { 'Content-Type': 'application/json' },
|
|
153
|
+
body,
|
|
154
|
+
signal: ctrl.signal,
|
|
155
|
+
});
|
|
156
|
+
if (r.ok) {
|
|
157
|
+
console.log('已向运行中的控制台同步 Shadowsocks (sslocal) 设置(与 Web 保存一致)。');
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
/* 下一端口 */
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(tid);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 与 Web「Shadowsocks / sslocal」页字段一致的交互问答。
|
|
171
|
+
* @param {Record<string, unknown>} currentSslocal
|
|
172
|
+
*/
|
|
173
|
+
async function promptSslocalFields(currentSslocal) {
|
|
174
|
+
const sslocal = { ...currentSslocal };
|
|
175
|
+
sslocal.enabled = await confirm({
|
|
176
|
+
message: '是否启用内置代理客户端 (sslocal)',
|
|
177
|
+
default: !!sslocal.enabled,
|
|
178
|
+
});
|
|
179
|
+
if (!sslocal.enabled) {
|
|
180
|
+
return { ...currentSslocal, enabled: false };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const server = await input({
|
|
184
|
+
message: '服务器地址',
|
|
185
|
+
default: sslocal.server || '',
|
|
186
|
+
});
|
|
187
|
+
if ((server || '').trim()) sslocal.server = server.trim();
|
|
188
|
+
|
|
189
|
+
const sp = await input({
|
|
190
|
+
message: '服务器端口',
|
|
191
|
+
default: String(sslocal.serverPort ?? 8388),
|
|
192
|
+
validate: validatePortNum,
|
|
193
|
+
});
|
|
194
|
+
sslocal.serverPort = Number(sp);
|
|
195
|
+
|
|
196
|
+
const lp = await input({
|
|
197
|
+
message: '本地 SOCKS5 端口',
|
|
198
|
+
default: String(sslocal.localPort ?? 1080),
|
|
199
|
+
validate: validatePortNum,
|
|
200
|
+
});
|
|
201
|
+
sslocal.localPort = Number(lp);
|
|
202
|
+
|
|
203
|
+
const changePw = await confirm({
|
|
204
|
+
message: '是否修改密码?(否则保留已保存密码)',
|
|
205
|
+
default: false,
|
|
206
|
+
});
|
|
207
|
+
if (changePw) {
|
|
208
|
+
const pw = await password({ message: '密码', mask: '*' });
|
|
209
|
+
sslocal.password = pw || '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const methods = getSslocalCiphers();
|
|
213
|
+
const methodChoices = methods.map((m) => ({ name: m, value: m }));
|
|
214
|
+
const currentMethod = methods.includes(sslocal.method) ? sslocal.method : methods[0];
|
|
215
|
+
sslocal.method = await select({
|
|
216
|
+
message: '加密方法',
|
|
217
|
+
choices: methodChoices,
|
|
218
|
+
default: currentMethod,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const to = await input({
|
|
222
|
+
message: '超时(秒)',
|
|
223
|
+
default: String(sslocal.timeout ?? 300),
|
|
224
|
+
validate: (v) => {
|
|
225
|
+
const n = Number((v || '').trim());
|
|
226
|
+
if (!Number.isInteger(n) || n < 1) return '请输入正整数';
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
sslocal.timeout = Number((to || '').trim());
|
|
231
|
+
sslocal.enabled = true;
|
|
232
|
+
return sslocal;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function runConfigProxyModeInteractive() {
|
|
236
|
+
const proxy = getProxyConfig();
|
|
237
|
+
/** @type {'direct'|'global'|'pac'|'mitm'} */
|
|
238
|
+
let current = 'pac';
|
|
239
|
+
if (!proxy.enabled) current = 'direct';
|
|
240
|
+
else if (proxy.mode === 'global') current = 'global';
|
|
241
|
+
else if (proxy.mode === 'mitm') current = 'mitm';
|
|
242
|
+
else current = 'pac';
|
|
243
|
+
|
|
244
|
+
console.log('\n与 Web「代理设置」共用配置。\n');
|
|
245
|
+
|
|
246
|
+
const choice = await select({
|
|
247
|
+
message: '选择代理模式',
|
|
248
|
+
choices: [
|
|
249
|
+
{ value: 'direct', name: '直连(无代理)' },
|
|
250
|
+
{ value: 'global', name: '系统代理(全局 HTTP)' },
|
|
251
|
+
{ value: 'pac', name: 'PAC 代理' },
|
|
252
|
+
{ value: 'mitm', name: '抓包代理 (MITM)' },
|
|
253
|
+
],
|
|
254
|
+
default: current,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
runConfigSetProxyMode(choice);
|
|
258
|
+
const pushed = await tryPushProxyToRunningServe();
|
|
259
|
+
if (!pushed) {
|
|
260
|
+
console.log('未检测到运行中的控制台(pac-proxy serve),仅已写入配置文件;启动后可自动同步或于 Web 中保存一次。');
|
|
261
|
+
}
|
|
262
|
+
printPathsHint(loadConfig());
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {Array<{ pattern?: string, id?: string, action?: string, priority?: number }>} existing
|
|
267
|
+
* @param {unknown} imported
|
|
268
|
+
*/
|
|
269
|
+
export function mergePacRulesImport(existing, imported) {
|
|
270
|
+
if (!Array.isArray(imported)) {
|
|
271
|
+
throw new Error('文件格式错误:需为规则 JSON 数组');
|
|
272
|
+
}
|
|
273
|
+
const merged = [...existing];
|
|
274
|
+
for (const r of imported) {
|
|
275
|
+
if (!r || typeof r !== 'object' || !r.pattern) continue;
|
|
276
|
+
const pattern = String(r.pattern).trim();
|
|
277
|
+
if (!pattern) continue;
|
|
278
|
+
const idx = merged.findIndex((x) => (x.pattern || '').trim() === pattern);
|
|
279
|
+
const entry = {
|
|
280
|
+
id: r.id || String(Date.now() + Math.random()),
|
|
281
|
+
pattern,
|
|
282
|
+
action: r.action === 'direct' ? 'direct' : r.action === 'block' ? 'block' : 'proxy',
|
|
283
|
+
priority: Number(r.priority) || 0,
|
|
284
|
+
};
|
|
285
|
+
if (idx >= 0) merged[idx] = { ...merged[idx], ...entry };
|
|
286
|
+
else merged.push(entry);
|
|
287
|
+
}
|
|
288
|
+
return merged;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* @param {Array<{ pattern?: string, id?: string }>} rules
|
|
293
|
+
* @param {string} patternTrimmed
|
|
294
|
+
* @param {string | undefined} excludeId 编辑时传入当前规则 id,新增时不传
|
|
295
|
+
*/
|
|
296
|
+
export function pacRulePatternTakenByOther(rules, patternTrimmed, excludeId) {
|
|
297
|
+
return rules.some((r) => {
|
|
298
|
+
if (excludeId != null && r.id === excludeId) return false;
|
|
299
|
+
return (r.pattern || '').trim() === patternTrimmed;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* @param {Array<{ pattern?: string, id?: string, action?: string, priority?: number }>} rules
|
|
305
|
+
* @param {string} id
|
|
306
|
+
* @param {{ pattern: string, action: string, priority: number }} fields
|
|
307
|
+
*/
|
|
308
|
+
export function applyPacRuleEdit(rules, id, fields) {
|
|
309
|
+
const idx = rules.findIndex((r) => r.id === id);
|
|
310
|
+
if (idx < 0) return { ok: false, error: 'not_found' };
|
|
311
|
+
if (pacRulePatternTakenByOther(rules, fields.pattern, id)) {
|
|
312
|
+
return { ok: false, error: 'duplicate' };
|
|
313
|
+
}
|
|
314
|
+
const next = [...rules];
|
|
315
|
+
next[idx] = { ...next[idx], ...fields };
|
|
316
|
+
return { ok: true, rules: next };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @param {{ pattern?: string, action?: string, priority?: number }} [defaults]
|
|
321
|
+
*/
|
|
322
|
+
export async function promptPacRuleFields(defaults = {}) {
|
|
323
|
+
const pattern = await input({
|
|
324
|
+
message: '匹配模式(如 *.google.com)',
|
|
325
|
+
default: (defaults.pattern || '').trim(),
|
|
326
|
+
validate: (v) => ((v || '').trim() ? true : '不能为空'),
|
|
327
|
+
});
|
|
328
|
+
const defaultAction =
|
|
329
|
+
defaults.action === 'direct' ? 'direct' : defaults.action === 'block' ? 'block' : 'proxy';
|
|
330
|
+
const action = await select({
|
|
331
|
+
message: '动作',
|
|
332
|
+
choices: [
|
|
333
|
+
{ value: 'proxy', name: '代理' },
|
|
334
|
+
{ value: 'direct', name: '直连' },
|
|
335
|
+
{ value: 'block', name: '拦截' },
|
|
336
|
+
],
|
|
337
|
+
default: defaultAction,
|
|
338
|
+
});
|
|
339
|
+
const pr = await input({
|
|
340
|
+
message: '优先级(数字越大越优先)',
|
|
341
|
+
default: String(defaults.priority ?? 0),
|
|
342
|
+
validate: (v) => {
|
|
343
|
+
const n = Number((v || '').trim());
|
|
344
|
+
if (Number.isNaN(n)) return '请输入数字';
|
|
345
|
+
return true;
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
pattern: String(pattern).trim(),
|
|
350
|
+
action,
|
|
351
|
+
priority: Number((pr || '').trim()) || 0,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function sortPacRulesByPriorityDesc(rules) {
|
|
356
|
+
return [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function printPathsHint(cfg) {
|
|
360
|
+
const home = getStoreDir();
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(`数据目录 PAC_PROXY_HOME: ${home}`);
|
|
363
|
+
console.log(`配置文件: ${getStorePath()}`);
|
|
364
|
+
if (cfg.mode === 'remote') {
|
|
365
|
+
console.log(`远程模式下 PAC 规则文件: ${getRemoteConfigPath()}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function runConfigPrint() {
|
|
370
|
+
const cfg = loadConfig();
|
|
371
|
+
console.log(JSON.stringify(sanitizeConfigForPrint(cfg), null, 2));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @param {string} filePath
|
|
376
|
+
*/
|
|
377
|
+
export async function runPacImportFile(filePath) {
|
|
378
|
+
const abs = path.resolve(filePath);
|
|
379
|
+
if (!fs.existsSync(abs)) {
|
|
380
|
+
console.error(`文件不存在: ${abs}`);
|
|
381
|
+
process.exitCode = 1;
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
385
|
+
let imported;
|
|
386
|
+
try {
|
|
387
|
+
imported = JSON.parse(text);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
console.error(`JSON 解析失败: ${e instanceof Error ? e.message : e}`);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const current = getPacRules();
|
|
394
|
+
const merged = mergePacRulesImport(current, imported);
|
|
395
|
+
setPacRules(merged);
|
|
396
|
+
console.log(`已合并写入 ${merged.length} 条 PAC 规则(与 Web 控制台一致)。`);
|
|
397
|
+
printPathsHint(loadConfig());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function promptPacOptional() {
|
|
401
|
+
const choice = await select({
|
|
402
|
+
message: 'PAC 规则(可选)',
|
|
403
|
+
choices: [
|
|
404
|
+
{ value: 'skip', name: '跳过,不改 PAC 规则' },
|
|
405
|
+
{ value: 'import', name: '从 JSON 文件合并导入' },
|
|
406
|
+
{ value: 'add', name: '追加一条规则' },
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (choice === 'skip') return;
|
|
411
|
+
|
|
412
|
+
if (choice === 'import') {
|
|
413
|
+
const fp = await input({
|
|
414
|
+
message: '规则 JSON 文件路径',
|
|
415
|
+
validate: (v) => ((v || '').trim() ? true : '请输入路径'),
|
|
416
|
+
});
|
|
417
|
+
const abs = path.resolve((fp || '').trim());
|
|
418
|
+
if (!fs.existsSync(abs)) {
|
|
419
|
+
console.error(`文件不存在: ${abs}`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
424
|
+
const imported = JSON.parse(text);
|
|
425
|
+
const current = getPacRules();
|
|
426
|
+
const merged = mergePacRulesImport(current, imported);
|
|
427
|
+
setPacRules(merged);
|
|
428
|
+
console.log(`已导入合并,当前共 ${merged.length} 条规则。`);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
console.error(e instanceof Error ? e.message : e);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (choice === 'add') {
|
|
436
|
+
const fields = await promptPacRuleFields();
|
|
437
|
+
const current = getPacRules();
|
|
438
|
+
if (pacRulePatternTakenByOther(current, fields.pattern, undefined)) {
|
|
439
|
+
console.error(`规则「${fields.pattern}」已存在,未追加。`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
current.push({
|
|
443
|
+
id: String(Date.now()),
|
|
444
|
+
...fields,
|
|
445
|
+
});
|
|
446
|
+
setPacRules(current);
|
|
447
|
+
console.log('已追加 1 条规则。');
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function runPacRulesInteractive() {
|
|
452
|
+
console.log('\nPAC 规则管理(与 Web 控制台共用存储)。\n');
|
|
453
|
+
|
|
454
|
+
for (;;) {
|
|
455
|
+
const action = await select({
|
|
456
|
+
message: '操作',
|
|
457
|
+
choices: [
|
|
458
|
+
{ value: 'list', name: '列出当前规则' },
|
|
459
|
+
{ value: 'add', name: '添加规则' },
|
|
460
|
+
{ value: 'edit', name: '编辑规则' },
|
|
461
|
+
{ value: 'remove', name: '删除规则' },
|
|
462
|
+
{ value: 'exit', name: '退出' },
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
if (action === 'exit') break;
|
|
467
|
+
|
|
468
|
+
const raw = getPacRules();
|
|
469
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
470
|
+
|
|
471
|
+
if (action === 'list') {
|
|
472
|
+
const sorted = sortPacRulesByPriorityDesc(list);
|
|
473
|
+
if (!sorted.length) {
|
|
474
|
+
console.log('\n(暂无规则)\n');
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
console.log('');
|
|
478
|
+
const wPat = Math.max(8, ...sorted.map((r) => (r.pattern || '').length));
|
|
479
|
+
const head = `${'匹配模式'.padEnd(wPat)} ${'动作'.padEnd(8)} 优先级`;
|
|
480
|
+
console.log(head);
|
|
481
|
+
console.log('-'.repeat(head.length + 2));
|
|
482
|
+
for (const r of sorted) {
|
|
483
|
+
const pat = (r.pattern || '').padEnd(wPat);
|
|
484
|
+
const act = (r.action === 'direct' ? 'direct' : r.action === 'block' ? 'block' : 'proxy').padEnd(8);
|
|
485
|
+
console.log(`${pat} ${act} ${r.priority ?? 0}`);
|
|
486
|
+
}
|
|
487
|
+
console.log(`\n共 ${sorted.length} 条\n`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (action === 'add') {
|
|
492
|
+
const fields = await promptPacRuleFields();
|
|
493
|
+
if (pacRulePatternTakenByOther(list, fields.pattern, undefined)) {
|
|
494
|
+
console.error(`规则「${fields.pattern}」已存在,未保存。\n`);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
const next = [...list, { id: String(Date.now()), ...fields }];
|
|
498
|
+
setPacRules(next);
|
|
499
|
+
console.log('已添加。\n');
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (action === 'edit' || action === 'remove') {
|
|
504
|
+
if (!list.length) {
|
|
505
|
+
console.log('\n暂无规则可编辑或删除。\n');
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const sorted = sortPacRulesByPriorityDesc(list);
|
|
509
|
+
const pick = await select({
|
|
510
|
+
message: action === 'edit' ? '选择要编辑的规则' : '选择要删除的规则',
|
|
511
|
+
choices: sorted.map((r) => {
|
|
512
|
+
const p = (r.pattern || '').trim() || '(空)';
|
|
513
|
+
const short = p.length > 48 ? `${p.slice(0, 45)}…` : p;
|
|
514
|
+
return {
|
|
515
|
+
value: r.id,
|
|
516
|
+
name: `[${r.priority ?? 0}] ${short} (${r.action === 'direct' ? '直连' : r.action === 'block' ? '拦截' : '代理'})`,
|
|
517
|
+
};
|
|
518
|
+
}),
|
|
519
|
+
});
|
|
520
|
+
const rule = list.find((r) => r.id === pick);
|
|
521
|
+
if (!rule) {
|
|
522
|
+
console.error('未找到该规则。\n');
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (action === 'remove') {
|
|
526
|
+
const ok = await confirm({
|
|
527
|
+
message: `确定删除「${(rule.pattern || '').trim()}」?`,
|
|
528
|
+
default: false,
|
|
529
|
+
});
|
|
530
|
+
if (!ok) {
|
|
531
|
+
console.log('已取消。\n');
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
setPacRules(list.filter((r) => r.id !== pick));
|
|
535
|
+
console.log('已删除。\n');
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
const fields = await promptPacRuleFields({
|
|
539
|
+
pattern: rule.pattern || '',
|
|
540
|
+
action: rule.action === 'direct' ? 'direct' : rule.action === 'block' ? 'block' : 'proxy',
|
|
541
|
+
priority: rule.priority ?? 0,
|
|
542
|
+
});
|
|
543
|
+
const result = applyPacRuleEdit(list, pick, fields);
|
|
544
|
+
if (!result.ok) {
|
|
545
|
+
if (result.error === 'duplicate') {
|
|
546
|
+
console.error(`与已有规则 pattern 冲突,未保存。\n`);
|
|
547
|
+
} else {
|
|
548
|
+
console.error('未找到该规则。\n');
|
|
549
|
+
}
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
setPacRules(result.rules);
|
|
553
|
+
console.log('已更新。\n');
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
printPathsHint(loadConfig());
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export async function runConfigInteractive() {
|
|
561
|
+
let cfg = loadConfig();
|
|
562
|
+
const proxy = { ...cfg.proxy };
|
|
563
|
+
const sslocal = { ...cfg.sslocal };
|
|
564
|
+
|
|
565
|
+
console.log('\n与 Web 控制台共用本地配置。留空回车可保持当前值(布尔项用提示选择)。\n');
|
|
566
|
+
|
|
567
|
+
const mode = await select({
|
|
568
|
+
message: '运行模式(PAC 规则存储位置)',
|
|
569
|
+
choices: [
|
|
570
|
+
{
|
|
571
|
+
value: 'local',
|
|
572
|
+
name: '本地模式 — 规则在 config.json',
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
value: 'remote',
|
|
576
|
+
name: '远程模式 — 规则在 remote_config.json(登录仍在 Web)',
|
|
577
|
+
},
|
|
578
|
+
],
|
|
579
|
+
default: cfg.mode === 'remote' ? 'remote' : 'local',
|
|
580
|
+
});
|
|
581
|
+
cfg = { ...cfg, mode };
|
|
582
|
+
|
|
583
|
+
proxy.enabled = await confirm({
|
|
584
|
+
message: '是否启用本地代理服务(与 Web「代理设置」一致)',
|
|
585
|
+
default: !!proxy.enabled,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
proxy.mode = await select({
|
|
589
|
+
message: '代理模式',
|
|
590
|
+
choices: [
|
|
591
|
+
{ value: 'pac', name: 'PAC 代理' },
|
|
592
|
+
{ value: 'global', name: '全局代理' },
|
|
593
|
+
{ value: 'mitm', name: '抓包代理 (MITM)' },
|
|
594
|
+
],
|
|
595
|
+
default: ['pac', 'global', 'mitm'].includes(proxy.mode) ? proxy.mode : 'pac',
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const up = await input({
|
|
599
|
+
message: '上游代理地址(socks5/http,留空保持)',
|
|
600
|
+
default: proxy.upstream || '',
|
|
601
|
+
validate: validateUpstreamUrl,
|
|
602
|
+
});
|
|
603
|
+
if ((up || '').trim()) proxy.upstream = up.trim();
|
|
604
|
+
|
|
605
|
+
const httpP = await input({
|
|
606
|
+
message: '本地 HTTP 代理端口',
|
|
607
|
+
default: String(proxy.httpPort ?? 5175),
|
|
608
|
+
validate: validatePortNum,
|
|
609
|
+
});
|
|
610
|
+
proxy.httpPort = Number(httpP);
|
|
611
|
+
|
|
612
|
+
const httpsP = await input({
|
|
613
|
+
message: '本地 HTTPS 代理端口',
|
|
614
|
+
default: String(proxy.httpsPort ?? 5176),
|
|
615
|
+
validate: validatePortNum,
|
|
616
|
+
});
|
|
617
|
+
proxy.httpsPort = Number(httpsP);
|
|
618
|
+
|
|
619
|
+
proxy.applySystemProxy = await confirm({
|
|
620
|
+
message: '保存后是否应用系统代理(需本机支持,见 README)',
|
|
621
|
+
default: proxy.applySystemProxy !== false,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const nextSslocal = await promptSslocalFields(sslocal);
|
|
625
|
+
|
|
626
|
+
cfg = {
|
|
627
|
+
...cfg,
|
|
628
|
+
proxy: { ...cfg.proxy, ...proxy },
|
|
629
|
+
sslocal: nextSslocal,
|
|
630
|
+
};
|
|
631
|
+
saveConfig(cfg);
|
|
632
|
+
|
|
633
|
+
await promptPacOptional();
|
|
634
|
+
|
|
635
|
+
console.log('\n配置已保存。');
|
|
636
|
+
const pushed = await tryPushProxyToRunningServe();
|
|
637
|
+
if (!pushed) {
|
|
638
|
+
console.log('未检测到运行中的控制台,代理相关进程与系统代理未自动应用;可启动 pac-proxy serve 后重试本命令或于 Web 保存。');
|
|
639
|
+
}
|
|
640
|
+
printPathsHint(loadConfig());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** 仅编辑 Shadowsocks (sslocal),与 Web「Shadowsocks」页共用 config.json 字段与保存副作用。 */
|
|
644
|
+
export async function runConfigShadowsocksInteractive() {
|
|
645
|
+
const cfg = loadConfig();
|
|
646
|
+
console.log('\nShadowsocks (sslocal) — 与 Web 控制台「代理客户端」页共用配置。\n');
|
|
647
|
+
|
|
648
|
+
const nextSslocal = await promptSslocalFields({ ...cfg.sslocal });
|
|
649
|
+
saveConfig({ ...cfg, sslocal: nextSslocal });
|
|
650
|
+
|
|
651
|
+
console.log('\n配置已保存。');
|
|
652
|
+
const pushed = await tryPushSslocalToRunningServe();
|
|
653
|
+
if (!pushed) {
|
|
654
|
+
console.log('未检测到运行中的控制台,sslocal 进程与上游代理未自动应用;可启动 pac-proxy serve 后重试本命令或在 Web 中保存。');
|
|
655
|
+
}
|
|
656
|
+
printPathsHint(loadConfig());
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function runConfigInit() {
|
|
660
|
+
console.log('\n首次配置向导(可随时用 pac-proxy config 再次编辑)。\n');
|
|
661
|
+
printPathsHint(loadConfig());
|
|
662
|
+
|
|
663
|
+
const mode = await select({
|
|
664
|
+
message: '运行模式',
|
|
665
|
+
choices: [
|
|
666
|
+
{ value: 'local', name: '本地模式' },
|
|
667
|
+
{ value: 'remote', name: '远程模式(PAC 规则存 remote_config.json)' },
|
|
668
|
+
],
|
|
669
|
+
default: 'local',
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const upstream = await input({
|
|
673
|
+
message: '上游代理 URL(例如 socks5://127.0.0.1:1080)',
|
|
674
|
+
default: 'socks5://127.0.0.1:1080',
|
|
675
|
+
validate: (v) => {
|
|
676
|
+
if (!(v || '').trim()) return '请输入上游地址';
|
|
677
|
+
return validateUpstreamUrl(v);
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
const httpPort = await input({
|
|
682
|
+
message: '本地 HTTP 端口',
|
|
683
|
+
default: '5175',
|
|
684
|
+
validate: validatePortNum,
|
|
685
|
+
});
|
|
686
|
+
const httpsPort = await input({
|
|
687
|
+
message: '本地 HTTPS 端口',
|
|
688
|
+
default: '5176',
|
|
689
|
+
validate: validatePortNum,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const enableClient = await confirm({
|
|
693
|
+
message: '是否启用内置代理客户端(sslocal)?',
|
|
694
|
+
default: false,
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
let sslocal = {
|
|
698
|
+
enabled: enableClient,
|
|
699
|
+
server: '',
|
|
700
|
+
serverPort: 8388,
|
|
701
|
+
localPort: 1080,
|
|
702
|
+
password: '',
|
|
703
|
+
method: 'aes-256-gcm',
|
|
704
|
+
timeout: 300,
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
if (enableClient) {
|
|
708
|
+
sslocal.server = await input({
|
|
709
|
+
message: '服务器地址',
|
|
710
|
+
validate: (v) => ((v || '').trim() ? true : '必填'),
|
|
711
|
+
});
|
|
712
|
+
const sp = await input({
|
|
713
|
+
message: '服务器端口',
|
|
714
|
+
default: '8388',
|
|
715
|
+
validate: validatePortNum,
|
|
716
|
+
});
|
|
717
|
+
sslocal.serverPort = Number(sp);
|
|
718
|
+
const lp = await input({
|
|
719
|
+
message: '本地 SOCKS5 端口',
|
|
720
|
+
default: '1080',
|
|
721
|
+
validate: validatePortNum,
|
|
722
|
+
});
|
|
723
|
+
sslocal.localPort = Number(lp);
|
|
724
|
+
sslocal.password = await password({ message: '密码', mask: '*' });
|
|
725
|
+
const methods = getSslocalCiphers();
|
|
726
|
+
sslocal.method = await select({
|
|
727
|
+
message: '加密方法',
|
|
728
|
+
choices: methods.map((m) => ({ name: m, value: m })),
|
|
729
|
+
default: 'aes-256-gcm',
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const proxySummary = {
|
|
734
|
+
enabled: false,
|
|
735
|
+
mode: 'pac',
|
|
736
|
+
upstream: upstream.trim(),
|
|
737
|
+
httpPort: Number(httpPort),
|
|
738
|
+
httpsPort: Number(httpsPort),
|
|
739
|
+
applySystemProxy: true,
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
const ok = await confirm({
|
|
743
|
+
message: `确认写入?\n 模式: ${mode}\n 上游: ${proxySummary.upstream}\n 端口: ${proxySummary.httpPort}/${proxySummary.httpsPort}\n sslocal: ${sslocal.enabled ? '开' : '关'}`,
|
|
744
|
+
default: true,
|
|
745
|
+
});
|
|
746
|
+
if (!ok) {
|
|
747
|
+
console.log('已取消。');
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const current = loadConfig();
|
|
752
|
+
const mergedSslocal = enableClient
|
|
753
|
+
? { ...current.sslocal, ...sslocal }
|
|
754
|
+
: { ...current.sslocal, enabled: false };
|
|
755
|
+
const merged = {
|
|
756
|
+
...current,
|
|
757
|
+
mode,
|
|
758
|
+
proxy: { ...current.proxy, ...proxySummary },
|
|
759
|
+
sslocal: mergedSslocal,
|
|
760
|
+
};
|
|
761
|
+
saveConfig(merged);
|
|
762
|
+
console.log('\n已保存。');
|
|
763
|
+
const pushed = await tryPushProxyToRunningServe();
|
|
764
|
+
if (!pushed) {
|
|
765
|
+
console.log('未检测到运行中的控制台,代理未自动应用;可启动 pac-proxy serve 后于 Web 保存或再次执行本向导。');
|
|
766
|
+
}
|
|
767
|
+
printPathsHint(loadConfig());
|
|
768
|
+
}
|