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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tt-help-cli-ycl",
3
- "version": "1.4.182",
3
+ "version": "1.4.183",
4
4
  "description": "TikTok user & video data scraper - extract ttSeller, verified, locationCreated from HTML source",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 → 注入 → 导航 → passport 验证身份。
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
- await context.addCookies(toPlaywrightCookies(account.cookies));
120
- await gotoTiktok(page);
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
- async function verifyLoggedIn(context) {
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 {