tt-help-cli-ycl 1.4.178 → 1.4.181
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-login-pool-ui.mjs +13 -2
- package/scripts/test-protocol-bridge.mjs +162 -0
- package/scripts/verify-protocol-quote.mjs +35 -0
- package/src/cli/login-pool.js +78 -30
- package/src/cli/protocol-bridge.js +274 -0
- package/src/lib/args.js +23 -2
- package/src/lib/constants.js +9 -2
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/server.js +27 -7
package/package.json
CHANGED
|
@@ -157,13 +157,24 @@ check("状态徽章=使用中", row1.includes("使用中"));
|
|
|
157
157
|
check("使用者=机器:口", row1.includes("F-11:9222"));
|
|
158
158
|
const poolSrc = await page.content();
|
|
159
159
|
check("独立页无 cookie 明文", !poolSrc.includes("SECRET-SID-XYZ"));
|
|
160
|
-
check("
|
|
160
|
+
check("独立页检测按钮已点亮(重登录已改名)", await page.$$eval("#content button:not([disabled])", (bs) => bs.some((b) => b.textContent.trim() === "检测")));
|
|
161
161
|
check("独立页含新增账号/批量检测按钮", await page.$$eval(".bar button", (bs) => {
|
|
162
162
|
const t = bs.map((b) => b.textContent).join();
|
|
163
163
|
return t.includes("新增账号") && t.includes("批量检测");
|
|
164
164
|
}));
|
|
165
165
|
const barSrc = await page.content();
|
|
166
|
-
check("
|
|
166
|
+
check("页面含协议桥四件套(protoHref/launchProto/blur 探测/降级 copyCmd)",
|
|
167
|
+
["function protoHref", "function launchProto", "addEventListener('blur'", "copyCmd("].every((s) => barSrc.includes(s)),
|
|
168
|
+
);
|
|
169
|
+
const protoUrl = await page.evaluate(() => protoHref("login-pool/relogin", { server: location.origin, username: "userA" }));
|
|
170
|
+
check("protoHref 构造协议 URL 正确(参数全编码)",
|
|
171
|
+
protoUrl === "tt-help://login-pool/relogin?server=" + encodeURIComponent(BASE) + "&username=userA", `got ${protoUrl}`);
|
|
172
|
+
const fallbackHtml = await page.evaluate(async () => {
|
|
173
|
+
launchProto("tt-help://login-pool/add?server=x", "CMD-FALLBACK-TEST", "ok");
|
|
174
|
+
await new Promise((r) => setTimeout(r, 2700));
|
|
175
|
+
return document.getElementById("msg").innerHTML;
|
|
176
|
+
});
|
|
177
|
+
check("未注册降级:2.5s 后出现复制按钮且命令正确嵌入", fallbackHtml.includes("copyCmd('CMD-FALLBACK-TEST')"), fallbackHtml.slice(0, 120));
|
|
167
178
|
check("模拟取号/入池粘cookies按钮已移除", !barSrc.includes("模拟取号") && !barSrc.includes("入池(粘cookies)"));
|
|
168
179
|
check("自动刷新无条件化(无勾选框)", !barSrc.includes('id="auto"'));
|
|
169
180
|
check("统计 chips 存在", await page.$$eval("#stats .chip", (cs) => cs.length >= 5));
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议桥单元测试 — parseProtocolUrl 白名单/参数校验 + 双平台模板生成 + 单 token 别名路由
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* [1] URL 解析:合法样例(大小写 normalize / origin 归一 / 尾斜杠 / note 清洗)
|
|
6
|
+
* [2] URL 拒绝:白名单外 action / 缺参 / 非 http server / username 非法 / scheme 不对
|
|
7
|
+
* [3] 模板生成:AppleScript(osacompile 源)与 Windows 包装 cmd 的结构断言
|
|
8
|
+
* [4] 别名路由:protocol-handle 单 token(OS 拉起链路兼容;parseArgs 无参签名读 process.argv)
|
|
9
|
+
*
|
|
10
|
+
* 运行: node scripts/test-protocol-bridge.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { strict as assert } from "assert";
|
|
13
|
+
import { parseProtocolUrl, buildAppleScript, buildWindowsCmd } from "../src/cli/protocol-bridge.js";
|
|
14
|
+
import { parseArgs } from "../src/lib/args.js";
|
|
15
|
+
|
|
16
|
+
let pass = 0;
|
|
17
|
+
let fail = 0;
|
|
18
|
+
const check = (name, fn) => {
|
|
19
|
+
try {
|
|
20
|
+
fn();
|
|
21
|
+
pass++;
|
|
22
|
+
console.log(` ✓ ${name}`);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
fail++;
|
|
25
|
+
console.log(` ✗ ${name}: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ── [1] 合法样例 ──
|
|
30
|
+
console.log("[1] URL 解析(合法)");
|
|
31
|
+
check("add 最小样例:host/path 解析 + server 取 origin", () => {
|
|
32
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http%3A%2F%2F172.18.154.201%3A3001");
|
|
33
|
+
assert.equal(p.action, "login-pool/add");
|
|
34
|
+
assert.equal(p.serverUrl, "http://172.18.154.201:3001");
|
|
35
|
+
assert.equal(p.username, undefined);
|
|
36
|
+
});
|
|
37
|
+
check("hostname 大写自动 normalize(非特殊 scheme 不自动小写,实测坑)", () => {
|
|
38
|
+
const p = parseProtocolUrl("tt-help://LOGIN-POOL/RELOGIN?server=https://x.io&username=u1");
|
|
39
|
+
assert.equal(p.action, "login-pool/relogin");
|
|
40
|
+
});
|
|
41
|
+
check("pathname 尾斜杠归一", () => {
|
|
42
|
+
const p = parseProtocolUrl("tt-help://login-pool/add/?server=http://x.io");
|
|
43
|
+
assert.equal(p.action, "login-pool/add");
|
|
44
|
+
});
|
|
45
|
+
check("server 路径丢弃只留 origin", () => {
|
|
46
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http%3A%2F%2Fx.io%2Fevil%2Fpath%3Fq%3D1");
|
|
47
|
+
assert.equal(p.serverUrl, "http://x.io");
|
|
48
|
+
});
|
|
49
|
+
check("username 字符集:字母数字点下划线", () => {
|
|
50
|
+
const p = parseProtocolUrl("tt-help://login-pool/relogin?server=http://x.io&username=user_1.2");
|
|
51
|
+
assert.equal(p.username, "user_1.2");
|
|
52
|
+
});
|
|
53
|
+
check("note 控制符清洗 + 200 限长", () => {
|
|
54
|
+
const long = "a".repeat(500);
|
|
55
|
+
const p = parseProtocolUrl(
|
|
56
|
+
"tt-help://login-pool/add?server=http://x.io¬e=" +
|
|
57
|
+
encodeURIComponent("备\n注\x00" + long),
|
|
58
|
+
);
|
|
59
|
+
assert.equal(p.note, "备注" + "a".repeat(198));
|
|
60
|
+
assert.ok(!p.note.includes("\x00"));
|
|
61
|
+
});
|
|
62
|
+
check("未知参数静默忽略", () => {
|
|
63
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http://x.io&proxy=http://evil&dir=/etc");
|
|
64
|
+
assert.equal(p.proxy, undefined);
|
|
65
|
+
assert.equal(p.dir, undefined);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ── [2] 拒绝样例 ──
|
|
69
|
+
console.log("[2] URL 解析(拒绝)");
|
|
70
|
+
const rejects = [
|
|
71
|
+
["白名单外 action(check 不开放)", "tt-help://login-pool/check?server=http://x.io"],
|
|
72
|
+
["任意命令形 action(防任意执行入口)", "tt-help://config/set?server=http://x.io"],
|
|
73
|
+
["路径注入形 action", "tt-help://../../win.ini?server=http://x.io"],
|
|
74
|
+
["scheme 不对", "https://login-pool/add?server=http://x.io"],
|
|
75
|
+
["缺 server", "tt-help://login-pool/add"],
|
|
76
|
+
["server 非 http(s)", "tt-help://login-pool/add?server=ftp://x.io"],
|
|
77
|
+
["server 垃圾串", "tt-help://login-pool/add?server=not-a-url"],
|
|
78
|
+
["relogin 缺 username", "tt-help://login-pool/relogin?server=http://x.io"],
|
|
79
|
+
["username 空格", "tt-help://login-pool/relogin?server=http://x.io&username=a%20b"],
|
|
80
|
+
["username 命令注入字符", "tt-help://login-pool/relogin?server=http://x.io&username=a%3Brm"],
|
|
81
|
+
["username 超长", "tt-help://login-pool/relogin?server=http://x.io&username=" + "a".repeat(65)],
|
|
82
|
+
["空 URL", ""],
|
|
83
|
+
["null", null],
|
|
84
|
+
];
|
|
85
|
+
for (const [why, raw] of rejects) {
|
|
86
|
+
check(`拒绝: ${why}`, () => {
|
|
87
|
+
assert.throws(() => parseProtocolUrl(raw), Error);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── [3] 模板生成 ──
|
|
92
|
+
console.log("[3] 双平台模板生成");
|
|
93
|
+
const NODE = "/Users/x/.nvm/versions/node/v24.14.1/bin/node";
|
|
94
|
+
const CLI = "/Users/x/pkg/tt-help-cli-ycl/cli.js";
|
|
95
|
+
check("AppleScript: open location 处理器 + quoted form 拼接", () => {
|
|
96
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
97
|
+
assert.ok(src.startsWith("on open location theURL"), "必须有 open location 处理器");
|
|
98
|
+
assert.ok(src.includes("quoted form of theURL"), "URL 必须经 quoted form 传递");
|
|
99
|
+
assert.ok(src.includes("open -a Terminal"), "必须拉起 Terminal");
|
|
100
|
+
assert.ok(src.includes("'#!/bin/bash"), "必须生成 .command(shebang)");
|
|
101
|
+
});
|
|
102
|
+
check("AppleScript: 生成源可通过 osacompile 语法结构(do shell script 成对引号)", () => {
|
|
103
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
104
|
+
const m = src.match(/do shell script ("(?:[^"\\]|\\.)*")/);
|
|
105
|
+
assert.ok(m, "do shell script 后必须是合法 AppleScript 字符串字面量");
|
|
106
|
+
assert.ok(/& quoted form of theURL & "(?:[^"\\]|\\.)*"\s*\nend open location/.test(src), "拼接式必须完整闭合");
|
|
107
|
+
assert.ok(src.includes('protocol handle \\"%s\\"'), "URL 槽必须引号化(quoted form 引号会被生成侧 shell 消费)");
|
|
108
|
+
});
|
|
109
|
+
check("AppleScript: node 路径烧录且单引号包裹", () => {
|
|
110
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
111
|
+
assert.ok(src.includes(`'${NODE}'`), "node 绝对路径必须烧录");
|
|
112
|
+
assert.ok(src.includes(`'${CLI}'`), "cli.js 绝对路径必须烧录");
|
|
113
|
+
});
|
|
114
|
+
check("AppleScript: .command 用 protocol handle 两 token(对齐路由)", () => {
|
|
115
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
116
|
+
assert.ok(src.includes('protocol handle \\"%s\\"'), "必须 protocol handle 且 URL 槽引号化");
|
|
117
|
+
assert.ok(!src.includes("protocol-handle"), "不得残留单 token 旧形态");
|
|
118
|
+
});
|
|
119
|
+
check("AppleScript: .command 退出码保持 + 按键防闪退 + 自清理", () => {
|
|
120
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
121
|
+
assert.ok(src.includes("st=$?"), "必须捕获退出码");
|
|
122
|
+
assert.ok(src.includes("read -n 1"), "必须按键保持窗口");
|
|
123
|
+
assert.ok(src.includes("rm -f"), "必须自清理临时 .command");
|
|
124
|
+
});
|
|
125
|
+
check("Windows cmd: 绝对路径 + protocol handle %1 + pause", () => {
|
|
126
|
+
const cmd = buildWindowsCmd({ nodePath: "C:\\n\\node.exe", cliPath: "C:\\p\\cli.js" });
|
|
127
|
+
assert.ok(cmd.startsWith("@echo off"), "必须关回显");
|
|
128
|
+
assert.ok(cmd.includes('"C:\\n\\node.exe" "C:\\p\\cli.js" protocol handle "%~1"'), "URL 槽必须 %~1 再引号化");
|
|
129
|
+
assert.ok(!cmd.includes("protocol-handle"), "不得残留单 token 旧形态");
|
|
130
|
+
assert.ok(cmd.includes("pause"), "必须 pause 防闪退");
|
|
131
|
+
assert.ok(cmd.includes("exit /b %RC%"), "必须透传退出码");
|
|
132
|
+
assert.ok(cmd.includes("\r\n"), "必须是 CRLF");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ── [4] 单 token 别名路由 ──
|
|
136
|
+
console.log("[4] 别名路由(parseArgs 无参签名,读 process.argv)");
|
|
137
|
+
check("protocol-handle 单 token → protocol/handle(OS 拉起链路兼容)", () => {
|
|
138
|
+
const oldArgv = process.argv;
|
|
139
|
+
process.argv = ["node", "tt-help", "protocol-handle", "tt-help://login-pool/add?server=http://x.io"];
|
|
140
|
+
try {
|
|
141
|
+
const p = parseArgs();
|
|
142
|
+
assert.equal(p.subcommand, "protocol");
|
|
143
|
+
assert.equal(p.action, "handle");
|
|
144
|
+
assert.equal(p.url, "tt-help://login-pool/add?server=http://x.io");
|
|
145
|
+
} finally {
|
|
146
|
+
process.argv = oldArgv;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
check("protocol handle 两 token 仍正常", () => {
|
|
150
|
+
const oldArgv = process.argv;
|
|
151
|
+
process.argv = ["node", "tt-help", "protocol", "handle", "tt-help://login-pool/add?server=http://x.io"];
|
|
152
|
+
try {
|
|
153
|
+
const p = parseArgs();
|
|
154
|
+
assert.equal(p.subcommand, "protocol");
|
|
155
|
+
assert.equal(p.action, "handle");
|
|
156
|
+
} finally {
|
|
157
|
+
process.argv = oldArgv;
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
|
|
162
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 验证模板输出的 .command 里 URL 是否带引号(模拟 printf 行为)
|
|
3
|
+
import { buildAppleScript, buildWindowsCmd } from "../src/cli/protocol-bridge.js";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { strict as assert } from "assert";
|
|
7
|
+
|
|
8
|
+
const NODE = "/Users/x/.nvm/versions/node/v24.14.1/bin/node";
|
|
9
|
+
const CLI = "/Users/x/pkg/cli.js";
|
|
10
|
+
const URL = "tt-help://login-pool/relogin?server=http%3A%2F%2F117.71.53.99%3A17301&username=asset698";
|
|
11
|
+
|
|
12
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
13
|
+
|
|
14
|
+
// 从 AppleScript 源抽取 sh 命令(模拟 AppleScript 字符串求值:\" → ",\\ → \)
|
|
15
|
+
const m = src.match(/do shell script "((?:[^"\\]|\\.)*)" & quoted form of theURL & "((?:[^"\\]|\\.)*)"\n/);
|
|
16
|
+
assert.ok(m, "do shell script 拼接式结构完整");
|
|
17
|
+
const unesc = (s) => s.replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
18
|
+
const sh = unesc(m[1]) + "'" + URL + "'" + unesc(m[2]);
|
|
19
|
+
|
|
20
|
+
// 用 /bin/sh 真实执行 printf(macOS 上等价;这里在 Linux 模拟——printf POSIX 行为一致)
|
|
21
|
+
const out2 = execFileSync("/bin/sh", ["-c", sh.replace("open -a Terminal", "echo OPEN-TARGET:")], { encoding: "utf-8" });
|
|
22
|
+
const target = (out2.match(/OPEN-TARGET: (\S+)/) || [])[1];
|
|
23
|
+
assert.ok(target, "未取得 .command 路径");
|
|
24
|
+
const out = readFileSync(target, "utf-8");
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// 断言:URL 必须带引号出现在 .command 里
|
|
28
|
+
assert.ok(out.includes(`protocol handle "${URL}"`), "URL 必须被双引号包裹");
|
|
29
|
+
assert.ok(out.includes(`"${NODE}"`), "node 路径必须被双引号包裹");
|
|
30
|
+
assert.ok(out.includes(`"${CLI}"`), "cli 路径必须被双引号包裹");
|
|
31
|
+
console.log("✓ macOS:URL/路径均已引号化(& 不再截断命令)");
|
|
32
|
+
|
|
33
|
+
const w = buildWindowsCmd({ nodePath: "C:\\n\\node.exe", cliPath: "C:\\p\\cli.js" });
|
|
34
|
+
assert.ok(w.includes('protocol handle "%~1"'), "Windows 必须用 %~1 再引号化");
|
|
35
|
+
console.log("✓ Windows:protocol handle \"%~1\"(引号恒在)");
|
package/src/cli/login-pool.js
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* add: 起独立 Edge(临时目录+代理)→ 人工在窗口里登录 TikTok → 轮询 passport 检出
|
|
10
10
|
* → 导出 cookies → POST /api/login-pool/import(幂等,同号跳过)
|
|
11
|
-
* relogin: POST /prepare 取池内旧 cookies → 注入本机 Edge
|
|
12
|
-
* →
|
|
11
|
+
* relogin: 检测:POST /prepare 取池内旧 cookies → 注入本机 Edge 打开账号当前登录态
|
|
12
|
+
* → 人工核对/退出重登(机器不做任何自动判定)→ 回车提交 → 身份校验 → POST /relogin 覆盖刷新
|
|
13
13
|
* check: 服务端无浏览器 HTTP 探测各号登录态(默认空闲+疑似),死号自动标疑似
|
|
14
14
|
*
|
|
15
15
|
* 协议桥(页面一键拉起本命令)为独立后续项,本命令即其执行端。
|
|
16
16
|
*/
|
|
17
17
|
import { mkdtempSync, rmSync } from "fs";
|
|
18
|
+
import { createInterface } from "readline";
|
|
18
19
|
import { tmpdir } from "os";
|
|
19
20
|
import { join } from "path";
|
|
20
21
|
import { chromium } from "playwright";
|
|
@@ -23,11 +24,37 @@ import { proxy as configuredProxy } from "../lib/constants.js";
|
|
|
23
24
|
|
|
24
25
|
const POLL_INTERVAL_MS = 3000;
|
|
25
26
|
const CDP_FALLBACK_NOTE = "(关掉窗口或 Ctrl+C 可中止)";
|
|
27
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
26
28
|
|
|
27
29
|
function log(t) {
|
|
28
30
|
console.error(`[登录池] ${t}`);
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/** 等待回车提交;stdin 关闭或超时则中止(不更新池)。返回 {promise, abort}:失败路径须 abort() 关闭监听,否则进程不退 */
|
|
34
|
+
function waitEnter(timeoutMs) {
|
|
35
|
+
let finish = null;
|
|
36
|
+
const promise = new Promise((resolve, reject) => {
|
|
37
|
+
const rl = createInterface({ input: process.stdin });
|
|
38
|
+
let done = false;
|
|
39
|
+
finish = (err) => {
|
|
40
|
+
if (done) return;
|
|
41
|
+
done = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
rl.close();
|
|
44
|
+
process.stdin.pause();
|
|
45
|
+
process.stdin.unref(); // 释放事件循环对 stdin 的引用:提交/中止后进程立即退出
|
|
46
|
+
err ? reject(err) : resolve();
|
|
47
|
+
};
|
|
48
|
+
const timer = setTimeout(
|
|
49
|
+
() => finish(new Error(`等待回车超时(${Math.round(timeoutMs / 1000)}s),已中止(不更新池)`)),
|
|
50
|
+
timeoutMs,
|
|
51
|
+
);
|
|
52
|
+
rl.once("line", () => finish(null));
|
|
53
|
+
rl.once("close", () => finish(new Error("stdin 已关闭,已中止(不更新池)")));
|
|
54
|
+
});
|
|
55
|
+
return { promise, abort: (msg) => finish && finish(new Error(msg || "已中止")) };
|
|
56
|
+
}
|
|
57
|
+
|
|
31
58
|
/** HTTP POST,默认 30s 超时(check 场景调用方自带更长超时) */
|
|
32
59
|
async function post(base, path, body, timeoutMs = 30 * 1000) {
|
|
33
60
|
const res = await fetch(`${String(base).replace(/\/+$/, "")}/api/login-pool/${path}`, {
|
|
@@ -106,45 +133,66 @@ async function cmdAdd(opts) {
|
|
|
106
133
|
}
|
|
107
134
|
}
|
|
108
135
|
|
|
109
|
-
/** relogin
|
|
136
|
+
/** relogin 子命令:检测——打开账号当前登录态,人工核对/退出重登后回车提交更新 */
|
|
110
137
|
async function cmdRelogin(opts) {
|
|
111
138
|
const { serverUrl, proxy, username, timeoutMs, dir } = opts;
|
|
112
139
|
if (!username) throw new Error("用法: tt-help login-pool relogin <username>");
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
name: c.name,
|
|
119
|
-
value: c.value,
|
|
120
|
-
domain: c.domain || ".tiktok.com",
|
|
121
|
-
path: c.path || "/",
|
|
122
|
-
secure: c.secure !== false,
|
|
123
|
-
httpOnly: c.httpOnly !== false,
|
|
124
|
-
...(typeof c.expires === "number" && c.expires > 0 ? { expires: c.expires } : {}),
|
|
125
|
-
}));
|
|
126
|
-
const session = await openEdgeSession({ proxy, dir });
|
|
140
|
+
// 尽早挂回车监听:fd0 在重活(Edge 启动/页面导航)期间可能被环境消费,迟挂会丢事件;
|
|
141
|
+
// 提前监听则回车随时可达,结果由下方 await 消费
|
|
142
|
+
const enter = waitEnter(timeoutMs);
|
|
143
|
+
enter.promise.catch(() => {}); // 防 unhandled rejection(真正消费在下方 await/abort)
|
|
144
|
+
let session = null;
|
|
127
145
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
146
|
+
const prep = await post(serverUrl, "prepare", { username });
|
|
147
|
+
const oldCookies = prep.account.cookies;
|
|
148
|
+
log(`目标 @${username}(池内 ${oldCookies.length} 条 cookies)。在窗口中打开当前登录态…`);
|
|
149
|
+
const toPlaywrightCookies = (cookies) =>
|
|
150
|
+
(cookies || []).map((c) => ({
|
|
151
|
+
name: c.name,
|
|
152
|
+
value: c.value,
|
|
153
|
+
domain: c.domain || ".tiktok.com",
|
|
154
|
+
path: c.path || "/",
|
|
155
|
+
secure: c.secure !== false,
|
|
156
|
+
httpOnly: c.httpOnly !== false,
|
|
157
|
+
...(typeof c.expires === "number" && c.expires > 0 ? { expires: c.expires } : {}),
|
|
158
|
+
}));
|
|
159
|
+
session = await openEdgeSession({ proxy, dir });
|
|
160
|
+
await session.context.addCookies(toPlaywrightCookies(oldCookies));
|
|
161
|
+
await gotoTiktok(session.page);
|
|
162
|
+
log(`已在窗口打开 @${username}。可保持不动 / 退出重登 / 直接处理异常(人工操作)`);
|
|
163
|
+
log(
|
|
164
|
+
`操作完成后回这里按回车,提交最新登录态(${Math.round(timeoutMs / 1000)}s 无操作自动中止;关窗口=中止)…`,
|
|
165
|
+
);
|
|
166
|
+
await enter.promise;
|
|
167
|
+
|
|
168
|
+
// 读提交时刻的登录态:页面可能在加载,未检出时短暂重试
|
|
169
|
+
let id = null;
|
|
170
|
+
for (let i = 0; i < 3; i++) {
|
|
171
|
+
id = await whoAmI(session.page);
|
|
172
|
+
if (id.hasAccount) break;
|
|
173
|
+
if (i < 2) {
|
|
174
|
+
log("未检测到登录态,3 秒后重试…");
|
|
175
|
+
await sleep(3000);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!id || !id.hasAccount) {
|
|
179
|
+
enter.abort();
|
|
180
|
+
throw new Error("窗口当前未登录(登出后未重登?),已中止(不更新池)");
|
|
137
181
|
}
|
|
138
182
|
if (id.username !== username) {
|
|
183
|
+
enter.abort();
|
|
139
184
|
throw new Error(`身份不符:窗口登录的是 @${id.username},目标是 @${username},已中止(不更新池)`);
|
|
140
185
|
}
|
|
141
186
|
const cookies = await session.context.cookies();
|
|
142
|
-
const r = await post(serverUrl, "relogin", { username, cookies, userId: "cli-login-pool", note: "
|
|
187
|
+
const r = await post(serverUrl, "relogin", { username, cookies, userId: "cli-login-pool", note: "命令行检测更新" });
|
|
143
188
|
log(
|
|
144
|
-
`✓ @${username}
|
|
189
|
+
`✓ @${username} 检测完成: cookies 已覆盖刷新(${r.account.cookieCount} 条)${r.revived ? ",已停用账号复活回池" : r.wasSuspect ? ",疑似已清回池" : ""}`,
|
|
145
190
|
);
|
|
191
|
+
} catch (e) {
|
|
192
|
+
enter.abort(); // 失败路径关闭回车监听,否则 readline 持有事件循环、进程不退
|
|
193
|
+
throw e;
|
|
146
194
|
} finally {
|
|
147
|
-
await session.cleanup();
|
|
195
|
+
if (session) await session.cleanup();
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
|
|
@@ -192,7 +240,7 @@ export async function handleLoginPool(parsed) {
|
|
|
192
240
|
" tt-help login-pool check [--server URL] [--proxy URL] [--ids id1,id2] [--json]",
|
|
193
241
|
"",
|
|
194
242
|
" add 新登录收号:弹出 Edge 人工登录 TikTok,检出后自动入池(幂等)",
|
|
195
|
-
" relogin
|
|
243
|
+
" relogin 检测:窗口打开账号当前登录态,人工核对/退出重登后按回车提交,身份校验后覆盖刷新",
|
|
196
244
|
" check 巡检:服务端 HTTP 探测各号登录态,死号自动标疑似",
|
|
197
245
|
].join("\n"),
|
|
198
246
|
);
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handleProtocolBridge — 协议桥:tt-help:// 自定义协议(网页 /login-pool 一键拉起本机命令)
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* tt-help protocol register 注册本机协议处理器(幂等,可重复执行)
|
|
6
|
+
* tt-help protocol unregister 注销协议处理器
|
|
7
|
+
* tt-help protocol handle <URL> 内部入口:处理器被系统拉起时回调(勿手动执行)
|
|
8
|
+
*
|
|
9
|
+
* 链路(2026-10-29 设计定稿,macOS 路线已经真机 POC):
|
|
10
|
+
* 页面按钮 → tt-help://login-pool/<add|relogin>?server=…&username=…
|
|
11
|
+
* → OS 协议处理器
|
|
12
|
+
* ├─ Windows: HKCU\Software\Classes\tt-help → 包内 scripts/protocol-bridge.cmd(新控制台窗+pause)
|
|
13
|
+
* └─ macOS: ~/Applications/tt-help.app(osacompile AppleScript applet,open location 收 URL
|
|
14
|
+
* —— shell 型 .app 收 argv 已实测不通:LS 只发 AppleEvent,argc=0)
|
|
15
|
+
* → tt-help protocol handle <URL> → 白名单校验 → 进程内直调 handleLoginPool(全程不过 shell)
|
|
16
|
+
*
|
|
17
|
+
* 安全: action 白名单只认 login-pool/add、login-pool/relogin;参数只透传
|
|
18
|
+
* server(origin)/username(字符集白名单)/note(去控制符限长);proxy/dir/timeout 不开放。
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
21
|
+
import { homedir, platform, tmpdir } from "os";
|
|
22
|
+
import { join } from "path";
|
|
23
|
+
import { spawnSync } from "child_process";
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
import { handleLoginPool } from "./login-pool.js";
|
|
26
|
+
|
|
27
|
+
const SCHEME = "tt-help";
|
|
28
|
+
const ALLOWED_ACTIONS = new Set(["login-pool/add", "login-pool/relogin"]);
|
|
29
|
+
const MAC_APP_DIR = join(homedir(), "Applications");
|
|
30
|
+
const MAC_APP = join(MAC_APP_DIR, "tt-help.app");
|
|
31
|
+
const BUNDLE_ID = "com.ycl.tt-help";
|
|
32
|
+
const WIN_REG_KEY = `HKCU\\Software\\Classes\\${SCHEME}`;
|
|
33
|
+
|
|
34
|
+
function log(t) {
|
|
35
|
+
console.error(`[协议桥] ${t}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** POSIX 单引号转义(用于 .command / do shell script 中的路径与 URL) */
|
|
39
|
+
function shQ(s) {
|
|
40
|
+
return `'${String(s).replace(/'/g, "'\\''")}'`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** package 根目录(本文件位于 <pkg>/src/cli/) */
|
|
44
|
+
function pkgRoot() {
|
|
45
|
+
return fileURLToPath(new URL("../..", import.meta.url));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ────────────────────────── URL 解析(handle 入口) ──────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 解析并校验协议 URL → { action, serverUrl, username?, note? }
|
|
52
|
+
* 注意: 非特殊 scheme 的 URL.hostname 不自动小写,必须显式 normalize(实测 LOGIN-POOL 原样保留)
|
|
53
|
+
*/
|
|
54
|
+
export function parseProtocolUrl(raw) {
|
|
55
|
+
if (!raw) throw new Error("缺少 URL 参数");
|
|
56
|
+
let u;
|
|
57
|
+
try {
|
|
58
|
+
u = new URL(String(raw).trim());
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`URL 无法解析: ${String(raw).slice(0, 120)}`);
|
|
61
|
+
}
|
|
62
|
+
if (u.protocol !== `${SCHEME}:`) throw new Error(`不支持的协议: ${u.protocol}(只认 ${SCHEME}://)`);
|
|
63
|
+
const action = `${u.hostname}${u.pathname.replace(/\/+$/, "")}`.toLowerCase();
|
|
64
|
+
if (!ALLOWED_ACTIONS.has(action)) {
|
|
65
|
+
throw new Error(`不支持的 action: ${action}(白名单: ${[...ALLOWED_ACTIONS].join(", ")})`);
|
|
66
|
+
}
|
|
67
|
+
const server = u.searchParams.get("server") || "";
|
|
68
|
+
let s;
|
|
69
|
+
try {
|
|
70
|
+
s = new URL(server);
|
|
71
|
+
} catch {
|
|
72
|
+
throw new Error("server 参数缺失或非法(需完整 http(s) URL)");
|
|
73
|
+
}
|
|
74
|
+
if (s.protocol !== "http:" && s.protocol !== "https:") throw new Error("server 仅支持 http/https");
|
|
75
|
+
const out = { action, serverUrl: s.origin };
|
|
76
|
+
if (action === "login-pool/relogin") {
|
|
77
|
+
const username = u.searchParams.get("username") || "";
|
|
78
|
+
if (!/^[A-Za-z0-9._]{1,64}$/.test(username)) throw new Error(`username 参数缺失或含非法字符: "${username.slice(0, 40)}"`);
|
|
79
|
+
out.username = username;
|
|
80
|
+
}
|
|
81
|
+
const note = (u.searchParams.get("note") || "").replace(/[\x00-\x1f\x7f]/g, "").slice(0, 200);
|
|
82
|
+
if (note) out.note = note;
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** handle 入口:解析 URL → 进程内直调 handleLoginPool(不经过任何 shell) */
|
|
87
|
+
async function cmdHandle(rawUrl) {
|
|
88
|
+
let p;
|
|
89
|
+
try {
|
|
90
|
+
p = parseProtocolUrl(rawUrl);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
log(`✗ ${e.message}`);
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
log(`收到 ${p.action}(server=${p.serverUrl}${p.username ? `,@${p.username}` : ""})`);
|
|
96
|
+
const parsed = {
|
|
97
|
+
subcommand: "login-pool",
|
|
98
|
+
action: p.action.split("/")[1],
|
|
99
|
+
username: p.username || null,
|
|
100
|
+
serverUrl: p.serverUrl,
|
|
101
|
+
proxy: null, // 走本机 ~/.tt-help.json 默认(与命令行行为一致)
|
|
102
|
+
noProxy: false,
|
|
103
|
+
note: p.note || null,
|
|
104
|
+
dir: null,
|
|
105
|
+
timeoutSec: 600,
|
|
106
|
+
ids: null,
|
|
107
|
+
json: false,
|
|
108
|
+
showHelp: false,
|
|
109
|
+
};
|
|
110
|
+
await handleLoginPool(parsed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ────────────────────────── macOS: osacompile applet ──────────────────────────
|
|
114
|
+
|
|
115
|
+
/** 生成 .applescript 源:open location → 建 .command(烧录绝对路径)→ open -a Terminal */
|
|
116
|
+
export function buildAppleScript({ nodePath, cliPath }) {
|
|
117
|
+
const asStr = (s) => `"${String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
118
|
+
// .command 内容: #!/bin/bash + 跑 node(不 exec,退出码非 0 时窗口必留)+ 按键保持
|
|
119
|
+
const shellP1 =
|
|
120
|
+
"t=$(mktemp ${TMPDIR:-/tmp}/tt-help-bridge.XXXXXX); f=\"$t.command\"; mv \"$t\" \"$f\"; " + // mktemp 要求 X 结尾,先随机后 mv 补 .command 后缀(实测:直接带后缀会退化成固定名,并发互踩)
|
|
121
|
+
"printf '#!/bin/bash\\n\"%s\" \"%s\" protocol handle \"%s\"\\nst=$?\\necho\\nrm -f \"$0\"\\nread -n 1 -s -r -p \"完成(退出码 $st)——按任意键关闭…\"\\nexit $st\\n' " +
|
|
122
|
+
`${shQ(nodePath)} ${shQ(cliPath)} `;
|
|
123
|
+
const shellP2 = ' > "$f"; chmod +x "$f"; /usr/bin/open -a Terminal "$f"';
|
|
124
|
+
return (
|
|
125
|
+
"on open location theURL\n" +
|
|
126
|
+
" do shell script " + asStr(shellP1) + " & quoted form of theURL & " + asStr(shellP2) + "\n" +
|
|
127
|
+
"end open location\n"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const MAC_LSREG =
|
|
132
|
+
"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister";
|
|
133
|
+
const MAC_PLISTBUDDY = "/usr/libexec/PlistBuddy";
|
|
134
|
+
|
|
135
|
+
function macRun(cmd, args, { check = true } = {}) {
|
|
136
|
+
const r = spawnSync(cmd, args, { encoding: "utf-8" });
|
|
137
|
+
if (check && r.status !== 0) {
|
|
138
|
+
throw new Error(`${cmd} 失败(退出码 ${r.status}): ${(r.stderr || r.stdout || "").slice(0, 300)}`);
|
|
139
|
+
}
|
|
140
|
+
return r;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** PlistBuddy 设值:存在则 Set,不存在则 Add */
|
|
144
|
+
function plistSet(plist, key, type, value) {
|
|
145
|
+
let r = macRun(MAC_PLISTBUDDY, ["-c", `Set :${key} ${value}`, plist], { check: false });
|
|
146
|
+
if (r.status !== 0) {
|
|
147
|
+
r = macRun(MAC_PLISTBUDDY, ["-c", `Add :${key} ${type} ${value}`, plist], { check: false });
|
|
148
|
+
if (r.status !== 0) throw new Error(`PlistBuddy 写入 ${key} 失败: ${(r.stderr || "").slice(0, 200)}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function registerDarwin() {
|
|
153
|
+
const osacompile = "/usr/bin/osacompile";
|
|
154
|
+
if (!existsSync(osacompile)) throw new Error("找不到 /usr/bin/osacompile(macOS 系统自带,异常情况)");
|
|
155
|
+
const nodePath = process.execPath;
|
|
156
|
+
const cliPath = join(pkgRoot(), "cli.js");
|
|
157
|
+
if (!existsSync(cliPath)) throw new Error(`找不到 cli.js: ${cliPath}`);
|
|
158
|
+
|
|
159
|
+
mkdirSync(MAC_APP_DIR, { recursive: true });
|
|
160
|
+
const src = join(mkdtempSync(join(tmpdir(), "tt-help-reg-")), "tt-help.applescript");
|
|
161
|
+
writeFileSync(src, buildAppleScript({ nodePath, cliPath }));
|
|
162
|
+
|
|
163
|
+
log(`编译 applet(node=${nodePath})…`);
|
|
164
|
+
rmSync(MAC_APP, { recursive: true, force: true });
|
|
165
|
+
macRun(osacompile, ["-o", MAC_APP, src]);
|
|
166
|
+
|
|
167
|
+
const plist = join(MAC_APP, "Contents", "Info.plist"); // PlistBuddy 只认 plist 文件,不认 .app 目录
|
|
168
|
+
plistSet(plist, "CFBundleIdentifier", "string", BUNDLE_ID);
|
|
169
|
+
plistSet(plist, "CFBundleName", "string", "tt-help");
|
|
170
|
+
plistSet(plist, "LSUIElement", "bool", "true"); // 不占 Dock
|
|
171
|
+
// URL scheme 声明(POC 验证过的 PlistBuddy 注入路径)
|
|
172
|
+
macRun(MAC_PLISTBUDDY, ["-c", "Add :CFBundleURLTypes array", plist], { check: false });
|
|
173
|
+
macRun(MAC_PLISTBUDDY, ["-c", "Add :CFBundleURLTypes:0 dict", plist], { check: false });
|
|
174
|
+
plistSet(plist, "CFBundleURLTypes:0:CFBundleURLName", "string", BUNDLE_ID);
|
|
175
|
+
macRun(MAC_PLISTBUDDY, ["-c", "Add :CFBundleURLTypes:0:CFBundleURLSchemes array", plist], { check: false });
|
|
176
|
+
macRun(MAC_PLISTBUDDY, ["-c", `Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string ${SCHEME}`, plist], { check: false });
|
|
177
|
+
macRun("/usr/bin/codesign", ["--force", "--deep", "-s", "-", MAC_APP], { check: false }); // ad-hoc 重签,尽力而为
|
|
178
|
+
|
|
179
|
+
macRun(MAC_LSREG, ["-f", MAC_APP]);
|
|
180
|
+
rmSync(src, { force: true });
|
|
181
|
+
rmSync(join(src, ".."), { recursive: true, force: true });
|
|
182
|
+
log(`✓ 已注册 ${SCHEME}:// 协议处理器: ${MAC_APP}`);
|
|
183
|
+
log(" 验证: 浏览器打开 /login-pool 页面点「新增账号」即可一键拉起;或执行:");
|
|
184
|
+
log(` open "${SCHEME}://login-pool/add?server=http%3A%2F%2F127.0.0.1%3A3001"`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function unregisterDarwin() {
|
|
188
|
+
if (!existsSync(MAC_APP)) {
|
|
189
|
+
log(`未发现已注册的处理器(${MAC_APP} 不存在),无需注销`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
macRun(MAC_LSREG, ["-u", MAC_APP], { check: false });
|
|
193
|
+
rmSync(MAC_APP, { recursive: true, force: true });
|
|
194
|
+
log(`✓ 已注销并删除 ${MAC_APP}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ────────────────────────── Windows: HKCU + 包内包装 cmd ──────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 生成包装 cmd 内容(register 时写进包 scripts/ 目录,npm 升级原地覆盖,注册表路径永不变)
|
|
201
|
+
* %1 恒由注册表带引号传入 + URL 参数百分号编码 = 无注入面;pause 保证最终结果不闪退
|
|
202
|
+
*/
|
|
203
|
+
export function buildWindowsCmd({ nodePath, cliPath }) {
|
|
204
|
+
return [
|
|
205
|
+
"@echo off",
|
|
206
|
+
"title tt-help 登录池",
|
|
207
|
+
`"${nodePath}" "${cliPath}" protocol handle "%~1"`,
|
|
208
|
+
"set RC=%ERRORLEVEL%",
|
|
209
|
+
"echo.",
|
|
210
|
+
`pause`,
|
|
211
|
+
"exit /b %RC%",
|
|
212
|
+
"",
|
|
213
|
+
].join("\r\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function registerWindows() {
|
|
217
|
+
const nodePath = process.execPath;
|
|
218
|
+
const cliPath = join(pkgRoot(), "cli.js");
|
|
219
|
+
const wrapperPath = join(pkgRoot(), "scripts", "protocol-bridge.cmd");
|
|
220
|
+
writeFileSync(wrapperPath, buildWindowsCmd({ nodePath, cliPath }), "utf-8");
|
|
221
|
+
|
|
222
|
+
const regAdd = (args) => macRun("reg", ["add", ...args]);
|
|
223
|
+
regAdd([WIN_REG_KEY, "/ve", "/d", `URL:${SCHEME}`, "/f"]);
|
|
224
|
+
regAdd([WIN_REG_KEY, "/v", "URL Protocol", "/d", "", "/f"]);
|
|
225
|
+
regAdd([`${WIN_REG_KEY}\\shell\\open\\command`, "/ve", "/d", `"${wrapperPath}" "%1"`, "/f"]);
|
|
226
|
+
log(`✓ 已注册 ${SCHEME}:// 协议处理器(HKCU,免管理员): ${wrapperPath}`);
|
|
227
|
+
log(" 验证: 浏览器打开 /login-pool 页面点「新增账号」;首次系统会弹「打开 tt-help?」确认框");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function unregisterWindows() {
|
|
231
|
+
const r = macRun("reg", ["delete", WIN_REG_KEY, "/f"], { check: false });
|
|
232
|
+
if (r.status !== 0) {
|
|
233
|
+
log(`未发现已注册的处理器(注册表 ${WIN_REG_KEY} 不存在),无需注销`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
log(`✓ 已注销注册表项 ${WIN_REG_KEY}(包装 cmd 随包升级/卸载自然消失)`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ────────────────────────── 入口 ──────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {object} parsed parseProtocolBridgeArgs 结果 { action, url, showHelp }
|
|
243
|
+
*/
|
|
244
|
+
export async function handleProtocolBridge(parsed) {
|
|
245
|
+
const { action, url, showHelp } = parsed;
|
|
246
|
+
if (showHelp || !action) {
|
|
247
|
+
console.error(
|
|
248
|
+
[
|
|
249
|
+
"用法:",
|
|
250
|
+
` tt-help protocol register 注册 ${SCHEME}:// 协议处理器(幂等)`,
|
|
251
|
+
` tt-help protocol unregister 注销协议处理器`,
|
|
252
|
+
` tt-help protocol handle <URL> 内部命令:处理器回调入口(勿手动执行)`,
|
|
253
|
+
"",
|
|
254
|
+
" 协议桥 = 网页 /login-pool「新增账号/重登录」按钮一键拉起本机 tt-help 命令。",
|
|
255
|
+
" 注册一次后,页面按钮直接弹出登录窗口(Windows/macOS;Linux 后置)。",
|
|
256
|
+
].join("\n"),
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (action === "handle") return cmdHandle(url);
|
|
261
|
+
if (action === "register") {
|
|
262
|
+
if (platform() === "darwin") return registerDarwin();
|
|
263
|
+
if (platform() === "win32") return registerWindows();
|
|
264
|
+
log("Linux 平台暂不支持(后置)。协议桥面向操作员桌面机(Windows/macOS)。");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (action === "unregister") {
|
|
268
|
+
if (platform() === "darwin") return unregisterDarwin();
|
|
269
|
+
if (platform() === "win32") return unregisterWindows();
|
|
270
|
+
log("Linux 平台暂不支持(后置)。");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
throw new Error(`未知子命令: ${action}(支持 register / unregister / handle)`);
|
|
274
|
+
}
|
package/src/lib/args.js
CHANGED
|
@@ -1175,7 +1175,7 @@ function parseLoginPoolArgs(args) {
|
|
|
1175
1175
|
let noProxy = false;
|
|
1176
1176
|
let note = null;
|
|
1177
1177
|
let dir = null;
|
|
1178
|
-
let timeoutSec =
|
|
1178
|
+
let timeoutSec = 600;
|
|
1179
1179
|
let ids = null;
|
|
1180
1180
|
let json = false;
|
|
1181
1181
|
let showHelp = false;
|
|
@@ -1193,7 +1193,7 @@ function parseLoginPoolArgs(args) {
|
|
|
1193
1193
|
} else if (arg === "--dir" && i + 1 < args.length) {
|
|
1194
1194
|
dir = args[++i];
|
|
1195
1195
|
} else if (arg === "--timeout" && i + 1 < args.length) {
|
|
1196
|
-
timeoutSec = parseInt(args[++i], 10) ||
|
|
1196
|
+
timeoutSec = parseInt(args[++i], 10) || 600;
|
|
1197
1197
|
} else if (arg === "--ids" && i + 1 < args.length) {
|
|
1198
1198
|
ids = args[++i].split(",").map((s) => s.trim()).filter(Boolean);
|
|
1199
1199
|
} else if (arg === "--json") {
|
|
@@ -1232,6 +1232,18 @@ function parseLoginPoolArgs(args) {
|
|
|
1232
1232
|
};
|
|
1233
1233
|
}
|
|
1234
1234
|
|
|
1235
|
+
function parseProtocolBridgeArgs(args) {
|
|
1236
|
+
let action = null;
|
|
1237
|
+
let url = null;
|
|
1238
|
+
let showHelp = false;
|
|
1239
|
+
for (const arg of args) {
|
|
1240
|
+
if (arg === "-h" || arg === "--help") showHelp = true;
|
|
1241
|
+
else if (!action) action = arg;
|
|
1242
|
+
else if (!url) url = arg;
|
|
1243
|
+
}
|
|
1244
|
+
return { subcommand: "protocol", action, url, showHelp };
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1235
1247
|
function parseOpenArgs(args) {
|
|
1236
1248
|
let openPort = null;
|
|
1237
1249
|
let openList = false;
|
|
@@ -1790,6 +1802,15 @@ export function parseArgs() {
|
|
|
1790
1802
|
return parseLoginPoolArgs(args.slice(1));
|
|
1791
1803
|
}
|
|
1792
1804
|
|
|
1805
|
+
if (args.length > 0 && args[0] === "protocol") {
|
|
1806
|
+
return parseProtocolBridgeArgs(args.slice(1));
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// protocol-handle 单 token 别名:协议处理器拉起链路的历史形态,兼容已注册机器
|
|
1810
|
+
if (args.length > 0 && args[0] === "protocol-handle") {
|
|
1811
|
+
return parseProtocolBridgeArgs(["handle", ...args.slice(1)]);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1793
1814
|
if (args.length > 0 && args[0] === "comments") {
|
|
1794
1815
|
return parseCommentsArgs(args.slice(1));
|
|
1795
1816
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -294,13 +294,13 @@ const HELP_TEXT = [
|
|
|
294
294
|
" tt-help login-pool <add|relogin|check> [选项]",
|
|
295
295
|
" 账号中心:新登录收号 / 重登录 / 巡检(与网页 /login-pool 同一套账号库)",
|
|
296
296
|
" add 弹出独立 Edge,人工登录 TikTok 后自动导出登录态入池(幂等)",
|
|
297
|
-
" relogin <username>
|
|
297
|
+
" relogin <username> 检测:窗口打开账号当前登录态,人工核对/退出重登后按回车提交,覆盖刷新池内 cookies",
|
|
298
298
|
" 选项:",
|
|
299
299
|
" --server <URL> 账号中心地址(watch 服务端)",
|
|
300
300
|
" --proxy <地址> Edge/探测代理,默认配置文件 proxy;--no-proxy 直连",
|
|
301
301
|
" --note <备注> add 备注信息",
|
|
302
302
|
" --dir <路径> 自定义 Edge 用户目录(默认临时目录用后即清)",
|
|
303
|
-
" --timeout <秒>
|
|
303
|
+
" --timeout <秒> 等待人工操作上限,默认 600",
|
|
304
304
|
" --ids <id1,id2> check 只探测指定账号",
|
|
305
305
|
" --json check 输出 JSON",
|
|
306
306
|
" 示例: tt-help login-pool add --server http://172.18.154.201:3011",
|
|
@@ -308,6 +308,13 @@ const HELP_TEXT = [
|
|
|
308
308
|
" tt-help login-pool check --server http://172.18.154.201:3011 --proxy http://127.0.0.1:7897",
|
|
309
309
|
" 示例: tt-help videostats data/result.db -p 3",
|
|
310
310
|
"",
|
|
311
|
+
" tt-help protocol <register|unregister|handle> [URL]",
|
|
312
|
+
" 协议桥:注册 tt-help:// 协议处理器,网页 /login-pool 按钮一键拉起本机命令(Windows/macOS)",
|
|
313
|
+
" register 注册处理器(幂等;Windows 写 HKCU,macOS 生成 ~/Applications/tt-help.app)",
|
|
314
|
+
" unregister 注销处理器",
|
|
315
|
+
" handle <URL> 内部命令:处理器回调入口(系统拉起时调用,勿手动执行)",
|
|
316
|
+
" 示例: tt-help protocol register",
|
|
317
|
+
"",
|
|
311
318
|
" db-import --db <db路径> [--users users.json] [--done done.json] [--videos videos.json]",
|
|
312
319
|
" 将 legacy JSON 显式导入 SQLite;运行期不再自动读取 result*.json",
|
|
313
320
|
" 示例: tt-help db-import --db data/result.db --users data/result.json --done data/result-done.json --videos data/result-videos.json",
|
package/src/main.js
CHANGED
|
@@ -28,6 +28,7 @@ import { handleWatchdog } from "./cli/watchdog.js";
|
|
|
28
28
|
import { handleClashDaemon } from "./cli/clash-daemon.js";
|
|
29
29
|
import { handleLoginCheck } from "./cli/login-check.js";
|
|
30
30
|
import { handleLoginPool } from "./cli/login-pool.js";
|
|
31
|
+
import { handleProtocolBridge } from "./cli/protocol-bridge.js";
|
|
31
32
|
|
|
32
33
|
async function main() {
|
|
33
34
|
const parsed = parseArgs();
|
|
@@ -83,6 +84,8 @@ async function main() {
|
|
|
83
84
|
return handleLoginCheck(parsed);
|
|
84
85
|
case "login-pool":
|
|
85
86
|
return handleLoginPool(parsed);
|
|
87
|
+
case "protocol":
|
|
88
|
+
return handleProtocolBridge(parsed);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
const {
|
package/src/npm-main.js
CHANGED
|
@@ -24,6 +24,7 @@ import { handleSearch } from "./cli/search.js";
|
|
|
24
24
|
import { handleWatchdog } from "./cli/watchdog.js";
|
|
25
25
|
import { handleLoginCheck } from "./cli/login-check.js";
|
|
26
26
|
import { handleLoginPool } from "./cli/login-pool.js";
|
|
27
|
+
import { handleProtocolBridge } from "./cli/protocol-bridge.js";
|
|
27
28
|
|
|
28
29
|
function exitUnsupportedCommand(command) {
|
|
29
30
|
console.error(
|
|
@@ -82,6 +83,8 @@ async function main() {
|
|
|
82
83
|
return handleLoginCheck(parsed);
|
|
83
84
|
case "login-pool":
|
|
84
85
|
return handleLoginPool(parsed);
|
|
86
|
+
case "protocol":
|
|
87
|
+
return handleProtocolBridge(parsed);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
const {
|
package/src/watch/server.js
CHANGED
|
@@ -686,7 +686,7 @@ function serveLoginPoolPage(res) {
|
|
|
686
686
|
<div class="sub">登录态统一存放与租借:业务取号注入 / 节点启动导入 / 巡检与重登录</div>
|
|
687
687
|
<div class="bar">
|
|
688
688
|
<button onclick="load()">刷新</button>
|
|
689
|
-
<button onclick="addCmdDlg()" title="
|
|
689
|
+
<button onclick="addCmdDlg()" title="一键拉起本机 tt-help:弹出 Edge 登录 TikTok 后自动入池(本机需已安装 tt-help 并执行过 tt-help protocol register;未注册时自动回退为复制命令)">新增账号</button>
|
|
690
690
|
<button id="btn-check" class="primary" onclick="checkNow()" title="逐号 HTTP 探测登录态(空闲+疑似),死号自动标疑似">批量检测</button>
|
|
691
691
|
<span id="msg"></span>
|
|
692
692
|
<span class="chips" id="stats"></span>
|
|
@@ -758,7 +758,7 @@ async function load() {
|
|
|
758
758
|
(a.status === 'retired' ? '<button onclick="enable(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\',false)">启用</button> ' : '') +
|
|
759
759
|
(a.status !== 'retired' ? '<button onclick="disable(\\'' + a.id + '\\',\\'' + esc(a.username) + '\\',' + (a.status === 'leased') + ')">停用</button> ' : '') +
|
|
760
760
|
(a.status === 'retired' ? '<button onclick="del(\\'' + a.id + '\\')">删除</button> ' : '') +
|
|
761
|
-
'<button onclick="reloginDlg(\\'' + esc(a.username) + '\\')" title="
|
|
761
|
+
'<button onclick="reloginDlg(\\'' + esc(a.username) + '\\')" title="一键拉起本机 tt-help 检测:窗口打开账号当前登录态,可退出重登或保持,回终端按回车提交更新池内 cookies">检测</button>' +
|
|
762
762
|
'</td>' +
|
|
763
763
|
'<td><details data-acc="' + esc(a.id) + '" ' + (openHist.has(a.id) ? 'open' : '') + ' ontoggle="onHistToggle(this)"><summary class="muted">' + (a.history ? a.history.length : 0) + ' 条</summary>' +
|
|
764
764
|
(a.history || []).slice(0, 8).map(h => '<div class="muted">' + fmt(h.at) + ' ' + esc(h.event) + ' ' + esc(h.detail) + '</div>').join('') + '</details></td>' +
|
|
@@ -776,20 +776,40 @@ async function load() {
|
|
|
776
776
|
'<span class="chip">已停用 <b>' + nRet + '</b></span>';
|
|
777
777
|
document.getElementById('content').innerHTML =
|
|
778
778
|
'<table><tr><th>#</th><th>账号</th><th>状态</th><th>使用者</th><th>最近使用</th><th>次数</th><th>有效期</th><th>检测</th><th>操作</th><th>历史</th></tr>' + rows + '</table>' +
|
|
779
|
-
'<div style="padding: 10px 14px;"><p class="muted" style="margin:0">排序:使用中 → 空闲 → 疑似 → 已停用 · 每 15 秒自动刷新 · 有效期 = sessionid cookie 承诺寿命 · 检测 = 登录态探测(真实判据 = 业务出活)· cookies
|
|
779
|
+
'<div style="padding: 10px 14px;"><p class="muted" style="margin:0">排序:使用中 → 空闲 → 疑似 → 已停用 · 每 15 秒自动刷新 · 有效期 = sessionid cookie 承诺寿命 · 检测 = 登录态探测(真实判据 = 业务出活)· cookies 明文不出服务端 · 「新增账号/重登录」一键拉起需本机装 tt-help 并执行过 <b>tt-help protocol register</b></p></div>';
|
|
780
780
|
} catch (e) { msg('加载失败: ' + e.message, true); }
|
|
781
781
|
}
|
|
782
782
|
function copyCmd(cmd) {
|
|
783
783
|
if (navigator.clipboard) navigator.clipboard.writeText(cmd).catch(() => {});
|
|
784
784
|
prompt('复制命令(在装有 tt-help 的机器上执行):', cmd);
|
|
785
785
|
}
|
|
786
|
+
function protoHref(action, params) {
|
|
787
|
+
const q = [];
|
|
788
|
+
for (const k in params) q.push(k + '=' + encodeURIComponent(params[k]));
|
|
789
|
+
return 'tt-help://' + action + '?' + q.join('&');
|
|
790
|
+
}
|
|
791
|
+
let protoBlurred = false;
|
|
792
|
+
window.addEventListener('blur', () => { protoBlurred = true; });
|
|
793
|
+
function launchProto(url, fallbackCmd, okMsg) {
|
|
794
|
+
protoBlurred = false;
|
|
795
|
+
location.href = url;
|
|
796
|
+
setProgress('已发起一键拉起 tt-help…');
|
|
797
|
+
setTimeout(() => {
|
|
798
|
+
if (protoBlurred) { msg(okMsg); return; }
|
|
799
|
+
const el = document.getElementById('msg');
|
|
800
|
+
el.style.color = '#d97706';
|
|
801
|
+
el.innerHTML = '未检测到协议处理器(或已在弹窗取消)。<button onclick="copyCmd(\\'' + fallbackCmd + '\\')">复制命令</button> <span class="muted">或本机执行 <b>tt-help protocol register</b> 注册后即可一键拉起</span>';
|
|
802
|
+
}, 2500);
|
|
803
|
+
}
|
|
786
804
|
function addCmdDlg() {
|
|
787
|
-
|
|
788
|
-
|
|
805
|
+
launchProto(protoHref('login-pool/add', { server: location.origin }),
|
|
806
|
+
'tt-help login-pool add --server ' + location.origin,
|
|
807
|
+
'已拉起 tt-help:请在弹出的窗口登录 TikTok,完成后回来点刷新');
|
|
789
808
|
}
|
|
790
809
|
function reloginDlg(name) {
|
|
791
|
-
|
|
792
|
-
|
|
810
|
+
launchProto(protoHref('login-pool/relogin', { server: location.origin, username: name }),
|
|
811
|
+
'tt-help login-pool relogin ' + name + ' --server ' + location.origin,
|
|
812
|
+
'已拉起 tt-help 检测 @' + name + ':窗口中核对/退出重登,完成后回终端按回车提交,然后回来点刷新');
|
|
793
813
|
}
|
|
794
814
|
function setProgress(t) {
|
|
795
815
|
const el = document.getElementById('msg');
|