tt-help-cli-ycl 1.4.182 → 1.4.183
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 与心跳命令通道无关:acquire 响应直接携带明文 cookies(POST 专属通道,与心跳下发同级),
|
|
5
5
|
* 注入由业务进程在自己持有的浏览器实例上执行(POC 实证 SOP,见知识库 edge-login-transfer.md):
|
|
6
|
-
* context.clearCookies() + addCookies → 导航 tiktok.com → passport/web/account/info 验证身份
|
|
6
|
+
* context.clearCookies() + 挂载一次性设备档案清档 + addCookies → 导航 tiktok.com → passport/web/account/info 验证身份
|
|
7
7
|
*
|
|
8
8
|
* 语义约定(2026-10-26 定稿:以号为主键,无幂等/无端口键):
|
|
9
9
|
* - acquire: 挑最久未用的空闲号 → 租约 → 响应带明文 cookies;池空/服务器不可达抛错
|
|
@@ -93,6 +93,34 @@ export async function gotoTiktok(page, timeout = 45000) {
|
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* 挂载「下一次文档加载起点」的一次性设备档案清空(addInitScript + token 自限,无需拆卸)。
|
|
98
|
+
* token 不匹配才清,清后写入新 token:下一次导航起 token 相等直接跳过,不误伤账号正常续用。
|
|
99
|
+
* 为什么不在活页上直接清:活页的 TikTok JS 会在清理后数秒内把旧设备 id 写回(实测),
|
|
100
|
+
* 必须赶在「新文档自身 JS 运行前」清,新会话才以全新设备初始化(配对软封修复,2026-11-01 实验)。
|
|
101
|
+
* 挂载失败仅告警不阻断(退化为旧行为)。返回的函数为兼容保留的空操作。
|
|
102
|
+
*/
|
|
103
|
+
export async function armDeviceWipeOnNextLoad(context, page) {
|
|
104
|
+
try {
|
|
105
|
+
const token = `w${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
106
|
+
await page.addInitScript((t) => {
|
|
107
|
+
try {
|
|
108
|
+
if (localStorage.getItem("__tt_wipe_token") === t) return;
|
|
109
|
+
localStorage.clear();
|
|
110
|
+
sessionStorage.clear();
|
|
111
|
+
localStorage.setItem("__tt_wipe_token", t);
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
try { if (window.caches && caches.keys) { caches.keys().then((ks) => Promise.all(ks.map((k) => caches.delete(k)))).catch(() => {}); } } catch (e) {}
|
|
114
|
+
try { if (navigator.serviceWorker && navigator.serviceWorker.getRegistrations) { navigator.serviceWorker.getRegistrations().then((rs) => Promise.all(rs.map((r) => r.unregister()))).catch(() => {}); } } catch (e) {}
|
|
115
|
+
try { if (window.indexedDB && indexedDB.databases) { indexedDB.databases().then((dbs) => (dbs || []).forEach((d) => { try { const rq = indexedDB.deleteDatabase(d.name); rq.onsuccess = rq.onerror = rq.onblocked = () => {}; } catch (e) {} })).catch(() => {}); } } catch (e) {}
|
|
116
|
+
}, token);
|
|
117
|
+
return async () => {};
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`[登录池] 设备档案清理挂载失败(忽略,退化为旧行为): ${String(e).slice(0, 120)}`);
|
|
120
|
+
return async () => {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
function toPlaywrightCookies(cookies) {
|
|
97
125
|
return (cookies || []).map((c) => ({
|
|
98
126
|
name: c.name,
|
|
@@ -107,7 +135,7 @@ function toPlaywrightCookies(cookies) {
|
|
|
107
135
|
|
|
108
136
|
/**
|
|
109
137
|
* 原地注入账号(同一 Edge 运行实例,不重启浏览器;POC T2 实证)
|
|
110
|
-
* 清旧 cookie →
|
|
138
|
+
* 清旧 cookie → 挂载一次性清档 → 注入 → 导航(新文档起点清上一账号设备档案)→ passport 验证身份。
|
|
111
139
|
* @returns {Promise<{ok: boolean, username?: string, reason?: string}>}
|
|
112
140
|
*/
|
|
113
141
|
export async function injectPoolAccount(page, account, { verifyUsername } = {}) {
|
|
@@ -116,8 +144,14 @@ export async function injectPoolAccount(page, account, { verifyUsername } = {})
|
|
|
116
144
|
}
|
|
117
145
|
const context = page.context();
|
|
118
146
|
await context.clearCookies();
|
|
119
|
-
|
|
120
|
-
await
|
|
147
|
+
// 换号配对软封修复(2026-11-01 实验):新文档起点清上一账号设备档案,让新会话以全新设备初始化
|
|
148
|
+
const disarmWipe = await armDeviceWipeOnNextLoad(context, page);
|
|
149
|
+
try {
|
|
150
|
+
await context.addCookies(toPlaywrightCookies(account.cookies));
|
|
151
|
+
await gotoTiktok(page);
|
|
152
|
+
} finally {
|
|
153
|
+
await disarmWipe();
|
|
154
|
+
}
|
|
121
155
|
const id = await whoAmI(page);
|
|
122
156
|
if (!id.hasAccount) {
|
|
123
157
|
return {
|
|
@@ -19,6 +19,7 @@ import os from "os";
|
|
|
19
19
|
import path from "path";
|
|
20
20
|
import { existsSync, rmSync } from "fs";
|
|
21
21
|
import { ensureBrowserReady, killEdgeProcesses } from "../lib/browser/cdp.js";
|
|
22
|
+
import { armDeviceWipeOnNextLoad } from "../lib/login-pool-client.js";
|
|
22
23
|
|
|
23
24
|
const PERSIST_WAIT_MS = 35 * 1000; // Edge cookie flush 周期裕量(实证 ≥35s)
|
|
24
25
|
const ACCOUNT_INFO_URL = "https://www.tiktok.com/passport/web/account/info/";
|
|
@@ -54,10 +55,13 @@ function normalizeProxy(proxyHint, configProxy) {
|
|
|
54
55
|
return configProxy || null;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/** 活实例登录验证:page 上下文 fetch account/info,拿 username 即已登录
|
|
58
|
-
|
|
58
|
+
/** 活实例登录验证:page 上下文 fetch account/info,拿 username 即已登录
|
|
59
|
+
* wipeOnce=true 时在本次导航起点一次性清上一账号设备档案(注入首次用;重启复验不再清,避免设备 id 二次翻转) */
|
|
60
|
+
async function verifyLoggedIn(context, { wipeOnce = false } = {}) {
|
|
59
61
|
const page = await context.newPage();
|
|
62
|
+
let disarm = null;
|
|
60
63
|
try {
|
|
64
|
+
if (wipeOnce) disarm = await armDeviceWipeOnNextLoad(context, page);
|
|
61
65
|
await page.goto("https://www.tiktok.com/", {
|
|
62
66
|
waitUntil: "domcontentloaded",
|
|
63
67
|
timeout: 30000,
|
|
@@ -91,6 +95,7 @@ async function verifyLoggedIn(context) {
|
|
|
91
95
|
);
|
|
92
96
|
return r;
|
|
93
97
|
} finally {
|
|
98
|
+
if (disarm) await disarm();
|
|
94
99
|
await withTimeout(page.close(), 5000, "page.close").catch(() => {});
|
|
95
100
|
}
|
|
96
101
|
}
|
|
@@ -146,7 +151,8 @@ async function applyLoginToPortInner({ port, cookies, proxyHint, username, confi
|
|
|
146
151
|
const browser = await ensureBrowserReady({ port, userDataDir: dir, proxyServer });
|
|
147
152
|
const context = browser.contexts()[0] || (await browser.newContext());
|
|
148
153
|
|
|
149
|
-
// 3. 注入(先清旧 cookie
|
|
154
|
+
// 3. 注入(先清旧 cookie,避免同域残留干扰判定;验证导航起点一次性清上一账号设备档案——
|
|
155
|
+
// profile 持久化的 localStorage 跨账号残留,换号同款配对软封风险)
|
|
150
156
|
log(`端口 ${port}: 注入 ${cookies.length} 条 cookies...`);
|
|
151
157
|
try {
|
|
152
158
|
await context.clearCookies();
|
|
@@ -163,9 +169,9 @@ async function applyLoginToPortInner({ port, cookies, proxyHint, username, confi
|
|
|
163
169
|
})),
|
|
164
170
|
);
|
|
165
171
|
|
|
166
|
-
// 4.
|
|
167
|
-
log(`端口 ${port}:
|
|
168
|
-
const v = await verifyLoggedIn(context);
|
|
172
|
+
// 4. 活实例验证(本次导航起点一次性清设备档案)
|
|
173
|
+
log(`端口 ${port}: 活实例验证(含设备档案清理)...`);
|
|
174
|
+
const v = await verifyLoggedIn(context, { wipeOnce: true });
|
|
169
175
|
if (!v.username) {
|
|
170
176
|
await withTimeout(browser.close(), 20000, "browser.close").catch(() => {});
|
|
171
177
|
return {
|