tt-help-cli-ycl 1.4.115 → 1.4.119
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 +1 -1
- package/scripts/test-clash-discover.mjs +126 -0
- package/scripts/test-clash-multichannel.mjs +120 -0
- package/src/clash/api.js +29 -3
- package/src/clash/benchmark/lib/clash-api.js +3 -232
- package/src/clash/checker.js +63 -30
- package/src/clash/daemon.js +285 -108
- package/src/clash/discover.js +451 -0
- package/src/clash/ipc.js +65 -20
- package/src/cli/clash-daemon.js +10 -1
- package/src/cli/login-check.js +309 -0
- package/src/cli/refresh-mini.js +36 -24
- package/src/cli/watchdog.js +2 -0
- package/src/lib/args.js +81 -0
- package/src/lib/constants.js +17 -1
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +34 -3
- package/src/watch/public/app.js +218 -21
- package/src/watch/public/index.html +20 -1
- package/src/watch/watchdog-server.js +90 -4
- package/src/watchdog/agent.js +166 -7
- package/src/watchdog/reporter.js +32 -1
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleLoginCheck — 批量检测 CDP 端口对应浏览器是否登录 TikTok
|
|
3
|
+
*
|
|
4
|
+
* 流程(每端口):
|
|
5
|
+
* 1. 探测端口是否已开(未开 → 标记 launched)
|
|
6
|
+
* 2. ensureBrowserReady:未开自动启动 Edge(带代理),已开直接 CDP 连接
|
|
7
|
+
* 3. 新开 target 检测(不动其他程序正在使用的页面)
|
|
8
|
+
* 4. 导航 tiktok.com → passport/web/account/info 主检测:
|
|
9
|
+
* - 返回 username(登录号)→ 已登录(一步拿到账号)
|
|
10
|
+
* - HTTP 200 无账号数据 → 未登录
|
|
11
|
+
* - 异常/非 200 → 回退 isLoggedIn(cookie sessionid + DOM 验真)
|
|
12
|
+
* 5. 关闭自己开的 target;本次启动的浏览器 kill 进程,已启动的仅断 CDP
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import http from "http";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import path from "path";
|
|
18
|
+
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
19
|
+
import { isLoggedIn } from "../lib/browser/page.js";
|
|
20
|
+
import { proxy as configuredProxy } from "../lib/constants.js";
|
|
21
|
+
import { writeJson } from "./utils.js";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_BASE_PORT = 9222;
|
|
24
|
+
const DEFAULT_PORT_COUNT = 10;
|
|
25
|
+
const STARTUP_TIKTOK_URL = "https://www.tiktok.com/";
|
|
26
|
+
const ACCOUNT_INFO_URL = "https://www.tiktok.com/passport/web/account/info/";
|
|
27
|
+
const GOTO_TIMEOUT = 30000;
|
|
28
|
+
const ACCOUNT_INFO_TIMEOUT = 10000;
|
|
29
|
+
|
|
30
|
+
function checkPortOpen(port) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const req = http.get(`http://127.0.0.1:${port}/json`, (res) => {
|
|
33
|
+
res.on("data", () => {});
|
|
34
|
+
res.on("end", () => resolve(res.statusCode === 200));
|
|
35
|
+
});
|
|
36
|
+
req.on("error", () => resolve(false));
|
|
37
|
+
req.setTimeout(3000, () => {
|
|
38
|
+
resolve(false);
|
|
39
|
+
req.destroy();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function userDataDirForPort(port) {
|
|
45
|
+
// 与 open.js / explore.js 固定端口模式一致:每个端口独立 _p{port} 目录(含 9222)
|
|
46
|
+
return path.join(
|
|
47
|
+
os.homedir(),
|
|
48
|
+
"Library",
|
|
49
|
+
"Application Support",
|
|
50
|
+
`Microsoft Edge For Testing_p${port}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 通过 passport/web/account/info 判断登录并获取账号
|
|
56
|
+
* 在页面上下文执行 fetch,自动携带浏览器 cookie
|
|
57
|
+
* 返回结构(已登录): data.data = { username(登录号), screen_name(昵称), user_id, sec_user_id, ... }
|
|
58
|
+
* @returns {{httpStatus: number, username: string|null, screenName: string|null, hasAccount: boolean, error?: string}}
|
|
59
|
+
*/
|
|
60
|
+
async function checkByAccountInfo(page) {
|
|
61
|
+
try {
|
|
62
|
+
return await page.evaluate(
|
|
63
|
+
async ({ url, timeout }) => {
|
|
64
|
+
try {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
67
|
+
const res = await fetch(url, {
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
credentials: "include",
|
|
70
|
+
headers: { accept: "application/json" },
|
|
71
|
+
});
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
let data = null;
|
|
74
|
+
try {
|
|
75
|
+
data = await res.json();
|
|
76
|
+
} catch {
|
|
77
|
+
// 非 JSON 响应(如验证码页面),按异常处理
|
|
78
|
+
}
|
|
79
|
+
const d = data?.data || null;
|
|
80
|
+
return {
|
|
81
|
+
httpStatus: res.status,
|
|
82
|
+
// 登录号:username 优先(新版结构),unique_id 兼容旧结构
|
|
83
|
+
username: d?.username || d?.unique_id || null,
|
|
84
|
+
screenName: d?.screen_name || null,
|
|
85
|
+
hasAccount: !!d && !!(d.user_id || d.sec_user_id),
|
|
86
|
+
// 服务端明确报会话过期(未登录时同样返回该错误,需结合 cookie 区分)
|
|
87
|
+
expired: d?.name === "session_expired" || d?.error_code === 13,
|
|
88
|
+
};
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return {
|
|
91
|
+
httpStatus: 0,
|
|
92
|
+
username: null,
|
|
93
|
+
screenName: null,
|
|
94
|
+
hasAccount: false,
|
|
95
|
+
expired: false,
|
|
96
|
+
error: e?.message || String(e),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
{ url: ACCOUNT_INFO_URL, timeout: ACCOUNT_INFO_TIMEOUT },
|
|
101
|
+
);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return {
|
|
104
|
+
httpStatus: 0,
|
|
105
|
+
username: null,
|
|
106
|
+
screenName: null,
|
|
107
|
+
hasAccount: false,
|
|
108
|
+
expired: false,
|
|
109
|
+
error: e?.message || String(e),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function hasSessionIdCookie(page) {
|
|
115
|
+
try {
|
|
116
|
+
const cookies = await page.context().cookies("https://www.tiktok.com");
|
|
117
|
+
return cookies.some((c) => c.name === "sessionid");
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function checkOnePort(port, proxyServer) {
|
|
124
|
+
const result = {
|
|
125
|
+
port,
|
|
126
|
+
status: "unknown", // logged_in | logged_out | error
|
|
127
|
+
account: null,
|
|
128
|
+
screenName: null,
|
|
129
|
+
launched: false, // 本次命令启动的浏览器
|
|
130
|
+
fallback: false, // 是否走了 DOM 回退
|
|
131
|
+
expired: false, // session 是否已过期
|
|
132
|
+
message: "",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
let browser = null;
|
|
136
|
+
let page = null;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const wasOpen = await checkPortOpen(port);
|
|
140
|
+
result.launched = !wasOpen;
|
|
141
|
+
if (!wasOpen) {
|
|
142
|
+
console.error(`[端口 ${port}] 未运行,启动浏览器探测...`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cdpOptions = {
|
|
146
|
+
port,
|
|
147
|
+
// 显式传 userDataDir,确保 9222 也走 _p9222(与 open/explore 一致)
|
|
148
|
+
userDataDir: userDataDirForPort(port),
|
|
149
|
+
};
|
|
150
|
+
if (proxyServer) cdpOptions.proxyServer = proxyServer;
|
|
151
|
+
|
|
152
|
+
// Edge 刚启动时 CDP 可能瞬时异常(setDownloadBehavior),重试连接
|
|
153
|
+
for (let attempt = 1; ; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
browser = await ensureBrowserReady(cdpOptions);
|
|
156
|
+
break;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
if (e.message?.includes("setDownloadBehavior") && attempt < 3) {
|
|
159
|
+
console.error(
|
|
160
|
+
`[端口 ${port}] CDP 连接异常,等待后重试 ${attempt}/3...`,
|
|
161
|
+
);
|
|
162
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
throw e;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const ctx = browser.contexts()[0];
|
|
170
|
+
if (!ctx) throw new Error("浏览器无可用 context");
|
|
171
|
+
page = await ctx.newPage();
|
|
172
|
+
|
|
173
|
+
await page.goto(STARTUP_TIKTOK_URL, {
|
|
174
|
+
waitUntil: "domcontentloaded",
|
|
175
|
+
timeout: GOTO_TIMEOUT,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const info = await checkByAccountInfo(page);
|
|
179
|
+
if (info.username) {
|
|
180
|
+
result.status = "logged_in";
|
|
181
|
+
result.account = info.username;
|
|
182
|
+
result.screenName = info.screenName;
|
|
183
|
+
} else if (info.expired && info.httpStatus === 200) {
|
|
184
|
+
// 服务端报 session 过期:结合浏览器是否还持有 sessionid cookie 区分
|
|
185
|
+
// 有 cookie → 曾登录但已过期;无 cookie → 从未登录
|
|
186
|
+
const hasSession = await hasSessionIdCookie(page);
|
|
187
|
+
if (hasSession) {
|
|
188
|
+
result.status = "expired";
|
|
189
|
+
result.expired = true;
|
|
190
|
+
} else {
|
|
191
|
+
result.status = "logged_out";
|
|
192
|
+
}
|
|
193
|
+
} else if (info.httpStatus === 200 && !info.error) {
|
|
194
|
+
if (info.hasAccount && info.screenName) {
|
|
195
|
+
// 有账号数据但缺 username 字段(结构异常)→ 用昵称兜底
|
|
196
|
+
result.status = "logged_in";
|
|
197
|
+
result.screenName = info.screenName;
|
|
198
|
+
} else {
|
|
199
|
+
// 接口正常返回但没有账号数据 → 明确未登录
|
|
200
|
+
result.status = "logged_out";
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// 接口异常(风控/网络/非 JSON)→ 回退 cookie + DOM 检测
|
|
204
|
+
result.fallback = true;
|
|
205
|
+
console.error(
|
|
206
|
+
`[端口 ${port}] account/info 不可用(${info.error || "HTTP " + info.httpStatus}),回退 cookie+DOM 检测...`,
|
|
207
|
+
);
|
|
208
|
+
result.status = (await isLoggedIn(page)) ? "logged_in" : "logged_out";
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
result.status = "error";
|
|
212
|
+
result.message = e?.message || String(e);
|
|
213
|
+
} finally {
|
|
214
|
+
try {
|
|
215
|
+
if (page) await page.close().catch(() => {});
|
|
216
|
+
} catch {
|
|
217
|
+
// ignore
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
if (browser) await browser.close().catch(() => {});
|
|
221
|
+
} catch {
|
|
222
|
+
// ignore
|
|
223
|
+
}
|
|
224
|
+
if (result.launched) {
|
|
225
|
+
console.error(`[端口 ${port}] 关闭探测用浏览器...`);
|
|
226
|
+
await killEdgeProcesses(userDataDirForPort(port), port);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function printResult(r) {
|
|
233
|
+
const statusText = {
|
|
234
|
+
logged_in: "已登录",
|
|
235
|
+
logged_out: "未登录",
|
|
236
|
+
expired: "已过期",
|
|
237
|
+
error: "探测失败",
|
|
238
|
+
}[r.status];
|
|
239
|
+
const accountText =
|
|
240
|
+
r.status === "logged_in" && r.account ? `@${r.account}` : "-";
|
|
241
|
+
const nameText =
|
|
242
|
+
r.status === "logged_in" && r.screenName && r.screenName !== r.account
|
|
243
|
+
? ` (${r.screenName})`
|
|
244
|
+
: "";
|
|
245
|
+
const extras = [];
|
|
246
|
+
if (r.launched) extras.push("启动探测");
|
|
247
|
+
if (r.expired) extras.push("session过期");
|
|
248
|
+
if (r.fallback) extras.push("DOM回退");
|
|
249
|
+
if (r.message) extras.push(r.message);
|
|
250
|
+
const extraText = extras.length > 0 ? ` [${extras.join(", ")}]` : "";
|
|
251
|
+
console.log(
|
|
252
|
+
`${String(r.port).padEnd(5)} ${statusText.padEnd(6)} ${accountText}${nameText}${extraText}`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function handleLoginCheck(options = {}) {
|
|
257
|
+
const {
|
|
258
|
+
loginCheckPorts = null,
|
|
259
|
+
loginCheckJson = false,
|
|
260
|
+
loginCheckProxy = null,
|
|
261
|
+
loginCheckConcurrency = 1,
|
|
262
|
+
} = options;
|
|
263
|
+
|
|
264
|
+
const proxyServer = loginCheckProxy || configuredProxy;
|
|
265
|
+
if (proxyServer) {
|
|
266
|
+
console.error(`代理: ${proxyServer}`);
|
|
267
|
+
} else {
|
|
268
|
+
console.error(`代理: 未配置`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const ports =
|
|
272
|
+
loginCheckPorts && loginCheckPorts.length > 0
|
|
273
|
+
? [...new Set(loginCheckPorts)]
|
|
274
|
+
: Array.from(
|
|
275
|
+
{ length: DEFAULT_PORT_COUNT },
|
|
276
|
+
(_, i) => DEFAULT_BASE_PORT + i,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const concurrency = Math.max(
|
|
280
|
+
1,
|
|
281
|
+
Math.min(loginCheckConcurrency || 1, ports.length),
|
|
282
|
+
);
|
|
283
|
+
console.error(`检测端口: ${ports.join(", ")}(并发 ${concurrency})`);
|
|
284
|
+
|
|
285
|
+
const results = new Map();
|
|
286
|
+
const queue = [...ports];
|
|
287
|
+
|
|
288
|
+
async function worker() {
|
|
289
|
+
while (queue.length > 0) {
|
|
290
|
+
const port = queue.shift();
|
|
291
|
+
const r = await checkOnePort(port, proxyServer);
|
|
292
|
+
results.set(port, r);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
297
|
+
|
|
298
|
+
const ordered = ports.map((p) => results.get(p)).filter(Boolean);
|
|
299
|
+
|
|
300
|
+
if (loginCheckJson) {
|
|
301
|
+
writeJson(ordered, null);
|
|
302
|
+
} else {
|
|
303
|
+
console.log(`\n端口 状态 账号`);
|
|
304
|
+
for (const r of ordered) printResult(r);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const failed = ordered.filter((r) => r.status === "error");
|
|
308
|
+
if (failed.length > 0) process.exitCode = 2;
|
|
309
|
+
}
|
package/src/cli/refresh-mini.js
CHANGED
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
killEdgeProcesses,
|
|
38
38
|
} from "../lib/browser/cdp.js";
|
|
39
39
|
import { createApiClient } from "../lib/api-client.js";
|
|
40
|
-
import { requireClashOrExit } from "../clash/checker.js";
|
|
41
40
|
import { readHealth, writeCommand } from "../clash/ipc.js";
|
|
42
41
|
import { spawn } from "child_process";
|
|
43
42
|
import { fileURLToPath } from "url";
|
|
@@ -83,6 +82,19 @@ async function withRetry(label, fn) {
|
|
|
83
82
|
*/
|
|
84
83
|
let spawnedDaemonPid = null;
|
|
85
84
|
|
|
85
|
+
/** 从代理 URL 解析端口(http://127.0.0.1:7890 → 7890),无法解析返回 0(默认通道) */
|
|
86
|
+
function proxyPortOf(url) {
|
|
87
|
+
if (!url) return 0;
|
|
88
|
+
try {
|
|
89
|
+
const u = new URL(url);
|
|
90
|
+
const p = parseInt(u.port, 10);
|
|
91
|
+
return p > 0 ? p : 0;
|
|
92
|
+
} catch {
|
|
93
|
+
const m = String(url).match(/:(\d+)/);
|
|
94
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
function spawnClashDaemon() {
|
|
87
99
|
try {
|
|
88
100
|
const mainJs = path.join(__dirname, "..", "main.js");
|
|
@@ -115,10 +127,10 @@ function stopSpawnedDaemon() {
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/**
|
|
118
|
-
* 用 daemon 的 IPC 健康状态判断其是否存活(黑盒接口:clash-health.json
|
|
130
|
+
* 用 daemon 的 IPC 健康状态判断其是否存活(黑盒接口:clash-health.json,按通道端口)
|
|
119
131
|
*/
|
|
120
|
-
function isDaemonAlive() {
|
|
121
|
-
const h = readHealth();
|
|
132
|
+
function isDaemonAlive(port = 0) {
|
|
133
|
+
const h = readHealth(port);
|
|
122
134
|
return !!(h && !h.stale);
|
|
123
135
|
}
|
|
124
136
|
|
|
@@ -140,20 +152,13 @@ export async function handleRefreshMini(options) {
|
|
|
140
152
|
ensureDaemon,
|
|
141
153
|
} = options;
|
|
142
154
|
|
|
155
|
+
// 配置了什么代理就用什么代理(--proxy > ~/.tt-help.json 的 proxy);
|
|
156
|
+
// 未配置则不注入 proxyServer,浏览器走系统默认代理。
|
|
157
|
+
// 节点切换仍由 clash-daemon 负责(触发通知模式),不在此处自动解析。
|
|
143
158
|
const effectiveProxy = exploreProxy || configuredProxy;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
effectiveProxy.includes("127.0.0.1") ||
|
|
148
|
-
effectiveProxy.includes("localhost")
|
|
149
|
-
) {
|
|
150
|
-
// refresh-mini 必须走本地 Clash:未配置代理时自动发现,节点轮换才有意义
|
|
151
|
-
resolvedProxy = await requireClashOrExit("refresh-mini");
|
|
152
|
-
} else {
|
|
153
|
-
console.error(
|
|
154
|
-
`[警告] 使用非本地代理 ${effectiveProxy},Clash 节点轮换将无效`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
159
|
+
const resolvedProxy = effectiveProxy || null;
|
|
160
|
+
// 切换通知通道:按本实例的代理端口路由到 daemon 对应控制端
|
|
161
|
+
const proxyPort = proxyPortOf(resolvedProxy);
|
|
157
162
|
|
|
158
163
|
let userId = exploreUserId || configuredUserId;
|
|
159
164
|
let browser = null;
|
|
@@ -196,7 +201,7 @@ export async function handleRefreshMini(options) {
|
|
|
196
201
|
if (exploreJobLocations) console.error(`任务国家: ${exploreJobLocations}`);
|
|
197
202
|
console.error(`空闲间隔: ${exploreInterval || 30} 秒`);
|
|
198
203
|
console.error(
|
|
199
|
-
`代理: ${resolvedProxy || "
|
|
204
|
+
`代理: ${resolvedProxy || "系统默认"}${
|
|
200
205
|
rotateMin > 0
|
|
201
206
|
? `(每 ${rotateMin} 分钟轮换节点)`
|
|
202
207
|
: "(定时轮换已关闭)"
|
|
@@ -310,8 +315,8 @@ export async function handleRefreshMini(options) {
|
|
|
310
315
|
|
|
311
316
|
// --ensure-daemon:检查 clash-daemon 是否存活,未存活则拉起(默认不检测)
|
|
312
317
|
if (ensureDaemon) {
|
|
313
|
-
if (isDaemonAlive()) {
|
|
314
|
-
const h = readHealth();
|
|
318
|
+
if (isDaemonAlive(proxyPort)) {
|
|
319
|
+
const h = readHealth(proxyPort);
|
|
315
320
|
console.error(
|
|
316
321
|
`[daemon] 已运行(上次巡检 ${h?.ageSeconds ?? "?"}s 前)`,
|
|
317
322
|
);
|
|
@@ -339,16 +344,23 @@ export async function handleRefreshMini(options) {
|
|
|
339
344
|
proxySwitchCount++;
|
|
340
345
|
lastProxyTriggerTime = Date.now();
|
|
341
346
|
consecutiveTaskErrors = 0;
|
|
342
|
-
const before = readHealth()?.lastSwitch || "";
|
|
347
|
+
const before = readHealth(proxyPort)?.lastSwitch || "";
|
|
343
348
|
console.error(
|
|
344
|
-
`[代理] 触发切换通知(${reason},累计 ${proxySwitchCount} 次),等待 daemon 回执...`,
|
|
349
|
+
`[代理] 触发切换通知(${reason}${proxyPort ? `,通道 ${proxyPort}` : ""},累计 ${proxySwitchCount} 次),等待 daemon 回执...`,
|
|
350
|
+
);
|
|
351
|
+
writeCommand(
|
|
352
|
+
{
|
|
353
|
+
action: "failover",
|
|
354
|
+
reason: `refresh-mini: ${reason}`,
|
|
355
|
+
proxyPort: proxyPort || undefined,
|
|
356
|
+
},
|
|
357
|
+
proxyPort,
|
|
345
358
|
);
|
|
346
|
-
writeCommand({ action: "failover", reason: `refresh-mini: ${reason}` });
|
|
347
359
|
|
|
348
360
|
const deadline = Date.now() + SWITCH_RECEIPT_TIMEOUT_MS;
|
|
349
361
|
while (Date.now() < deadline) {
|
|
350
362
|
await sleep(3000);
|
|
351
|
-
const h = readHealth();
|
|
363
|
+
const h = readHealth(proxyPort);
|
|
352
364
|
if (
|
|
353
365
|
h &&
|
|
354
366
|
h.lastSwitch &&
|
package/src/cli/watchdog.js
CHANGED
|
@@ -20,6 +20,7 @@ export async function handleWatchdog(options) {
|
|
|
20
20
|
watchdogInterval,
|
|
21
21
|
watchdogClientId,
|
|
22
22
|
watchdogNoDaemon,
|
|
23
|
+
watchdogLoginIntervalSec,
|
|
23
24
|
} = options;
|
|
24
25
|
|
|
25
26
|
// 服务端地址:--server 参数 > ~/.tt-help.json watchdogServer > server > 默认值
|
|
@@ -58,6 +59,7 @@ export async function handleWatchdog(options) {
|
|
|
58
59
|
clientId,
|
|
59
60
|
intervalMs: intervalSec * 1000,
|
|
60
61
|
spawnDaemon: !watchdogNoDaemon,
|
|
62
|
+
loginCheckIntervalMs: (watchdogLoginIntervalSec || 7200) * 1000,
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
await agent.start();
|
package/src/lib/args.js
CHANGED
|
@@ -720,6 +720,7 @@ function parseWatchdogArgs(args) {
|
|
|
720
720
|
let watchdogInterval = 15;
|
|
721
721
|
let watchdogClientId = null;
|
|
722
722
|
let watchdogNoDaemon = false;
|
|
723
|
+
let watchdogLoginIntervalSec = 7200;
|
|
723
724
|
|
|
724
725
|
for (let i = 0; i < args.length; i++) {
|
|
725
726
|
const arg = args[i];
|
|
@@ -731,6 +732,8 @@ function parseWatchdogArgs(args) {
|
|
|
731
732
|
watchdogClientId = args[++i];
|
|
732
733
|
} else if (arg === "--no-daemon") {
|
|
733
734
|
watchdogNoDaemon = true;
|
|
735
|
+
} else if (arg === "--login-interval") {
|
|
736
|
+
watchdogLoginIntervalSec = parseInt(args[++i]) || 7200;
|
|
734
737
|
}
|
|
735
738
|
}
|
|
736
739
|
|
|
@@ -740,6 +743,7 @@ function parseWatchdogArgs(args) {
|
|
|
740
743
|
watchdogInterval,
|
|
741
744
|
watchdogClientId,
|
|
742
745
|
watchdogNoDaemon,
|
|
746
|
+
watchdogLoginIntervalSec,
|
|
743
747
|
urls: [],
|
|
744
748
|
outputFormat: "json",
|
|
745
749
|
exploreCount: 0,
|
|
@@ -758,6 +762,10 @@ function parseClashDaemonArgs(args) {
|
|
|
758
762
|
let benchmark = 10;
|
|
759
763
|
let dryRun = false;
|
|
760
764
|
let country = null;
|
|
765
|
+
let apiPort = null;
|
|
766
|
+
let secret = null;
|
|
767
|
+
let selection = null;
|
|
768
|
+
const configPaths = [];
|
|
761
769
|
|
|
762
770
|
for (let i = 0; i < args.length; i++) {
|
|
763
771
|
const arg = args[i];
|
|
@@ -769,6 +777,15 @@ function parseClashDaemonArgs(args) {
|
|
|
769
777
|
country = args[++i] || null;
|
|
770
778
|
} else if (arg === "--dry-run") {
|
|
771
779
|
dryRun = true;
|
|
780
|
+
} else if (arg === "--api-port") {
|
|
781
|
+
apiPort = parseInt(args[++i]) || 0;
|
|
782
|
+
} else if (arg === "--secret") {
|
|
783
|
+
secret = args[++i] || "";
|
|
784
|
+
} else if (arg === "--selection") {
|
|
785
|
+
selection = args[++i] || null;
|
|
786
|
+
} else if (arg === "--config-path") {
|
|
787
|
+
const p = args[++i];
|
|
788
|
+
if (p) configPaths.push(p);
|
|
772
789
|
}
|
|
773
790
|
}
|
|
774
791
|
|
|
@@ -778,6 +795,10 @@ function parseClashDaemonArgs(args) {
|
|
|
778
795
|
benchmark,
|
|
779
796
|
country,
|
|
780
797
|
"dry-run": dryRun,
|
|
798
|
+
"api-port": apiPort,
|
|
799
|
+
secret,
|
|
800
|
+
selection,
|
|
801
|
+
"config-paths": configPaths,
|
|
781
802
|
urls: [],
|
|
782
803
|
outputFormat: "json",
|
|
783
804
|
exploreCount: 0,
|
|
@@ -992,6 +1013,62 @@ function parsePrecheckArgs(args) {
|
|
|
992
1013
|
};
|
|
993
1014
|
}
|
|
994
1015
|
|
|
1016
|
+
function parsePortsArg(str) {
|
|
1017
|
+
const ports = [];
|
|
1018
|
+
for (const part of str.split(",")) {
|
|
1019
|
+
const p = part.trim();
|
|
1020
|
+
if (!p) continue;
|
|
1021
|
+
if (/^\d+-\d+$/.test(p)) {
|
|
1022
|
+
const [a, b] = p.split("-").map(Number);
|
|
1023
|
+
for (let x = Math.min(a, b); x <= Math.max(a, b); x++) ports.push(x);
|
|
1024
|
+
} else if (/^\d+$/.test(p)) {
|
|
1025
|
+
ports.push(Number(p));
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return ports;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function parseLoginCheckArgs(args) {
|
|
1032
|
+
let loginCheckPorts = null;
|
|
1033
|
+
let loginCheckJson = false;
|
|
1034
|
+
let loginCheckProxy = null;
|
|
1035
|
+
let loginCheckConcurrency = 1;
|
|
1036
|
+
|
|
1037
|
+
for (let i = 0; i < args.length; i++) {
|
|
1038
|
+
const arg = args[i];
|
|
1039
|
+
if (arg === "--json") {
|
|
1040
|
+
loginCheckJson = true;
|
|
1041
|
+
} else if (arg === "--proxy" && i + 1 < args.length) {
|
|
1042
|
+
loginCheckProxy = args[++i];
|
|
1043
|
+
} else if (
|
|
1044
|
+
(arg === "-c" || arg === "--concurrency") &&
|
|
1045
|
+
i + 1 < args.length
|
|
1046
|
+
) {
|
|
1047
|
+
loginCheckConcurrency = parseInt(args[++i], 10) || 1;
|
|
1048
|
+
} else if (!arg.startsWith("-")) {
|
|
1049
|
+
loginCheckPorts = parsePortsArg(arg);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return {
|
|
1054
|
+
subcommand: "login-check",
|
|
1055
|
+
loginCheckPorts,
|
|
1056
|
+
loginCheckJson,
|
|
1057
|
+
loginCheckProxy,
|
|
1058
|
+
loginCheckConcurrency,
|
|
1059
|
+
urls: [],
|
|
1060
|
+
outputFormat: "json",
|
|
1061
|
+
exploreCount: 0,
|
|
1062
|
+
showConfig: false,
|
|
1063
|
+
showHelp: false,
|
|
1064
|
+
customProxy: null,
|
|
1065
|
+
configAction: null,
|
|
1066
|
+
configValue: null,
|
|
1067
|
+
pipeMode: false,
|
|
1068
|
+
filterStr: null,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
995
1072
|
function parseOpenArgs(args) {
|
|
996
1073
|
let openPort = null;
|
|
997
1074
|
let openList = false;
|
|
@@ -1542,6 +1619,10 @@ export function parseArgs() {
|
|
|
1542
1619
|
return parseOpenArgs(args.slice(1));
|
|
1543
1620
|
}
|
|
1544
1621
|
|
|
1622
|
+
if (args.length > 0 && args[0] === "login-check") {
|
|
1623
|
+
return parseLoginCheckArgs(args.slice(1));
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1545
1626
|
if (args.length > 0 && args[0] === "comments") {
|
|
1546
1627
|
return parseCommentsArgs(args.slice(1));
|
|
1547
1628
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -262,6 +262,17 @@ const HELP_TEXT = [
|
|
|
262
262
|
" 示例: tt-help open 9222",
|
|
263
263
|
" tt-help open --list",
|
|
264
264
|
"",
|
|
265
|
+
" tt-help login-check [端口列表] [选项]",
|
|
266
|
+
" 批量检测 CDP 端口对应浏览器是否登录 TikTok,并返回登录账号",
|
|
267
|
+
" 端口列表: 逗号分隔,支持范围,如 9222,9223,9225-9227;默认 9222-9231",
|
|
268
|
+
" 未运行的端口自动启动浏览器探测,探测完成后关闭",
|
|
269
|
+
" 选项:",
|
|
270
|
+
" --proxy <地址> 浏览器代理,默认使用配置文件 proxy",
|
|
271
|
+
" -c, --concurrency <数量> 并发检测数,默认 1",
|
|
272
|
+
" --json 输出 JSON 格式",
|
|
273
|
+
" 示例: tt-help login-check 9222,9223,9224 --proxy http://127.0.0.1:7897",
|
|
274
|
+
" tt-help login-check --json",
|
|
275
|
+
"",
|
|
265
276
|
" videostats <db路径> -p <并发数>",
|
|
266
277
|
" 批量刷新视频统计数据(playCount, diggCount, commentCount, shareCount, collectCount)",
|
|
267
278
|
" 直接读取并更新 SQLite videos 表",
|
|
@@ -420,7 +431,7 @@ const HELP_TEXT = [
|
|
|
420
431
|
" tt-help watchdog -i 30 --client-id my-server-01",
|
|
421
432
|
"",
|
|
422
433
|
" tt-help clash-daemon [选项]",
|
|
423
|
-
" Clash Verge 代理节点健康守护:定时巡检 TikTok 可达性,异常时自动切换节点",
|
|
434
|
+
" Clash Verge / mihomo 代理节点健康守护:定时巡检 TikTok 可达性,异常时自动切换节点",
|
|
424
435
|
" 看门狗启动时会自动拉起此守护进程",
|
|
425
436
|
" 选项:",
|
|
426
437
|
" --interval <秒> 巡检间隔,默认 120 秒(2分钟)",
|
|
@@ -428,6 +439,11 @@ const HELP_TEXT = [
|
|
|
428
439
|
" --country <国家代码> 只保留指定国家节点,默认不过滤",
|
|
429
440
|
" 支持: US, GB, DE, FR, JP, KR, SG, HK, TW, RU 等",
|
|
430
441
|
" --dry-run 只检测不切换(调试用)",
|
|
442
|
+
" --api-port <端口> 控制 API 端口(默认自动发现;显式指定验证失败时报错)",
|
|
443
|
+
" --secret <secret> 控制 API 密钥(默认自动获取)",
|
|
444
|
+
" --selection <策略> 多控制端选择策略: system-proxy / preference / first-alive",
|
|
445
|
+
" --config-path <路径> 追加自定义 config.yaml 探测路径(可重复)",
|
|
446
|
+
" 控制端自动发现: 依次探测 Clash Verge / 69cloud 配置 → 端口枚举,按策略选定",
|
|
431
447
|
" 看门狗运行时可通过 /tmp/clash-cmd.json 发命令动态调整:",
|
|
432
448
|
' {"action":"switchNode","node":"节点名"} 一次性切换到指定节点(不干扰自检)',
|
|
433
449
|
' {"action":"setCountryFilter","country":"US"} 切换国家过滤',
|
package/src/main.js
CHANGED
|
@@ -26,6 +26,7 @@ import { handleRawSync } from "./cli/raw-sync.js";
|
|
|
26
26
|
import { handleSearch } from "./cli/search.js";
|
|
27
27
|
import { handleWatchdog } from "./cli/watchdog.js";
|
|
28
28
|
import { handleClashDaemon } from "./cli/clash-daemon.js";
|
|
29
|
+
import { handleLoginCheck } from "./cli/login-check.js";
|
|
29
30
|
|
|
30
31
|
async function main() {
|
|
31
32
|
const parsed = parseArgs();
|
|
@@ -77,6 +78,8 @@ async function main() {
|
|
|
77
78
|
return handleWatchdog(parsed);
|
|
78
79
|
case "clash-daemon":
|
|
79
80
|
return handleClashDaemon(parsed);
|
|
81
|
+
case "login-check":
|
|
82
|
+
return handleLoginCheck(parsed);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
const {
|
package/src/npm-main.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { handleRawSync } from "./cli/raw-sync.js";
|
|
23
23
|
import { handleSearch } from "./cli/search.js";
|
|
24
24
|
import { handleWatchdog } from "./cli/watchdog.js";
|
|
25
|
+
import { handleLoginCheck } from "./cli/login-check.js";
|
|
25
26
|
|
|
26
27
|
function exitUnsupportedCommand(command) {
|
|
27
28
|
console.error(
|
|
@@ -76,6 +77,8 @@ async function main() {
|
|
|
76
77
|
return handleSearch(parsed);
|
|
77
78
|
case "watchdog":
|
|
78
79
|
return handleWatchdog(parsed);
|
|
80
|
+
case "login-check":
|
|
81
|
+
return handleLoginCheck(parsed);
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
const {
|