| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- /**
- * ============================================================================
- * TikTok 登录态提取(PC 侧 / Node.js + adb)
- *
- * 用途:跟 Auto.js 版(tiktok_extract_cookie.js)等价的 PC 备用通道。
- * 在设备有 mount namespace 隔离、且 Auto.js 侧不便运行时,从 PC 侧
- * `adb shell su 0` 提取(adb shell 是宿主完整 root 视图)。
- *
- * Cookie 来源(双源互相校验):
- * 1) <dataDir>/shared_prefs/ttnetCookieStore.xml
- * 值 = Java 序列化的 hex(ACED0005…),不是明文!
- * 字节流结构:[TC_STRING 名称][TC_STRING 值][TC_STRING 域名][TC_STRING 路径]
- * → 取第 2 个 TC_STRING(0x74) 即 cookie 值。
- * 2) <dataDir>/app_webview/Default/Cookies (SQLite,name/value 分列)
- * → 用 node:sqlite 读(Node 22 内置,可能需要 --experimental-sqlite)
- *
- * 【运行】
- * node tiktok_extract_cookie_pc.js
- * 前置:adb 已 connect 设备、设备已 root、TikTok 已登录。
- * 产出:/sdcard/Download/tiktok_data/tiktok_cookie.txt(tiktok_stats.js 自动读取)
- * ============================================================================
- */
- const { spawnSync } = require("child_process");
- const fs = require("fs");
- const path = require("path");
- // ============================== 配置 ========================================
- const CFG = {
- pkg: "com.zhiliaoapp.musically",
- adbSerial: "", // 多设备时填,如 "106.119.179.124:23098";单设备留空
- remoteDir: "/sdcard/Download/tiktok_data",
- tmpDir: path.join(__dirname, "_extract_tmp"),
- remoteTmp: "/sdcard/Download/_ttnet_dump",
- cookieFileRemote: "/sdcard/Download/tiktok_data/tiktok_cookie.txt",
- // 最终 Cookie 串的 key 顺序(优先级从高到低)
- priorityKeys: [
- "sessionid", "sessionid_ss", "sid_tt", "uid_tt", "uid_tt_ss", "sid_guard",
- "msToken", "odin_tt", "passport_csrf_token", "passport_csrf_token_default",
- "multi_sids", "tt-target-idc", "store-idc", "ttwid", "s_v_web_id",
- "cmpl_token", "d_ticket", "install_id", "ttreq"
- ]
- };
- // ============================== adb 封装 ====================================
- function adb(parts) {
- const s = CFG.adbSerial ? ["-s", CFG.adbSerial] : [];
- const argv = ["adb"].concat(s, parts);
- const r = spawnSync(argv[0], argv.slice(1), {
- encoding: "utf8", windowsHide: true, maxBuffer: 64 * 1024 * 1024
- });
- const out = (r.stdout || "") + (r.stderr || "");
- if (r.error && !out) throw r.error;
- return out;
- }
- function adbShell(cmdStr) { return adb(["shell", cmdStr]); }
- // 用 su 0 执行(本设备 su 不支持 -c,必须 su 0 sh -c '...')
- function su(cmdStr) { return adbShell("su 0 sh -c '" + cmdStr.replace(/'/g, "'\\''") + "'"); }
- function mask(v) {
- if (!v) return "";
- return v.length <= 14 ? v : v.slice(0, 10) + "..." + v.slice(-4) + " (len=" + v.length + ")";
- }
- // ============================== Java 序列化 hex 解析 =========================
- // 顺序扫描字节流,抽取全部 TC_STRING(0x74)
- function scanTcStrings(buf) {
- const out = [];
- let i = 0;
- while (i < buf.length) {
- const m = buf[i];
- if (m === 0x74) { // TC_STRING
- if (i + 2 >= buf.length) break;
- const len = buf.readUInt16BE(i + 1);
- if (i + 3 + len > buf.length) { i++; continue; }
- out.push(buf.slice(i + 3, i + 3 + len).toString("utf8"));
- i += 3 + len;
- } else if (m === 0x77) { // TC_BLOCKDATA + 1B len
- i += 2 + buf[i + 1];
- } else if (m === 0x7a) { // TC_BLOCKDATALONG + 4B len
- i += 5 + buf.readUInt32BE(i + 1);
- } else i++; // TC_NULL/ENDBLOCKDATA/…
- }
- return out;
- }
- function parseTtnetXml(xml) {
- const map = {};
- const re = /<string name="([^"]+)">([0-9A-Fa-f]+)<\/string>/g;
- let m, n = 0;
- while ((m = re.exec(xml)) !== null) {
- const key = m[1], hex = m[2];
- const host = key.split("|")[0];
- let strs;
- try { strs = scanTcStrings(Buffer.from(hex, "hex")); } catch (e) { continue; }
- if (strs.length < 2) continue;
- const name = strs[0], value = strs[1];
- if (!name || !value) continue;
- if (!map[name] || value.length > map[name].value.length) map[name] = { value, domain: host };
- n++;
- }
- console.log("[xml] 解析条目 " + n + " 个,去重后 Cookie 名 " + Object.keys(map).length + " 个");
- return map;
- }
- // 读 WebView 的 Cookies SQLite(可选,失败不影响)
- function parseWebviewCookies(dbPath) {
- const map = {};
- let sqlite;
- try { sqlite = require("node:sqlite"); }
- catch (e) {
- console.log("[sqlite] node:sqlite 不可用(Node 22 需加 --experimental-sqlite),跳过");
- return map;
- }
- try {
- const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
- const rows = db.prepare("SELECT name, value, host_key FROM cookies").all();
- let n = 0;
- for (const r of rows) {
- if (!r.name || !r.value) continue;
- if (!map[r.name] || r.value.length > map[r.name].value.length) {
- map[r.name] = { value: r.value, domain: r.host_key || "" };
- }
- n++;
- }
- db.close();
- console.log("[sqlite] 读到 " + rows.length + " 行,有效 Cookie 名 " + Object.keys(map).length + " 个");
- } catch (e) {
- console.log("[sqlite] 解析失败(不影响主流程): " + e.message);
- }
- return map;
- }
- function mergeInto(dst, src) {
- for (const k of Object.keys(src)) {
- if (!dst[k] || src[k].value.length > dst[k].value.length) dst[k] = src[k];
- }
- return dst;
- }
- function buildCookieString(map) {
- const parts = [], used = new Set();
- for (const k of CFG.priorityKeys) {
- if (map[k]) { parts.push(k + "=" + map[k].value); used.add(k); }
- }
- for (const k of Object.keys(map).sort()) {
- if (!used.has(k)) parts.push(k + "=" + map[k].value);
- }
- return parts.join("; ");
- }
- // ============================== 主流程 ======================================
- function main() {
- console.log("==== TikTok 登录态提取(PC/adb) ====\n");
- // 0) 设备检查
- const devs = adb(["devices"]).trim().split("\n").slice(1)
- .filter(l => l.trim() && !l.startsWith("*")).map(l => l.split("\t")[0]);
- console.log("[adb] 在线设备: " + (devs.join(", ") || "(无)"));
- if (devs.length === 0) { console.log("✗ 无在线设备,请先 adb connect"); process.exit(1); }
- if (CFG.adbSerial && !devs.includes(CFG.adbSerial)) {
- console.log("✗ 指定的设备 " + CFG.adbSerial + " 不在线"); process.exit(1);
- }
- // 1) 定位数据目录(pm dump 最权威),并确认可读
- let dataDir = "";
- const dump = su("pm dump " + CFG.pkg + " 2>&1 | grep -i -m1 datadir");
- const dm = dump.match(/dataDir=([^\s,]+)/);
- if (dm) dataDir = dm[1];
- if (!dataDir) {
- for (const c of ["/data/user/0/" + CFG.pkg, "/data/data/" + CFG.pkg]) {
- if (su("test -d " + c + " && echo YES || echo NO").includes("YES")) { dataDir = c; break; }
- }
- }
- if (!dataDir) { console.log("✗ 未能定位数据目录(未 root / TikTok 未安装?)"); process.exit(1); }
- console.log("[dir] " + dataDir + " -> " +
- (su("test -d " + dataDir + "/shared_prefs && echo YES || echo NO").includes("YES") ? "可读 ✓" : "不可读 ✗"));
- fs.mkdirSync(CFG.tmpDir, { recursive: true });
- adbShell("mkdir -p " + CFG.remoteDir);
- // 2) 把两个 Cookie 存储拷到 /sdcard 再 pull
- const stage = [
- ["shared_prefs/ttnetCookieStore.xml", "ttnetCookieStore.xml"],
- ["app_webview/Default/Cookies", "Cookies"],
- ["app_webview/Default/Cookies-wal", "Cookies-wal"],
- ["app_webview/Default/Cookies-shm", "Cookies-shm"]
- ];
- const staged = {};
- for (const [rel, name] of stage) {
- const remote = CFG.remoteTmp + "_" + name;
- su("cp '" + dataDir + "/" + rel + "' '" + remote + "' 2>/dev/null; chmod 644 '" + remote + "' 2>/dev/null");
- const local = path.join(CFG.tmpDir, name);
- adb(["pull", remote, local]);
- const ok = fs.existsSync(local) && fs.statSync(local).size > 0;
- console.log("[stage] " + rel + " -> " + (ok ? "✓ " + fs.statSync(local).size + " 字节" : "✗ 缺失"));
- if (ok) staged[name] = local;
- }
- if (!staged["ttnetCookieStore.xml"] && !staged["Cookies"]) {
- console.log("✗ 两个 Cookie 存储都拿不到"); process.exit(1);
- }
- // 3) 解析(双源合并)
- let map = {};
- if (staged["ttnetCookieStore.xml"]) {
- map = mergeInto(map, parseTtnetXml(fs.readFileSync(staged["ttnetCookieStore.xml"], "utf8")));
- }
- if (staged["Cookies"]) {
- map = mergeInto(map, parseWebviewCookies(staged["Cookies"]));
- }
- const keys = Object.keys(map).sort();
- if (keys.length === 0) { console.log("✗ 未解析到任何 Cookie,TikTok 是否已登录?"); process.exit(1); }
- const essential = ["sessionid", "sid_tt", "uid_tt", "msToken"];
- const missing = essential.filter(k => !map[k]);
- console.log("\n---- Cookie(脱敏)----");
- for (const k of keys) {
- const tag = CFG.priorityKeys.includes(k) ? "★" : " ";
- console.log(" " + tag + " " + k.padEnd(34) + "= " + mask(map[k].value));
- }
- console.log("\n" +
- (missing.length ? "⚠ 缺少关键项: " + missing.join(", ")
- : "✓ 关键登录态齐全 (sessionid / sid_tt / uid_tt / msToken)"));
- // 4) 写本地 + push 回设备
- const cookieStr = buildCookieString(map);
- const localCookie = path.join(CFG.tmpDir, "tiktok_cookie.txt");
- fs.writeFileSync(localCookie, cookieStr);
- console.log("\n[cookie] 串长度 " + cookieStr.length + ",段数 " + cookieStr.split("; ").length);
- const pushOut = adb(["push", localCookie, CFG.cookieFileRemote]);
- console.log("[push] " + pushOut.trim().split("\n")[0]);
- const back = adb(["shell", "cat " + CFG.cookieFileRemote]).trim();
- console.log("[verify] 设备端长度 " + back.length + (back === cookieStr ? " ✓ 与本地一致" : " ✗ 不一致"));
- console.log("\n==== 完成 ====");
- console.log("下一步:运行 tiktok_stats.js(会自动读取 " + CFG.cookieFileRemote + ")");
- console.log("注:官方评论接口已被 A-Bogus 签名拦截,登录态对评论无帮助;");
- console.log(" 评论数据由 tiktok_stats.js 走镜像接口获取。");
- }
- main();
|