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.
@@ -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
+ }
@@ -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
- let resolvedProxy = effectiveProxy;
145
- if (
146
- !effectiveProxy ||
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 &&
@@ -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
  }
@@ -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 {