tt-help-cli-ycl 1.4.178 → 1.4.179
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 +12 -1
- package/scripts/test-protocol-bridge.mjs +131 -0
- package/src/cli/protocol-bridge.js +274 -0
- package/src/lib/args.js +16 -0
- package/src/lib/constants.js +7 -0
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/server.js +27 -7
package/package.json
CHANGED
|
@@ -163,7 +163,18 @@ check("独立页含新增账号/批量检测按钮", await page.$$eval(".bar but
|
|
|
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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议桥单元测试 — parseProtocolUrl 白名单/参数校验 + 双平台模板生成
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:
|
|
5
|
+
* [1] URL 解析:合法样例(大小写 normalize / origin 归一 / 尾斜杠 / note 清洗)
|
|
6
|
+
* [2] URL 拒绝:白名单外 action / 缺参 / 非 http server / username 非法 / scheme 不对
|
|
7
|
+
* [3] 模板生成:AppleScript(osacompile 源)与 Windows 包装 cmd 的结构断言
|
|
8
|
+
*
|
|
9
|
+
* 运行: node scripts/test-protocol-bridge.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { strict as assert } from "assert";
|
|
12
|
+
import { parseProtocolUrl, buildAppleScript, buildWindowsCmd } from "../src/cli/protocol-bridge.js";
|
|
13
|
+
|
|
14
|
+
let pass = 0;
|
|
15
|
+
let fail = 0;
|
|
16
|
+
const check = (name, fn) => {
|
|
17
|
+
try {
|
|
18
|
+
fn();
|
|
19
|
+
pass++;
|
|
20
|
+
console.log(` ✓ ${name}`);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
fail++;
|
|
23
|
+
console.log(` ✗ ${name}: ${e.message}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ── [1] 合法样例 ──
|
|
28
|
+
console.log("[1] URL 解析(合法)");
|
|
29
|
+
check("add 最小样例:host/path 解析 + server 取 origin", () => {
|
|
30
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http%3A%2F%2F172.18.154.201%3A3001");
|
|
31
|
+
assert.equal(p.action, "login-pool/add");
|
|
32
|
+
assert.equal(p.serverUrl, "http://172.18.154.201:3001");
|
|
33
|
+
assert.equal(p.username, undefined);
|
|
34
|
+
});
|
|
35
|
+
check("hostname 大写自动 normalize(非特殊 scheme 不自动小写,实测坑)", () => {
|
|
36
|
+
const p = parseProtocolUrl("tt-help://LOGIN-POOL/RELOGIN?server=https://x.io&username=u1");
|
|
37
|
+
assert.equal(p.action, "login-pool/relogin");
|
|
38
|
+
});
|
|
39
|
+
check("pathname 尾斜杠归一", () => {
|
|
40
|
+
const p = parseProtocolUrl("tt-help://login-pool/add/?server=http://x.io");
|
|
41
|
+
assert.equal(p.action, "login-pool/add");
|
|
42
|
+
});
|
|
43
|
+
check("server 路径丢弃只留 origin", () => {
|
|
44
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http%3A%2F%2Fx.io%2Fevil%2Fpath%3Fq%3D1");
|
|
45
|
+
assert.equal(p.serverUrl, "http://x.io");
|
|
46
|
+
});
|
|
47
|
+
check("username 字符集:字母数字点下划线", () => {
|
|
48
|
+
const p = parseProtocolUrl("tt-help://login-pool/relogin?server=http://x.io&username=user_1.2");
|
|
49
|
+
assert.equal(p.username, "user_1.2");
|
|
50
|
+
});
|
|
51
|
+
check("note 控制符清洗 + 200 限长", () => {
|
|
52
|
+
const long = "a".repeat(500);
|
|
53
|
+
const p = parseProtocolUrl(
|
|
54
|
+
"tt-help://login-pool/add?server=http://x.io¬e=" +
|
|
55
|
+
encodeURIComponent("备\n注\x00" + long),
|
|
56
|
+
);
|
|
57
|
+
assert.equal(p.note, "备注" + "a".repeat(198));
|
|
58
|
+
assert.ok(!p.note.includes("\x00"));
|
|
59
|
+
});
|
|
60
|
+
check("未知参数静默忽略", () => {
|
|
61
|
+
const p = parseProtocolUrl("tt-help://login-pool/add?server=http://x.io&proxy=http://evil&dir=/etc");
|
|
62
|
+
assert.equal(p.proxy, undefined);
|
|
63
|
+
assert.equal(p.dir, undefined);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── [2] 拒绝样例 ──
|
|
67
|
+
console.log("[2] URL 解析(拒绝)");
|
|
68
|
+
const rejects = [
|
|
69
|
+
["白名单外 action(check 不开放)", "tt-help://login-pool/check?server=http://x.io"],
|
|
70
|
+
["任意命令形 action(防任意执行入口)", "tt-help://config/set?server=http://x.io"],
|
|
71
|
+
["路径注入形 action", "tt-help://../../win.ini?server=http://x.io"],
|
|
72
|
+
["scheme 不对", "https://login-pool/add?server=http://x.io"],
|
|
73
|
+
["缺 server", "tt-help://login-pool/add"],
|
|
74
|
+
["server 非 http(s)", "tt-help://login-pool/add?server=ftp://x.io"],
|
|
75
|
+
["server 垃圾串", "tt-help://login-pool/add?server=not-a-url"],
|
|
76
|
+
["relogin 缺 username", "tt-help://login-pool/relogin?server=http://x.io"],
|
|
77
|
+
["username 空格", "tt-help://login-pool/relogin?server=http://x.io&username=a%20b"],
|
|
78
|
+
["username 命令注入字符", "tt-help://login-pool/relogin?server=http://x.io&username=a%3Brm"],
|
|
79
|
+
["username 超长", "tt-help://login-pool/relogin?server=http://x.io&username=" + "a".repeat(65)],
|
|
80
|
+
["空 URL", ""],
|
|
81
|
+
["null", null],
|
|
82
|
+
];
|
|
83
|
+
for (const [why, raw] of rejects) {
|
|
84
|
+
check(`拒绝: ${why}`, () => {
|
|
85
|
+
assert.throws(() => parseProtocolUrl(raw), Error);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── [3] 模板生成 ──
|
|
90
|
+
console.log("[3] 双平台模板生成");
|
|
91
|
+
const NODE = "/Users/x/.nvm/versions/node/v24.14.1/bin/node";
|
|
92
|
+
const CLI = "/Users/x/pkg/tt-help-cli-ycl/cli.js";
|
|
93
|
+
check("AppleScript: open location 处理器 + quoted form 拼接", () => {
|
|
94
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
95
|
+
assert.ok(src.startsWith("on open location theURL"), "必须有 open location 处理器");
|
|
96
|
+
assert.ok(src.includes("quoted form of theURL"), "URL 必须经 quoted form 传递");
|
|
97
|
+
assert.ok(src.includes("open -a Terminal"), "必须拉起 Terminal");
|
|
98
|
+
assert.ok(src.includes("'#!/bin/bash"), "必须生成 .command(shebang)");
|
|
99
|
+
});
|
|
100
|
+
check("AppleScript: 生成源可通过 osacompile 语法结构(do shell script 成对引号)", () => {
|
|
101
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
102
|
+
const m = src.match(/do shell script ("(?:[^"\\]|\\.)*")/);
|
|
103
|
+
assert.ok(m, "do shell script 后必须是合法 AppleScript 字符串字面量");
|
|
104
|
+
// 拼接表达式收尾
|
|
105
|
+
assert.ok(/& quoted form of theURL & "(?:[^"\\]|\\.)*"\s*\nend open location/.test(src), "拼接式必须完整闭合");
|
|
106
|
+
});
|
|
107
|
+
check("AppleScript: node 路径烧录且单引号包裹", () => {
|
|
108
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
109
|
+
assert.ok(src.includes(`'${NODE}'`), "node 绝对路径必须烧录");
|
|
110
|
+
assert.ok(src.includes(`'${CLI}'`), "cli.js 绝对路径必须烧录");
|
|
111
|
+
});
|
|
112
|
+
check("AppleScript: 路径含空格时仍单引号安全", () => {
|
|
113
|
+
const src = buildAppleScript({ nodePath: "/Users/a b/n/node", cliPath: "/Users/a b/n/cli.js" });
|
|
114
|
+
assert.ok(src.includes("'/Users/a b/n/node'"), "含空格路径必须在单引号内");
|
|
115
|
+
});
|
|
116
|
+
check("AppleScript: .command 退出码保持 + 按键防闪退", () => {
|
|
117
|
+
const src = buildAppleScript({ nodePath: NODE, cliPath: CLI });
|
|
118
|
+
assert.ok(src.includes("st=$?"), "必须捕获退出码");
|
|
119
|
+
assert.ok(src.includes("read -n 1"), "必须按键保持窗口");
|
|
120
|
+
});
|
|
121
|
+
check("Windows cmd: 绝对路径 + %1 引号约定 + pause", () => {
|
|
122
|
+
const cmd = buildWindowsCmd({ nodePath: "C:\\n\\node.exe", cliPath: "C:\\p\\cli.js" });
|
|
123
|
+
assert.ok(cmd.startsWith("@echo off"), "必须关回显");
|
|
124
|
+
assert.ok(cmd.includes('"C:\\n\\node.exe" "C:\\p\\cli.js" protocol-handle %1'), "参数必须是单变量 %1");
|
|
125
|
+
assert.ok(cmd.includes("pause"), "必须 pause 防闪退");
|
|
126
|
+
assert.ok(cmd.includes("exit /b %RC%"), "必须透传退出码");
|
|
127
|
+
assert.ok(cmd.includes("\r\n"), "必须是 CRLF");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
console.log(`\n结果: ${pass} 通过, ${fail} 失败`);
|
|
131
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -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: 300,
|
|
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
|
@@ -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,10 @@ 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
|
+
|
|
1793
1809
|
if (args.length > 0 && args[0] === "comments") {
|
|
1794
1810
|
return parseCommentsArgs(args.slice(1));
|
|
1795
1811
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -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');
|