tt-help-cli-ycl 1.4.115 → 1.4.117

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.115",
3
+ "version": "1.4.117",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * clash 控制端自动发现 — 验证脚本
4
+ * 用法: node scripts/test-clash-discover.mjs
5
+ */
6
+ import {
7
+ parseFlatYaml,
8
+ parseController,
9
+ verifyController,
10
+ discover,
11
+ invalidateDiscoverCache,
12
+ listListeningPorts,
13
+ getSystemProxyPorts,
14
+ } from "../src/clash/discover.js";
15
+ import { configure, getBaseConfig, resetConfig } from "../src/clash/api.js";
16
+ import { readFileSync, existsSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ let failed = 0;
21
+ function check(name, cond, extra = "") {
22
+ console.log(`${cond ? "✓" : "✗"} ${name}${extra ? ` — ${extra}` : ""}`);
23
+ if (!cond) failed++;
24
+ }
25
+
26
+ // ── 单元 ──
27
+ const yaml = parseFlatYaml(
28
+ `external-controller: 127.0.0.1:19835\nsecret: "abc123"\nmode: rule\nproxies:\n - a\n`,
29
+ );
30
+ check(
31
+ "parseFlatYaml 顶层字段",
32
+ yaml["external-controller"] === "127.0.0.1:19835" && yaml.secret === "abc123",
33
+ );
34
+ check("parseFlatYaml 跳过嵌套/列表", !("proxies" in yaml));
35
+ check("parseController 标准格式", parseController("127.0.0.1:9097") === 9097);
36
+ check("parseController 空值", parseController("") === null);
37
+ check("listListeningPorts 枚举非空", listListeningPorts().length > 0);
38
+
39
+ // ── 集成: 按当前环境自适应验证 ──
40
+ const listening = listListeningPorts();
41
+ const clvUp = listening.includes(9097);
42
+ // 69cloud 控制端口会漂移(19835 → 19216 …),从配置文件动态读取
43
+ const n69CfgPath = join(
44
+ homedir(),
45
+ "Library/Application Support/69cloud",
46
+ "config",
47
+ "config.yaml",
48
+ );
49
+ let n69Port = null;
50
+ if (existsSync(n69CfgPath)) {
51
+ n69Port = parseController(
52
+ parseFlatYaml(readFileSync(n69CfgPath, "utf-8"))["external-controller"],
53
+ );
54
+ }
55
+ const n69Up = n69Port !== null && listening.includes(n69Port);
56
+
57
+ if (clvUp) {
58
+ check(
59
+ "verifyController 9097 + abcdefg",
60
+ await verifyController(9097, "abcdefg"),
61
+ );
62
+ } else {
63
+ console.log("○ 跳过 9097 验证(Clash Verge 未运行)");
64
+ }
65
+ if (n69Up) {
66
+ check(
67
+ "verifyController 69cloud + 错误secret 应失败",
68
+ !(await verifyController(n69Port, "wrong-secret")),
69
+ );
70
+ } else {
71
+ console.log("○ 跳过 69cloud 验证(69cloud 未运行)");
72
+ }
73
+
74
+ const sysPorts = getSystemProxyPorts();
75
+ console.log(` 系统代理端口: [${sysPorts.join(", ") || "无"}]`);
76
+
77
+ // ── 集成: 自动发现 ──
78
+ invalidateDiscoverCache();
79
+ const d1 = await discover();
80
+ if (clvUp || n69Up) {
81
+ check(
82
+ "discover 自动模式(有客户端在线)",
83
+ !d1.fallback && listening.includes(d1.port),
84
+ `选定 ${d1.clientName} @${d1.port} 来源=${d1.source} proxy=${d1.proxyUrl}`,
85
+ );
86
+ console.log(` -> ${JSON.stringify(d1)}`);
87
+
88
+ // 显式覆盖(用自动发现选定的控制端做正向验证)
89
+ invalidateDiscoverCache();
90
+ const d2 = await discover({ apiPort: d1.port, secret: d1.secret });
91
+ check(
92
+ "discover 显式覆盖",
93
+ d2.port === d1.port && d2.source === "手动指定",
94
+ `@${d2.port} 来源=${d2.source}`,
95
+ );
96
+ } else {
97
+ check(
98
+ "discover 无客户端 → 回退默认值",
99
+ d1.fallback === true &&
100
+ d1.port === 9097 &&
101
+ d1.source === "未发现可用控制端",
102
+ `选定 ${d1.clientName} @${d1.port} 来源=${d1.source}`,
103
+ );
104
+ }
105
+
106
+ // 显式覆盖错误端口 → 必须抛错(任何环境都验证)
107
+ invalidateDiscoverCache();
108
+ let threw = false;
109
+ try {
110
+ await discover({ apiPort: 12345, secret: "x" });
111
+ } catch (e) {
112
+ threw = true;
113
+ console.log(` 预期报错: ${e.message}`);
114
+ }
115
+ check("discover 错误端口必须报错", threw);
116
+
117
+ // ── api.configure 注入 ──
118
+ resetConfig();
119
+ configure({ port: 19835, secret: "demo" });
120
+ const bc = getBaseConfig();
121
+ check("api.configure 注入", bc.port === 19835 && bc.secret === "demo");
122
+ resetConfig();
123
+ check("api.resetConfig 还原", getBaseConfig().port === 9097);
124
+
125
+ console.log(failed ? `\n✗ ${failed} 项失败` : "\n✓ 全部通过");
126
+ process.exit(failed ? 1 : 0);
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * clash 多控制端多通道 — 验证脚本
4
+ * 用法: node scripts/test-clash-multichannel.mjs
5
+ */
6
+ import { discoverAll, getSystemProxyPorts } from "../src/clash/discover.js";
7
+ import * as ipc from "../src/clash/ipc.js";
8
+
9
+ let failed = 0;
10
+ function check(name, cond, extra = "") {
11
+ console.log(`${cond ? "✓" : "✗"} ${name}${extra ? ` — ${extra}` : ""}`);
12
+ if (!cond) failed++;
13
+ }
14
+
15
+ // ── 1. discoverAll:多控制端发现 ──
16
+ const all = await discoverAll();
17
+ console.log(` 发现 ${all.length} 个控制端:`);
18
+ for (const c of all) {
19
+ console.log(
20
+ ` - [${c.proxyPort}] ${c.clientName} @${c.port}(${c.source}${c.fallback ? ",回退默认值" : ""})`,
21
+ );
22
+ }
23
+ check("discoverAll 返回数组", Array.isArray(all) && all.length > 0);
24
+ check(
25
+ "每个控制端含 proxyPort/proxyUrl",
26
+ all.every((c) => c.proxyPort > 0 && c.proxyUrl?.startsWith("http://")),
27
+ );
28
+ const sysPorts = getSystemProxyPorts();
29
+ if (sysPorts.length && all.length > 1) {
30
+ check(
31
+ "system-proxy 命中者排第一(主控制端)",
32
+ sysPorts.includes(all[0].proxyPort),
33
+ `主控制端 proxyPort=${all[0].proxyPort}, 系统代理=[${sysPorts.join(",")}]`,
34
+ );
35
+ } else {
36
+ console.log(
37
+ `○ 跳过主控制端排序断言(系统代理=[${sysPorts.join(",")}] 或单控制端)`,
38
+ );
39
+ }
40
+
41
+ // ── 2. IPC 多通道读写 ──
42
+ const TEST_PORT = 7890;
43
+ ipc.writeCommand(
44
+ { action: "failover", reason: "multichannel-test", proxyPort: TEST_PORT },
45
+ TEST_PORT,
46
+ );
47
+ const readBack = ipc.readCommand(TEST_PORT);
48
+ check(
49
+ "按端口写读命令通道",
50
+ Array.isArray(readBack) &&
51
+ readBack[0]?.action === "failover" &&
52
+ readBack[0]?.proxyPort === TEST_PORT,
53
+ );
54
+
55
+ ipc.writeHealth({ node: "test", healthy: true }, TEST_PORT);
56
+ const hBack = ipc.readHealth(TEST_PORT);
57
+ check(
58
+ "按端口写读健康通道",
59
+ !!hBack && hBack.node === "test" && hBack.healthy === true,
60
+ );
61
+
62
+ // 默认通道与端口通道互不影响
63
+ const legacyCmd = ipc.readCommand(0);
64
+ check(
65
+ "默认通道独立(未被端口通道污染)",
66
+ !legacyCmd || legacyCmd[0]?.action !== "failover" || !legacyCmd[0]?.proxyPort,
67
+ );
68
+
69
+ const channels = ipc.listCommandChannels();
70
+ check(
71
+ "listCommandChannels 含测试通道",
72
+ channels.includes(TEST_PORT),
73
+ `channels=[${channels.join(",")}]`,
74
+ );
75
+
76
+ ipc.clearCommand(TEST_PORT);
77
+ check("clearCommand 清空端口通道", ipc.readCommand(TEST_PORT) === null);
78
+
79
+ // ── 3. 路由逻辑(用 daemon 实例验证,不启动守护) ──
80
+ import { ClashDaemon } from "../src/clash/daemon.js";
81
+ const d = new ClashDaemon({ dryRun: true });
82
+ d._controllers = all;
83
+ d._primary = all[0];
84
+ const routed = d._routeController({
85
+ action: "failover",
86
+ proxyPort: all[0].proxyPort,
87
+ });
88
+ check(
89
+ "命令按 proxyPort 路由到对应控制端",
90
+ !!routed && routed.proxyPort === all[0].proxyPort,
91
+ );
92
+ const routedMiss = d._routeController({ action: "failover", proxyPort: 59999 });
93
+ check("未知端口回退主控制端", !!routedMiss && routedMiss === d._primary);
94
+
95
+ // 多控制端时:第二个控制端也能被路由命中
96
+ if (all.length > 1) {
97
+ const routed2 = d._routeController({
98
+ action: "switchNode",
99
+ node: "x",
100
+ proxyPort: all[1].proxyPort,
101
+ });
102
+ check(
103
+ "第二个控制端按端口路由命中",
104
+ !!routed2 && routed2.proxyPort === all[1].proxyPort,
105
+ `${routed2?.clientName} @${routed2?.port}`,
106
+ );
107
+ } else {
108
+ console.log("○ 单控制端,跳过第二控制端路由断言");
109
+ }
110
+
111
+ // 清理测试产生的健康文件
112
+ try {
113
+ const { rmSync } = await import("fs");
114
+ const { tmpdir } = await import("os");
115
+ const { join } = await import("path");
116
+ rmSync(join(tmpdir(), `clash-health-${TEST_PORT}.json`), { force: true });
117
+ } catch {}
118
+
119
+ console.log(failed ? `\n✗ ${failed} 项失败` : "\n✓ 全部通过");
120
+ process.exit(failed ? 1 : 0);
package/src/clash/api.js CHANGED
@@ -3,11 +3,37 @@
3
3
  *
4
4
  * API 参考: https://wiki.metacubex.one/en/api
5
5
  * 认证方式: Authorization: Bearer ${secret}
6
- * 依赖 Clash Verge GUI 运行时绑定的 TCP 127.0.0.1:9097
6
+ * 端口/secret 默认指向 Clash Verge (127.0.0.1:9097),
7
+ * 可通过 configure() 注入自动发现结果(见 discover.js),保留默认值向后兼容。
7
8
  */
8
9
 
9
- const BASE_URL = "http://127.0.0.1:9097";
10
- const SECRET = "abcdefg";
10
+ let BASE_URL = "http://127.0.0.1:9097";
11
+ let SECRET = "abcdefg";
12
+
13
+ /**
14
+ * 注入控制端配置(自动发现结果 / 手动覆盖)
15
+ * @param {{port?:number, secret?:string}} [opts] 只更新传入的字段
16
+ */
17
+ export function configure({ port, secret } = {}) {
18
+ if (port) BASE_URL = `http://127.0.0.1:${port}`;
19
+ if (typeof secret === "string") SECRET = secret;
20
+ }
21
+
22
+ /** 读取当前生效的端口/secret(供 checker/日志使用) */
23
+ export function getBaseConfig() {
24
+ const m = BASE_URL.match(/:(\d+)$/);
25
+ return {
26
+ baseUrl: BASE_URL,
27
+ port: m ? parseInt(m[1], 10) : 9097,
28
+ secret: SECRET,
29
+ };
30
+ }
31
+
32
+ /** 重置为默认值(Clash Verge 9097) */
33
+ export function resetConfig() {
34
+ BASE_URL = "http://127.0.0.1:9097";
35
+ SECRET = "abcdefg";
36
+ }
11
37
 
12
38
  function headers() {
13
39
  return {
@@ -1,234 +1,5 @@
1
1
  /**
2
- * Clash Verge (mihomo) RESTful API 客户端
3
- *
4
- * API 参考: https://wiki.metacubex.one/en/api
5
- * 认证方式: Authorization: Bearer ${secret}
6
- * 依赖 Clash Verge GUI 运行时绑定的 TCP 127.0.0.1:9097
2
+ * benchmark 子模块 API 客户端 — 与主模块 api.js 统一来源(决策 3A)
3
+ * 端口/secret 由主模块 api.js 的 configure() 注入,避免双份硬编码
7
4
  */
8
-
9
- const BASE_URL = "http://127.0.0.1:9097";
10
- const SECRET = "abcdefg";
11
-
12
- function headers() {
13
- return {
14
- Authorization: `Bearer ${SECRET}`,
15
- "Content-Type": "application/json",
16
- };
17
- }
18
-
19
- async function request(path, options = {}) {
20
- const url = `${BASE_URL}${path}`;
21
- const res = await fetch(url, {
22
- ...options,
23
- headers: { ...headers(), ...options.headers },
24
- });
25
- if (!res.ok) {
26
- const body = await res.text().catch(() => "");
27
- throw new Error(`HTTP ${res.status} ${res.statusText}: ${body}`);
28
- }
29
- if (res.status === 204) return null;
30
- const text = await res.text();
31
- return text ? JSON.parse(text) : null;
32
- }
33
-
34
- // ===== 代理相关 =====
35
-
36
- /** 获取所有代理(含代理组和节点) */
37
- export async function getProxies() {
38
- return request("/proxies");
39
- }
40
-
41
- /** 获取单个代理/组详情 */
42
- export async function getProxy(name) {
43
- return request(`/proxies/${encodeURIComponent(name)}`);
44
- }
45
-
46
- /** 切换代理组的选中节点 */
47
- export async function switchProxy(groupName, nodeName) {
48
- return request(`/proxies/${encodeURIComponent(groupName)}`, {
49
- method: "PUT",
50
- body: JSON.stringify({ name: nodeName }),
51
- });
52
- }
53
-
54
- /**
55
- * 获取所有代理组(Selector/URLTest/Fallback/LoadBalance 类型)
56
- * 返回: [{ name, type, now, all }]
57
- */
58
- export async function getGroups() {
59
- const data = await getProxies();
60
- const proxies = data.proxies || {};
61
- const groups = [];
62
- const groupTypes = new Set([
63
- "Selector",
64
- "URLTest",
65
- "Fallback",
66
- "LoadBalance",
67
- ]);
68
-
69
- for (const [name, info] of Object.entries(proxies)) {
70
- if (groupTypes.has(info.type)) {
71
- groups.push({
72
- name,
73
- type: info.type,
74
- now: info.now || "",
75
- all: info.all || [],
76
- });
77
- }
78
- }
79
- return groups;
80
- }
81
-
82
- /**
83
- * 获取所有独立节点(非代理组)
84
- * 返回: [{ name, type, alive, delay }]
85
- */
86
- export async function getNodes() {
87
- const data = await getProxies();
88
- const proxies = data.proxies || {};
89
- const nodes = [];
90
- const groupTypes = new Set([
91
- "Selector",
92
- "URLTest",
93
- "Fallback",
94
- "LoadBalance",
95
- ]);
96
-
97
- for (const [name, info] of Object.entries(proxies)) {
98
- if (!groupTypes.has(info.type)) {
99
- nodes.push({
100
- name,
101
- type: info.type,
102
- alive: info.alive ?? false,
103
- delay:
104
- info.history?.length > 0
105
- ? info.history[info.history.length - 1].delay
106
- : null,
107
- udp: info.udp ?? false,
108
- });
109
- }
110
- }
111
- return nodes;
112
- }
113
-
114
- // ===== 配置相关 =====
115
-
116
- /** 获取当前运行配置 */
117
- export async function getConfigs() {
118
- return request("/configs");
119
- }
120
-
121
- /** 更新运行配置(部分更新) */
122
- export async function patchConfigs(patch) {
123
- return request("/configs", {
124
- method: "PATCH",
125
- body: JSON.stringify(patch),
126
- });
127
- }
128
-
129
- /** 强制重载配置 */
130
- export async function reloadConfigs() {
131
- return request("/configs?force=true", { method: "PUT" });
132
- }
133
-
134
- // ===== 系统 =====
135
-
136
- /** 获取内核版本 */
137
- export async function getVersion() {
138
- return request("/version");
139
- }
140
-
141
- /** 获取内存使用 */
142
- export async function getMemory() {
143
- return request("/memory");
144
- }
145
-
146
- // ===== 连接 =====
147
-
148
- /** 获取当前活跃连接 */
149
- export async function getConnections() {
150
- return request("/connections");
151
- }
152
-
153
- /** 关闭指定连接 */
154
- export async function closeConnection(id) {
155
- return request(`/connections/${id}`, { method: "DELETE" });
156
- }
157
-
158
- /** 关闭所有连接 */
159
- export async function closeAllConnections() {
160
- return request("/connections", { method: "DELETE" });
161
- }
162
-
163
- // ===== 规则 =====
164
-
165
- /** 获取规则列表 */
166
- export async function getRules() {
167
- return request("/rules");
168
- }
169
-
170
- // ===== 代理提供者 =====
171
-
172
- /** 获取代理提供者列表 */
173
- export async function getProxyProviders() {
174
- return request("/providers/proxies");
175
- }
176
-
177
- /** 更新代理提供者 */
178
- export async function updateProxyProvider(name) {
179
- return request(`/providers/proxies/${encodeURIComponent(name)}`, {
180
- method: "PUT",
181
- });
182
- }
183
-
184
- /** 代理提供者健康检查 */
185
- export async function healthCheckProvider(name) {
186
- return request(`/providers/proxies/${encodeURIComponent(name)}/healthcheck`);
187
- }
188
-
189
- // ===== 便捷函数 =====
190
-
191
- /**
192
- * 获取指定组当前选中节点
193
- */
194
- export async function getCurrentNode(groupName) {
195
- const proxy = await getProxy(groupName);
196
- return proxy.now || null;
197
- }
198
-
199
- /**
200
- * 列出指定组的所有可选节点
201
- */
202
- export async function listGroupNodes(groupName) {
203
- const proxy = await getProxy(groupName);
204
- return proxy.all || [];
205
- }
206
-
207
- /**
208
- * 获取所有存活节点(按延迟排序)
209
- */
210
- export async function getAliveNodes() {
211
- const nodes = await getNodes();
212
- return nodes
213
- .filter((n) => n.alive && n.delay !== null && n.delay > 0)
214
- .sort((a, b) => (a.delay ?? Infinity) - (b.delay ?? Infinity));
215
- }
216
-
217
- /**
218
- * 切换到延迟最低的存活节点
219
- */
220
- export async function switchToFastest(groupName) {
221
- const alive = await getAliveNodes();
222
- if (alive.length === 0) {
223
- throw new Error("没有存活的节点");
224
- }
225
- // 需要确认目标节点在该组的 all 列表中
226
- const groupNodes = await listGroupNodes(groupName);
227
- for (const node of alive) {
228
- if (groupNodes.includes(node.name)) {
229
- await switchProxy(groupName, node.name);
230
- return node;
231
- }
232
- }
233
- throw new Error("存活节点都不在目标组中");
234
- }
5
+ export * from "../../api.js";
@@ -22,6 +22,8 @@ import * as api from "./api.js";
22
22
 
23
23
  const IS_WIN = process.platform === "win32";
24
24
  const HEALTHY_FILE = join(tmpdir(), "clash-healthy-nodes.json");
25
+ // 多控制端场景下按代理端口分文件(0/默认 = 单客户端旧文件);由 applyDiscovered 切换
26
+ let _healthyFile = HEALTHY_FILE;
25
27
 
26
28
  export const CONFIG = {
27
29
  testUrl: "https://www.tiktok.com/explore",
@@ -36,22 +38,27 @@ export const CONFIG = {
36
38
  const CURL_UA =
37
39
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
38
40
 
41
+ // 支持的 mihomo 系客户端进程名(FR-8 存活判定扩展)
42
+ const CLASH_PROCESS_NAMES = ["verge-mihomo", "69cloud-core"];
43
+
44
+ // 国家匹配:emoji 国旗可选(Clash Verge 节点名带国旗,69cloud 节点名纯中文/英文不带),
45
+ // 拉丁代码用词边界防误匹配(如 \bUS\b 不匹配 PLUS)
39
46
  const COUNTRY_PATTERNS = {
40
- US: /[🇺🇸]\s*(美国|US|USA|United\s*States)/i,
41
- GB: /[🇬🇧]\s*(英国|UK|GB|United\s*Kingdom)/i,
42
- DE: /[🇩🇪]\s*(德国|DE|Germany)/i,
43
- FR: /[🇫🇷]\s*(法国|FR|France)/i,
44
- JP: /[🇯🇵]\s*(日本|JP|Japan)/i,
45
- KR: /[🇰🇷]\s*(韩国|KR|Korea)/i,
46
- SG: /[🇸🇬]\s*(新加坡|SG|Singapore)/i,
47
- HK: /[🇭🇰]\s*(香港|HK|Hong\s*Kong)/i,
48
- TW: /[🇹🇼]\s*(台湾|TW|Taiwan)/i,
49
- RU: /[🇷🇺]\s*(俄罗斯|RU|Russia)/i,
50
- NL: /[🇳🇱]\s*(荷兰|NL|Netherlands)/i,
51
- IN: /[🇮🇳]\s*(印度|IN|India)/i,
52
- CA: /[🇨🇦]\s*(加拿大|CA|Canada)/i,
53
- AU: /[🇦🇺]\s*(澳大利亚|AU|Australia)/i,
54
- MY: /[🇲🇾]\s*(马来西亚|MY|Malaysia)/i,
47
+ US: /(?:[🇺🇸]\s*)?(美国|USA|United\s*States|\bUS\b)/i,
48
+ GB: /(?:[🇬🇧]\s*)?(英国|UK|United\s*Kingdom|\bGB\b)/i,
49
+ DE: /(?:[🇩🇪]\s*)?(德国|Germany|\bDE\b)/i,
50
+ FR: /(?:[🇫🇷]\s*)?(法国|France|\bFR\b)/i,
51
+ JP: /(?:[🇯🇵]\s*)?(日本|Japan|\bJP\b)/i,
52
+ KR: /(?:[🇰🇷]\s*)?(韩国|Korea|\bKR\b)/i,
53
+ SG: /(?:[🇸🇬]\s*)?(新加坡|Singapore|\bSG\b)/i,
54
+ HK: /(?:[🇭🇰]\s*)?(香港|Hong\s*Kong|\bHK\b)/i,
55
+ TW: /(?:[🇹🇼]\s*)?(台湾|Taiwan|\bTW\b)/i,
56
+ RU: /(?:[🇷🇺]\s*)?(俄罗斯|Russia|\bRU\b)/i,
57
+ NL: /(?:[🇳🇱]\s*)?(荷兰|Netherlands|\bNL\b)/i,
58
+ IN: /(?:[🇮🇳]\s*)?(印度|India|\bIN\b)/i,
59
+ CA: /(?:[🇨🇦]\s*)?(加拿大|Canada|\bCA\b)/i,
60
+ AU: /(?:[🇦🇺]\s*)?(澳大利亚|Australia|\bAU\b)/i,
61
+ MY: /(?:[🇲🇾]\s*)?(马来西亚|Malaysia|\bMY\b)/i,
55
62
  };
56
63
 
57
64
  let _proxyResolved = false;
@@ -73,6 +80,26 @@ export function resetProxyUrlCache() {
73
80
  _proxyResolved = false;
74
81
  }
75
82
 
83
+ /**
84
+ * 应用自动发现结果(discover.js)
85
+ * 控制端口写入 CONFIG.apiPort;代理端口直接采用发现到的 mixed-port,
86
+ * 避免 resolveProxyUrl() 再对旧端口发起探测。
87
+ * 多控制端场景:proxyPort 用于切换对应控制端的健康节点列表文件。
88
+ * @param {{port?:number, proxyUrl?:string, proxyPort?:number}} disc
89
+ */
90
+ export function applyDiscovered(disc = {}) {
91
+ if (disc.port) CONFIG.apiPort = disc.port;
92
+ if (disc.proxyUrl) {
93
+ CONFIG.proxyUrl = disc.proxyUrl;
94
+ _proxyResolved = true;
95
+ }
96
+ if (disc.proxyPort) {
97
+ _healthyFile = join(tmpdir(), `clash-healthy-nodes-${disc.proxyPort}.json`);
98
+ } else {
99
+ _healthyFile = HEALTHY_FILE;
100
+ }
101
+ }
102
+
76
103
  // ══════════════════════════════════════════════
77
104
  // 2. 进程 / API 可达性检测
78
105
  // ══════════════════════════════════════════════
@@ -104,19 +131,25 @@ export function checkClashAliveWithReason() {
104
131
  function _detectProcess() {
105
132
  try {
106
133
  if (IS_WIN) {
107
- const r = spawnSync(
108
- "tasklist",
109
- ["/FI", "IMAGENAME eq verge-mihomo.exe", "/NH"],
110
- { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
111
- );
112
- return r.stdout?.includes("verge-mihomo.exe");
134
+ for (const name of CLASH_PROCESS_NAMES) {
135
+ const r = spawnSync(
136
+ "tasklist",
137
+ ["/FI", `IMAGENAME eq ${name}.exe`, "/NH"],
138
+ { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] },
139
+ );
140
+ if (r.stdout?.includes(`${name}.exe`)) return true;
141
+ }
142
+ return false;
113
143
  }
114
- const r = spawnSync("pgrep", ["-x", "verge-mihomo"], {
115
- encoding: "utf-8",
116
- timeout: 5000,
117
- stdio: ["pipe", "pipe", "pipe"],
118
- });
119
- return r.status === 0 && !!r.stdout.trim();
144
+ for (const name of CLASH_PROCESS_NAMES) {
145
+ const r = spawnSync("pgrep", ["-x", name], {
146
+ encoding: "utf-8",
147
+ timeout: 5000,
148
+ stdio: ["pipe", "pipe", "pipe"],
149
+ });
150
+ if (r.status === 0 && r.stdout.trim()) return true;
151
+ }
152
+ return false;
120
153
  } catch {
121
154
  return false;
122
155
  }
@@ -367,8 +400,8 @@ export async function runDelayProbe({
367
400
  */
368
401
  export function loadHealthyList(country = null) {
369
402
  try {
370
- if (!existsSync(HEALTHY_FILE)) return _emptyList();
371
- const d = JSON.parse(readFileSync(HEALTHY_FILE, "utf-8"));
403
+ if (!existsSync(_healthyFile)) return _emptyList();
404
+ const d = JSON.parse(readFileSync(_healthyFile, "utf-8"));
372
405
  const allNodes = Array.isArray(d.nodes) ? d.nodes : [];
373
406
  const filter = country || CONFIG.nodeCountryFilter;
374
407
  const { filtered, excluded } = filterNodesByCountry(allNodes, filter);
@@ -388,7 +421,7 @@ export function loadHealthyList(country = null) {
388
421
  export function saveHealthyList(nodes) {
389
422
  try {
390
423
  writeFileSync(
391
- HEALTHY_FILE,
424
+ _healthyFile,
392
425
  JSON.stringify({ updated: new Date().toISOString(), nodes }, null, 2),
393
426
  "utf-8",
394
427
  );