tt-help-cli-ycl 1.4.172 → 1.4.176

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.172",
3
+ "version": "1.4.176",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 登录池(账号中心)重置脚本 — 在 201 上运行
3
+ *
4
+ * 作用(2026-10-26 清空重导需求):
5
+ * - 去重:丢弃 *-poc 实验副本与同 username 重复项(保留首个)
6
+ * - 清空使用痕迹:useCount=0、history 重置为单条 imported 事件
7
+ * - 清空命令队列(防遗留 applyLogin 误注入)
8
+ * - 租约【原样保留】:在用进程(如 F-1 search 组)不受任何影响、无注入打断
9
+ *
10
+ * 用法:
11
+ * node reset-login-pool.mjs # 预览(只打印,不写盘)
12
+ * node reset-login-pool.mjs --apply # 实际写盘(原子替换 tmp+rename)
13
+ *
14
+ * 写盘后无需重启进程:watch/standalone 通过 mtime 自动重载池文件。
15
+ */
16
+ import { readFileSync, writeFileSync, renameSync, copyFileSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { join } from "path";
19
+
20
+ const APPLY = process.argv.includes("--apply");
21
+ const FILE = join(homedir(), ".tt-help-watchdog-logins.json");
22
+
23
+ const data = JSON.parse(readFileSync(FILE, "utf-8"));
24
+ const now = Date.now();
25
+ const seen = new Set();
26
+ const kept = [];
27
+ const dropped = [];
28
+
29
+ for (const a of data.accounts || []) {
30
+ if (/-poc$/i.test(a.username)) {
31
+ dropped.push(`${a.username}(poc 实验副本)`);
32
+ continue;
33
+ }
34
+ if (seen.has(a.username)) {
35
+ dropped.push(`${a.username}(重复)`);
36
+ continue;
37
+ }
38
+ seen.add(a.username);
39
+ a.useCount = 0;
40
+ a.history = [{ at: now, event: "imported", detail: "重置导入(原租约保留)" }];
41
+ kept.push(a);
42
+ }
43
+ data.accounts = kept; // 去重后重建账号表(无此行则仅原地变更保留项,被丢弃项仍会写回)
44
+ data.queue = [];
45
+
46
+ console.log(`池文件: ${FILE}`);
47
+ console.log(`保留 ${kept.length} 个账号:`);
48
+ for (const a of kept) {
49
+ const l = a.lease;
50
+ console.log(
51
+ ` - ${a.username} ${a.status}${l ? ` ← ${l.userId}:${l.port}${l.appName ? " · " + l.appName : ""}` : ""} (cookies ${Array.isArray(a.cookies) ? a.cookies.length : 0} 条)`,
52
+ );
53
+ }
54
+ console.log(`丢弃 ${dropped.length} 个: ${dropped.join(", ") || "无"}`);
55
+ console.log(`命令队列: 已清空(原 ${((data.queue || []).length, 0)} 条待发)`);
56
+
57
+ if (!APPLY) {
58
+ console.log("\n[预览模式] 确认无误后执行: node reset-login-pool.mjs --apply");
59
+ } else {
60
+ copyFileSync(FILE, FILE + ".bak-reset-" + now); // 先备份
61
+ const tmp = FILE + ".tmp";
62
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
63
+ renameSync(tmp, FILE);
64
+ console.log(`\n[已写盘] 备份: ${FILE}.bak-reset-${now}`);
65
+ console.log("watch/standalone 将通过 mtime 自动重载,无需重启。打开 /login-pool 核对。");
66
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 登录池全链路实测(真实 agent + 真实 Edge 注入)
3
+ *
4
+ * 流程:
5
+ * 0. 临时 HOME + 本地 watch(3131) + 真实 WatchdogAgent(POOLTEST@dev, 心跳 3s)
6
+ * 1. API 入池(F-11:9223 导出的真实 cookies, @user2440618251624)
7
+ * 2. 指派 → POOLTEST:9299
8
+ * 3. agent 心跳取命令 → login-apply 真实 Edge 注入(Xvfb :99 + 7897 出口)
9
+ * 4. 轮询服务端直至 lastAppliedAt 落地 → 断言 ok + persisted
10
+ * 5. release(clearOnNode) → 等 clearLogin 回报 → 验证磁盘 Cookies 已删
11
+ *
12
+ * 运行: node scripts/test-login-pool-e2e-real.mjs
13
+ * 前提: Xvfb :99 已跑、7897 出口可用、9299/3131 空闲、~/login-pool-staging/f11-9223.json 存在
14
+ */
15
+ import { spawn } from "child_process";
16
+ import { mkdtempSync, readFileSync, existsSync } from "fs";
17
+ import { tmpdir, homedir } from "os";
18
+ import { join, dirname } from "path";
19
+ import { fileURLToPath } from "url";
20
+
21
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
22
+ const HOME = mkdtempSync(join(tmpdir(), "pool-real-"));
23
+ const PORT = 3131;
24
+ const BASE = `http://127.0.0.1:${PORT}`;
25
+ const TEST_PORT = 9299;
26
+ const USERNAME = "user2440618251624";
27
+
28
+ let pass = 0, fail = 0;
29
+ const check = (name, cond, extra = "") => {
30
+ cond ? (pass++, console.log(` ✓ ${name}`)) : (fail++, console.log(` ✗ ${name} ${extra}`));
31
+ };
32
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
33
+
34
+ // ── 起 watch ──
35
+ const srv = spawn("node", [join(root, "src", "main.js"), "watch", "-o", join(HOME, "t.db"), "-p", String(PORT)], {
36
+ cwd: root, env: { ...process.env, HOME }, stdio: ["ignore", "ignore", "ignore"],
37
+ });
38
+ for (let i = 0; i < 30; i++) {
39
+ try { await fetch(BASE + "/api/stats"); break; } catch { await sleep(500); }
40
+ }
41
+ console.log("✓ watch 就绪");
42
+
43
+ // ── 入池 ──
44
+ const cookies = JSON.parse(readFileSync(join(homedir(), "login-pool-staging", "f11-9223.json"), "utf-8"));
45
+ let r = await (await fetch(`${BASE}/api/watchdog/login-pool/add`, {
46
+ method: "POST", headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ username: USERNAME, cookies, note: "E2E-real: F-11:9223 导出", proxyHint: "7897" }),
48
+ })).json();
49
+ check("入池", r.ok, JSON.stringify(r).slice(0, 200));
50
+ const accId = r.account.id;
51
+
52
+ // ── 指派 ──
53
+ r = await (await fetch(`${BASE}/api/watchdog/login-pool/assign`, {
54
+ method: "POST", headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ id: accId, userId: "POOLTEST", port: TEST_PORT }),
56
+ })).json();
57
+ check("指派 POOLTEST:9299", r.ok, JSON.stringify(r).slice(0, 200));
58
+
59
+ // ── 真实 agent ──
60
+ const agentCode = `
61
+ import { WatchdogAgent } from ${JSON.stringify(join(root, "src", "watchdog", "agent.js"))};
62
+ const agent = new WatchdogAgent({
63
+ serverUrl: "${BASE}",
64
+ clientId: "pooltest@dev",
65
+ intervalMs: 3000,
66
+ spawnDaemon: false,
67
+ });
68
+ agent.running = true;
69
+ const beat = () => agent._sendHeartbeat().catch((e) => console.error("[beat]", e.message));
70
+ beat();
71
+ setInterval(beat, 3000);
72
+ setInterval(() => {}, 60000); // keep alive
73
+ `;
74
+ const agentScript = join(HOME, "agent.mjs");
75
+ (await import("fs")).writeFileSync(agentScript, agentCode);
76
+ // 节点约定:~/.tt-help.json 带 userId(服务端按它匹配命令队列)——生产节点都有,测试 HOME 补齐
77
+ (await import("fs")).writeFileSync(join(HOME, ".tt-help.json"), JSON.stringify({ server: BASE, userId: "POOLTEST" }));
78
+ const agent = spawn("node", [agentScript], {
79
+ cwd: root,
80
+ env: { ...process.env, HOME, DISPLAY: ":99" },
81
+ stdio: ["ignore", "ignore", "pipe"],
82
+ });
83
+ let agentLog = "";
84
+ agent.stderr.on("data", (d) => {
85
+ agentLog += d;
86
+ process.stderr.write(`[agent] ${d}`);
87
+ });
88
+ console.log("✓ agent 已启动(真实进程,DISPLAY=:99)");
89
+
90
+ // ── 轮询注入结果(最长 6 分钟) ──
91
+ let lease = null;
92
+ const t0 = Date.now();
93
+ while (Date.now() - t0 < 6 * 60 * 1000) {
94
+ await sleep(5000);
95
+ const pool = await (await fetch(`${BASE}/api/watchdog/login-pool`)).json();
96
+ lease = pool.accounts.find((a) => a.id === accId)?.lease;
97
+ if (lease && lease.lastAppliedAt) break;
98
+ }
99
+ check("节点回报已到达", !!lease?.lastAppliedAt, `agentLog 尾部: ${agentLog.slice(-400)}`);
100
+ check("注入成功", lease?.lastAppliedOk === true, `error 见上方 agent 日志`);
101
+ check("持久化成功", lease?.persisted === true);
102
+ check("回报用户名正确", lease?.appliedUsername === USERNAME, `got ${lease?.appliedUsername}`);
103
+
104
+ // 磁盘验证:Cookies 文件存在且非空
105
+ const ck1 = join(HOME, "Library/Application Support/Microsoft Edge For Testing_p" + TEST_PORT, "Default/Cookies");
106
+ const ck2 = join(HOME, "Library/Application Support/Microsoft Edge For Testing_p" + TEST_PORT, "Default/Network/Cookies");
107
+ const diskHas = existsSync(ck1) || existsSync(ck2);
108
+ check("磁盘 Cookies 落盘", diskHas);
109
+
110
+ // ── release + clearOnNode → clearLogin 回报 ──
111
+ r = await (await fetch(`${BASE}/api/watchdog/login-pool/release`, {
112
+ method: "POST", headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ id: accId, clearOnNode: true }),
114
+ })).json();
115
+ check("释放+清理下发", r.ok);
116
+
117
+ let cleared = false;
118
+ const t1 = Date.now();
119
+ while (Date.now() - t1 < 60 * 1000) {
120
+ await sleep(4000);
121
+ const pool = await (await fetch(`${BASE}/api/watchdog/login-pool`)).json();
122
+ const acc = pool.accounts.find((a) => a.id === accId);
123
+ if (acc?.history?.some((h) => h.event === "cleared")) { cleared = true; break; }
124
+ }
125
+ check("clearLogin 回报到达", cleared, agentLog.slice(-200));
126
+ await sleep(2000);
127
+ check("磁盘 Cookies 已删除", !existsSync(ck1) && !existsSync(ck2));
128
+
129
+ // 收尾
130
+ agent.kill("SIGKILL");
131
+ srv.kill("SIGKILL");
132
+ console.log(`\n═══ 结果: ${pass} 通过, ${fail} 失败 ═══`);
133
+ console.log(`临时 HOME(含测试 cookies,可直接删): ${HOME}`);
134
+ process.exit(fail > 0 ? 1 : 0);
@@ -0,0 +1,211 @@
1
+ /**
2
+ * 登录池 UI 冒烟测试 — 真实 watch 服务 + 无头 Edge
3
+ *
4
+ * 覆盖(2026-10-26 账号中心改版后):
5
+ * 1. 主页:账户中心入口链接存在(→ /login-pool),嵌入区块已移除,无 JS 错误,无 cookie 明文
6
+ * 2. 独立页 /login-pool:新列渲染(状态徽章/使用者/次数/有效期)、有效期秒→毫秒归一正确
7
+ * 3. API 指派 → 独占(已租不可再指派);心跳下发命令含明文 cookies(仅此通道)
8
+ * 4. 释放回池;suspect 拒绝无 force 指派;leased 删除保护
9
+ *
10
+ * 运行: node scripts/test-login-pool-ui.mjs(自行起停服务)
11
+ */
12
+ import { spawn } from "child_process";
13
+ import { mkdtempSync } from "fs";
14
+ import { tmpdir } from "os";
15
+ import { join, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { chromium } from "playwright";
18
+
19
+ const root = join(dirname(fileURLToPath(import.meta.url)), "..");
20
+ const HOME = mkdtempSync(join(tmpdir(), "loginpool-ui-"));
21
+ const PORT = 3129;
22
+ const BASE = `http://127.0.0.1:${PORT}`;
23
+
24
+ // ── 起 watch 服务 ──
25
+ const srv = spawn("node", [join(root, "src", "main.js"), "watch", "-o", join(HOME, "t.db"), "-p", String(PORT)], {
26
+ cwd: root,
27
+ env: { ...process.env, HOME },
28
+ stdio: ["ignore", "pipe", "pipe"],
29
+ });
30
+ srv.stdout.on("data", () => {});
31
+ srv.stderr.on("data", (d) => process.stderr.write(`[srv] ${d}`));
32
+ const srvReady = new Promise((resolve, reject) => {
33
+ const timer = setTimeout(() => reject(new Error("服务 15s 未就绪")), 15000);
34
+ srv.stderr.on("data", (d) => {
35
+ if (String(d).includes("监控服务已启动")) {
36
+ clearTimeout(timer);
37
+ resolve();
38
+ }
39
+ });
40
+ });
41
+ await srvReady;
42
+ console.log("✓ watch 服务已启动");
43
+
44
+ let pass = 0;
45
+ let fail = 0;
46
+ const check = (name, cond, extra = "") => {
47
+ if (cond) {
48
+ pass++;
49
+ console.log(` ✓ ${name}`);
50
+ } else {
51
+ fail++;
52
+ console.log(` ✗ ${name} ${extra}`);
53
+ }
54
+ };
55
+
56
+ // ── F-11 心跳上线 ──
57
+ async function heartbeat(body) {
58
+ const r = await fetch(`${BASE}/api/watchdog/heartbeat?clientId=F-11@ubuntu`, {
59
+ method: "POST",
60
+ headers: { "Content-Type": "application/json" },
61
+ body: JSON.stringify({ hostname: "ubuntu", config: { userId: "F-11" }, processes: [], ...body }),
62
+ });
63
+ return r.json();
64
+ }
65
+ await heartbeat({});
66
+ console.log("✓ F-11 心跳上线");
67
+
68
+ // ── API 入池(expires 用秒——cookie 规范,验证前端毫秒归一) ──
69
+ const EXPIRES_IN_90D_SEC = Math.floor(Date.now() / 1000) + 90 * 86400;
70
+ const COOKIES = [
71
+ { name: "sessionid", value: "SECRET-SID-XYZ", domain: ".tiktok.com", expires: EXPIRES_IN_90D_SEC },
72
+ { name: "unique_id", value: "userA", domain: ".tiktok.com" },
73
+ ];
74
+ let addRes = await fetch(`${BASE}/api/watchdog/login-pool/add`, {
75
+ method: "POST",
76
+ headers: { "Content-Type": "application/json" },
77
+ body: JSON.stringify({ cookies: COOKIES, note: "E2E 测试账号", proxyHint: "7897" }),
78
+ });
79
+ let addData = await addRes.json();
80
+ check("API 入池成功", addRes.ok && addData.ok && addData.account.username === "userA");
81
+ const accId = addData.account.id;
82
+ check("API 视图有效期已归一为毫秒", addData.account.sessionExpiresAt > Date.now() + 89 * 86400000,
83
+ `got ${addData.account.sessionExpiresAt}`);
84
+
85
+ // ── 浏览器 ──
86
+ const browser = await chromium.launch({ executablePath: "/usr/bin/microsoft-edge", headless: true });
87
+ const ctx = await browser.newContext({ viewport: { width: 1560, height: 1000 }, deviceScaleFactor: 1.5 });
88
+ const page = await ctx.newPage();
89
+ page.on("dialog", (d) => d.accept());
90
+ const errors = [];
91
+ page.on("pageerror", (e) => errors.push(String(e)));
92
+
93
+ // 登录(当日密码 tt@YYYYMMDD)
94
+ const pad = (n) => String(n).padStart(2, "0");
95
+ const d = new Date();
96
+ await page.goto(BASE + "/", { waitUntil: "domcontentloaded" });
97
+ await page.fill("#password", `tt@${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`);
98
+ const loginResp = page.waitForResponse((r) => r.url().includes("/api/login"), { timeout: 15000 });
99
+ await page.locator("#login-form").evaluate((f) => f.submit());
100
+ await loginResp;
101
+ await page.waitForTimeout(800);
102
+ console.log("✓ 已登录");
103
+
104
+ // ── 主页:入口链接 + 嵌入区块已移除 ──
105
+ await page.goto(BASE + "/#watchdog", { waitUntil: "domcontentloaded" });
106
+ await page.waitForTimeout(1200);
107
+ const mainSrc = await page.content();
108
+ check("主页含账号中心入口链接", !!mainSrc.match(/href="\/login-pool"/));
109
+ check("主页无 cookie 明文", !mainSrc.includes("SECRET-SID-XYZ"));
110
+ check("主页嵌入区块已移除", !mainSrc.includes("wdPoolSummary") && !mainSrc.includes("wdPoolList"));
111
+ const clientsRaw = await (await fetch(`${BASE}/api/watchdog/clients`)).text();
112
+ check("/clients 无 cookie 明文", !clientsRaw.includes("SECRET-SID-XYZ"));
113
+
114
+ // ── API 指派(独占租约)→ 心跳下发命令 ──
115
+ const asgRes = await fetch(`${BASE}/api/watchdog/login-pool/assign`, {
116
+ method: "POST",
117
+ headers: { "Content-Type": "application/json" },
118
+ body: JSON.stringify({ id: accId, userId: "F-11", port: 9222 }),
119
+ });
120
+ check("API 指派成功", asgRes.ok && (await asgRes.json()).account.status === "leased");
121
+ const asgDup = await fetch(`${BASE}/api/watchdog/login-pool/assign`, {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({ id: accId, userId: "F-11", port: 9223 }),
125
+ });
126
+ check("同账号双租拒绝", !asgDup.ok);
127
+
128
+ const hbReport = await (
129
+ await fetch(`${BASE}/api/watchdog/heartbeat?clientId=F-11@ubuntu`, {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify({
133
+ hostname: "ubuntu",
134
+ config: { userId: "F-11" },
135
+ processes: [],
136
+ loginPool: { applied: [{ action: "applyLogin", accountId: accId, port: 9222, username: "userA", ok: true, persisted: true, at: Date.now() }] },
137
+ }),
138
+ })
139
+ ).json();
140
+ check("心跳响应带 loginPool 命令", hbReport.loginPool && Array.isArray(hbReport.loginPool.commands), JSON.stringify(hbReport.loginPool));
141
+ check(
142
+ "命令含明文 cookies(下发通道)",
143
+ hbReport.loginPool.commands[0].cookies.some((c) => c.value === "SECRET-SID-XYZ"),
144
+ );
145
+ check("命令含端口", hbReport.loginPool.commands[0].port === 9222);
146
+
147
+ // ── 独立页 /login-pool:新设计渲染 ──
148
+ await page.goto(BASE + "/login-pool", { waitUntil: "domcontentloaded" });
149
+ await page.waitForFunction(() => document.querySelector("#content table"), { timeout: 15000 });
150
+ const headers = await page.textContent("#content table tr:first-child");
151
+ check(
152
+ "独立页新列齐全",
153
+ ["账号", "状态", "使用者", "最近使用", "次数", "有效期", "操作", "历史"].every((h) => headers.includes(h)),
154
+ );
155
+ const row1 = await page.textContent("#content table tr:nth-child(2)");
156
+ check("状态徽章=使用中", row1.includes("使用中"));
157
+ check("使用者=机器:口", row1.includes("F-11:9222"));
158
+ const poolSrc = await page.content();
159
+ check("独立页无 cookie 明文", !poolSrc.includes("SECRET-SID-XYZ"));
160
+ check("独立页含重登录置灰按钮", await page.$$eval("#content button[disabled]", (bs) => bs.some((b) => b.textContent.includes("重登录"))));
161
+
162
+ // 有效期归一显示:应为 ~90 天后的日期(而非 1970)
163
+ const day90 = new Date(Date.now() + 90 * 86400000).toLocaleDateString("zh-CN");
164
+ check("有效期秒→毫秒归一显示正确", row1.includes(day90), `期望含 ${day90}`);
165
+ check("有效期无 1970 异常", !row1.includes("1970"));
166
+
167
+ await page.screenshot({ path: "/tmp/loginpool-ui-standalone.png", fullPage: true });
168
+ console.log("✓ 截图: 独立页新版");
169
+
170
+ // ── 释放(不清理)──
171
+ const relRes = await fetch(`${BASE}/api/watchdog/login-pool/release`, {
172
+ method: "POST",
173
+ headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify({ id: accId, clearOnNode: false }),
175
+ });
176
+ check("释放回池", relRes.ok && (await relRes.json()).account.status === "pool");
177
+
178
+ // ── suspect 拒绝无 force 指派 ──
179
+ const susRes = await fetch(`${BASE}/api/watchdog/login-pool/update`, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify({ id: accId, status: "suspect" }),
183
+ });
184
+ check("迁入 suspect", susRes.ok && (await susRes.json()).account.status === "suspect");
185
+ const asSus = await fetch(`${BASE}/api/watchdog/login-pool/assign`, {
186
+ method: "POST",
187
+ headers: { "Content-Type": "application/json" },
188
+ body: JSON.stringify({ id: accId, userId: "F-11", port: 9223 }),
189
+ });
190
+ check("suspect 无 force 拒绝指派", !asSus.ok);
191
+
192
+ // ── 停用(retired)+ 删除保护顺序 ──
193
+ const retRes = await fetch(`${BASE}/api/watchdog/login-pool/update`, {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json" },
196
+ body: JSON.stringify({ id: accId, status: "retired" }),
197
+ });
198
+ check("停用(retired)", retRes.ok && (await retRes.json()).account.status === "retired");
199
+ const delLeased = await fetch(`${BASE}/api/watchdog/login-pool/delete`, {
200
+ method: "POST",
201
+ headers: { "Content-Type": "application/json" },
202
+ body: JSON.stringify({ id: accId }),
203
+ });
204
+ check("已停用可删除", delLeased.ok);
205
+
206
+ check("两页均无 JS 错误", errors.length === 0, errors.join("; "));
207
+
208
+ await browser.close();
209
+ srv.kill();
210
+ console.log(`\n═══ 结果: ${pass} 通过, ${fail} 失败 ═══`);
211
+ process.exit(fail > 0 ? 1 : 0);