tiktok_extract_cookie_pc.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /**
  2. * ============================================================================
  3. * TikTok 登录态提取(PC 侧 / Node.js + adb)
  4. *
  5. * 用途:跟 Auto.js 版(tiktok_extract_cookie.js)等价的 PC 备用通道。
  6. * 在设备有 mount namespace 隔离、且 Auto.js 侧不便运行时,从 PC 侧
  7. * `adb shell su 0` 提取(adb shell 是宿主完整 root 视图)。
  8. *
  9. * Cookie 来源(双源互相校验):
  10. * 1) <dataDir>/shared_prefs/ttnetCookieStore.xml
  11. * 值 = Java 序列化的 hex(ACED0005…),不是明文!
  12. * 字节流结构:[TC_STRING 名称][TC_STRING 值][TC_STRING 域名][TC_STRING 路径]
  13. * → 取第 2 个 TC_STRING(0x74) 即 cookie 值。
  14. * 2) <dataDir>/app_webview/Default/Cookies (SQLite,name/value 分列)
  15. * → 用 node:sqlite 读(Node 22 内置,可能需要 --experimental-sqlite)
  16. *
  17. * 【运行】
  18. * node tiktok_extract_cookie_pc.js
  19. * 前置:adb 已 connect 设备、设备已 root、TikTok 已登录。
  20. * 产出:/sdcard/Download/tiktok_data/tiktok_cookie.txt(tiktok_stats.js 自动读取)
  21. * ============================================================================
  22. */
  23. const { spawnSync } = require("child_process");
  24. const fs = require("fs");
  25. const path = require("path");
  26. // ============================== 配置 ========================================
  27. const CFG = {
  28. pkg: "com.zhiliaoapp.musically",
  29. adbSerial: "", // 多设备时填,如 "106.119.179.124:23098";单设备留空
  30. remoteDir: "/sdcard/Download/tiktok_data",
  31. tmpDir: path.join(__dirname, "_extract_tmp"),
  32. remoteTmp: "/sdcard/Download/_ttnet_dump",
  33. cookieFileRemote: "/sdcard/Download/tiktok_data/tiktok_cookie.txt",
  34. // 最终 Cookie 串的 key 顺序(优先级从高到低)
  35. priorityKeys: [
  36. "sessionid", "sessionid_ss", "sid_tt", "uid_tt", "uid_tt_ss", "sid_guard",
  37. "msToken", "odin_tt", "passport_csrf_token", "passport_csrf_token_default",
  38. "multi_sids", "tt-target-idc", "store-idc", "ttwid", "s_v_web_id",
  39. "cmpl_token", "d_ticket", "install_id", "ttreq"
  40. ]
  41. };
  42. // ============================== adb 封装 ====================================
  43. function adb(parts) {
  44. const s = CFG.adbSerial ? ["-s", CFG.adbSerial] : [];
  45. const argv = ["adb"].concat(s, parts);
  46. const r = spawnSync(argv[0], argv.slice(1), {
  47. encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024
  48. });
  49. const out = (r.stdout || "") + (r.stderr || "");
  50. if (r.error && !out) throw r.error;
  51. return out;
  52. }
  53. function adbShell(cmdStr) { return adb(["shell", cmdStr]); }
  54. // 用 su 0 执行(本设备 su 不支持 -c,必须 su 0 sh -c '...')
  55. function su(cmdStr) { return adbShell("su 0 sh -c '" + cmdStr.replace(/'/g, "'\\''") + "'"); }
  56. function mask(v) {
  57. if (!v) return "";
  58. return v.length <= 14 ? v : v.slice(0, 10) + "..." + v.slice(-4) + " (len=" + v.length + ")";
  59. }
  60. // ============================== Java 序列化 hex 解析 =========================
  61. // 顺序扫描字节流,抽取全部 TC_STRING(0x74)
  62. function scanTcStrings(buf) {
  63. const out = [];
  64. let i = 0;
  65. while (i < buf.length) {
  66. const m = buf[i];
  67. if (m === 0x74) { // TC_STRING
  68. if (i + 2 >= buf.length) break;
  69. const len = buf.readUInt16BE(i + 1);
  70. if (i + 3 + len > buf.length) { i++; continue; }
  71. out.push(buf.slice(i + 3, i + 3 + len).toString("utf8"));
  72. i += 3 + len;
  73. } else if (m === 0x77) { // TC_BLOCKDATA + 1B len
  74. i += 2 + buf[i + 1];
  75. } else if (m === 0x7a) { // TC_BLOCKDATALONG + 4B len
  76. i += 5 + buf.readUInt32BE(i + 1);
  77. } else i++; // TC_NULL/ENDBLOCKDATA/…
  78. }
  79. return out;
  80. }
  81. function parseTtnetXml(xml) {
  82. const map = {};
  83. const re = /<string name="([^"]+)">([0-9A-Fa-f]+)<\/string>/g;
  84. let m, n = 0;
  85. while ((m = re.exec(xml)) !== null) {
  86. const key = m[1], hex = m[2];
  87. const host = key.split("|")[0];
  88. let strs;
  89. try { strs = scanTcStrings(Buffer.from(hex, "hex")); } catch (e) { continue; }
  90. if (strs.length < 2) continue;
  91. const name = strs[0], value = strs[1];
  92. if (!name || !value) continue;
  93. if (!map[name] || value.length > map[name].value.length) map[name] = { value, domain: host };
  94. n++;
  95. }
  96. console.log("[xml] 解析条目 " + n + " 个,去重后 Cookie 名 " + Object.keys(map).length + " 个");
  97. return map;
  98. }
  99. // 读 WebView 的 Cookies SQLite(可选,失败不影响)
  100. function parseWebviewCookies(dbPath) {
  101. const map = {};
  102. let sqlite;
  103. try { sqlite = require("node:sqlite"); }
  104. catch (e) {
  105. console.log("[sqlite] node:sqlite 不可用(Node 22 需加 --experimental-sqlite),跳过");
  106. return map;
  107. }
  108. try {
  109. const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
  110. const rows = db.prepare("SELECT name, value, host_key FROM cookies").all();
  111. let n = 0;
  112. for (const r of rows) {
  113. if (!r.name || !r.value) continue;
  114. if (!map[r.name] || r.value.length > map[r.name].value.length) {
  115. map[r.name] = { value: r.value, domain: r.host_key || "" };
  116. }
  117. n++;
  118. }
  119. db.close();
  120. console.log("[sqlite] 读到 " + rows.length + " 行,有效 Cookie 名 " + Object.keys(map).length + " 个");
  121. } catch (e) {
  122. console.log("[sqlite] 解析失败(不影响主流程): " + e.message);
  123. }
  124. return map;
  125. }
  126. function mergeInto(dst, src) {
  127. for (const k of Object.keys(src)) {
  128. if (!dst[k] || src[k].value.length > dst[k].value.length) dst[k] = src[k];
  129. }
  130. return dst;
  131. }
  132. function buildCookieString(map) {
  133. const parts = [], used = new Set();
  134. for (const k of CFG.priorityKeys) {
  135. if (map[k]) { parts.push(k + "=" + map[k].value); used.add(k); }
  136. }
  137. for (const k of Object.keys(map).sort()) {
  138. if (!used.has(k)) parts.push(k + "=" + map[k].value);
  139. }
  140. return parts.join("; ");
  141. }
  142. // ============================== 主流程 ======================================
  143. function main() {
  144. console.log("==== TikTok 登录态提取(PC/adb) ====\n");
  145. // 0) 设备检查
  146. const devs = adb(["devices"]).trim().split("\n").slice(1)
  147. .filter(l => l.trim() && !l.startsWith("*")).map(l => l.split("\t")[0]);
  148. console.log("[adb] 在线设备: " + (devs.join(", ") || "(无)"));
  149. if (devs.length === 0) { console.log("✗ 无在线设备,请先 adb connect"); process.exit(1); }
  150. if (CFG.adbSerial && !devs.includes(CFG.adbSerial)) {
  151. console.log("✗ 指定的设备 " + CFG.adbSerial + " 不在线"); process.exit(1);
  152. }
  153. // 1) 定位数据目录(pm dump 最权威),并确认可读
  154. let dataDir = "";
  155. const dump = su("pm dump " + CFG.pkg + " 2>&1 | grep -i -m1 datadir");
  156. const dm = dump.match(/dataDir=([^\s,]+)/);
  157. if (dm) dataDir = dm[1];
  158. if (!dataDir) {
  159. for (const c of ["/data/user/0/" + CFG.pkg, "/data/data/" + CFG.pkg]) {
  160. if (su("test -d " + c + " && echo YES || echo NO").includes("YES")) { dataDir = c; break; }
  161. }
  162. }
  163. if (!dataDir) { console.log("✗ 未能定位数据目录(未 root / TikTok 未安装?)"); process.exit(1); }
  164. console.log("[dir] " + dataDir + " -> " +
  165. (su("test -d " + dataDir + "/shared_prefs && echo YES || echo NO").includes("YES") ? "可读 ✓" : "不可读 ✗"));
  166. fs.mkdirSync(CFG.tmpDir, { recursive: true });
  167. adbShell("mkdir -p " + CFG.remoteDir);
  168. // 2) 把两个 Cookie 存储拷到 /sdcard 再 pull
  169. const stage = [
  170. ["shared_prefs/ttnetCookieStore.xml", "ttnetCookieStore.xml"],
  171. ["app_webview/Default/Cookies", "Cookies"],
  172. ["app_webview/Default/Cookies-wal", "Cookies-wal"],
  173. ["app_webview/Default/Cookies-shm", "Cookies-shm"]
  174. ];
  175. const staged = {};
  176. for (const [rel, name] of stage) {
  177. const remote = CFG.remoteTmp + "_" + name;
  178. su("cp '" + dataDir + "/" + rel + "' '" + remote + "' 2>/dev/null; chmod 644 '" + remote + "' 2>/dev/null");
  179. const local = path.join(CFG.tmpDir, name);
  180. adb(["pull", remote, local]);
  181. const ok = fs.existsSync(local) && fs.statSync(local).size > 0;
  182. console.log("[stage] " + rel + " -> " + (ok ? "✓ " + fs.statSync(local).size + " 字节" : "✗ 缺失"));
  183. if (ok) staged[name] = local;
  184. }
  185. if (!staged["ttnetCookieStore.xml"] && !staged["Cookies"]) {
  186. console.log("✗ 两个 Cookie 存储都拿不到"); process.exit(1);
  187. }
  188. // 3) 解析(双源合并)
  189. let map = {};
  190. if (staged["ttnetCookieStore.xml"]) {
  191. map = mergeInto(map, parseTtnetXml(fs.readFileSync(staged["ttnetCookieStore.xml"], "utf8")));
  192. }
  193. if (staged["Cookies"]) {
  194. map = mergeInto(map, parseWebviewCookies(staged["Cookies"]));
  195. }
  196. const keys = Object.keys(map).sort();
  197. if (keys.length === 0) { console.log("✗ 未解析到任何 Cookie,TikTok 是否已登录?"); process.exit(1); }
  198. const essential = ["sessionid", "sid_tt", "uid_tt", "msToken"];
  199. const missing = essential.filter(k => !map[k]);
  200. console.log("\n---- Cookie(脱敏)----");
  201. for (const k of keys) {
  202. const tag = CFG.priorityKeys.includes(k) ? "★" : " ";
  203. console.log(" " + tag + " " + k.padEnd(34) + "= " + mask(map[k].value));
  204. }
  205. console.log("\n" +
  206. (missing.length ? "⚠ 缺少关键项: " + missing.join(", ")
  207. : "✓ 关键登录态齐全 (sessionid / sid_tt / uid_tt / msToken)"));
  208. // 4) 写本地 + push 回设备
  209. const cookieStr = buildCookieString(map);
  210. const localCookie = path.join(CFG.tmpDir, "tiktok_cookie.txt");
  211. fs.writeFileSync(localCookie, cookieStr);
  212. console.log("\n[cookie] 串长度 " + cookieStr.length + ",段数 " + cookieStr.split("; ").length);
  213. const pushOut = adb(["push", localCookie, CFG.cookieFileRemote]);
  214. console.log("[push] " + pushOut.trim().split("\n")[0]);
  215. const back = adb(["shell", "cat " + CFG.cookieFileRemote]).trim();
  216. console.log("[verify] 设备端长度 " + back.length + (back === cookieStr ? " ✓ 与本地一致" : " ✗ 不一致"));
  217. console.log("\n==== 完成 ====");
  218. console.log("下一步:运行 tiktok_stats.js(会自动读取 " + CFG.cookieFileRemote + ")");
  219. console.log("注:官方评论接口已被 A-Bogus 签名拦截,登录态对评论无帮助;");
  220. console.log(" 评论数据由 tiktok_stats.js 走镜像接口获取。");
  221. }
  222. main();