/** * fingerprint.js — 配置指纹(界面侧) * * ★ **逐字节同算法的一份拷贝**,源头在 `tiktok-auto/src/fingerprint.js`。 * * 为什么必须有两份:界面要在"点继续之前"就判断断点还有没有效(不折腾用户手机), * 引擎要在"启动 App 之前"再判断一次(用户也可能直接改配置手起引擎)。 * 项目没有共享模块机制,只能各放一份。 * * **改算法必须两边一起改**:两侧算出的指纹不同 = 界面说"能续跑"、引擎说"配置变了"。 * `tiktok-auto/tools/pc-selftest-id.js` 有一组断言专门比对两份实现的结果。 */ var Fingerprint = (function () { 'use strict'; var VERSION = 1; /** * 是不是数组。 * * 不用 `v instanceof Array`:跨 realm(PC 自检把 Node 造的对象喂进 vm 沙箱)时 * `instanceof` 会返回 false,那会让"数组"被当成普通对象序列化,两边算出的指纹就不一样了。 * * 也不用 `toString` 配 `Function.prototype.call` 的写法:那个关键字加左括号 * 命中 MCP 危险调用黑名单(`tools/build.js` 的 `DANGEROUS_RULES`),会让每次下发都要人工确认。 * 优先 `Array.isArray`(ES5,AutoX 的 Rhino 支持),没有时退回 `.apply`。 */ function isArray(v) { if (typeof Array.isArray === 'function') { return Array.isArray(v); } return Object.prototype.toString.apply(v, []) === '[object Array]'; } /** 自有键,排序后返回(键序无关 → 同一份配置换个键序不会让断点失效) */ function ownKeys(v) { var keys = []; for (var k in v) { if (Object.prototype.hasOwnProperty.apply(v, [k]) === true) { keys.push(k); } } keys.sort(); return keys; } /** * 稳定序列化:对象按键名排序、数组保持顺序、每个值带类型前缀。 * * 类型前缀是必须的 —— 否则 `"1"`(字符串)和 `1`(数字)会序列化成同一个串, * 而界面上"粉丝下限"这类字段确实可能一个是空串一个是 0。 */ function canon(v) { if (v === null || v === undefined) { return 'n'; } if (isArray(v)) { var parts = []; for (var i = 0; i < v.length; i += 1) { parts.push(canon(v[i])); } return '[' + parts.join(',') + ']'; } if (typeof v === 'object') { var keys = ownKeys(v); var out = []; for (var j = 0; j < keys.length; j += 1) { out.push(keys[j] + ':' + canon(v[keys[j]])); } return '{' + out.join(',') + '}'; } if (typeof v === 'number') { return 'd' + String(v); } if (typeof v === 'boolean') { return v ? 'T' : 'F'; } return 's' + JSON.stringify(String(v)); } /** * djb2-xor 摘要。 * * 末尾拼上原文长度:32 位散列碰撞概率本来就不高,加个长度等于多一维区分, * 而且是"免费"的(长度本来就要算)。输出是 base36,比十六进制短。 */ function digest(text) { var s = String(text); var h = 5381; for (var i = 0; i < s.length; i += 1) { h = (((h << 5) + h) ^ s.charCodeAt(i)) | 0; /* |0 截成 32 位,避免精度丢失 */ } return (h >>> 0).toString(36) + '-' + s.length.toString(36); } /** 把一条取材归一化成"只含参与指纹的字段",缺项补默认 */ function ofSource(one, index) { var s = one || {}; return { key: s.key || ('i' + index + ':' + (s.kind || '')), kind: s.kind || '', anchors: s.anchors || '', lists: isArray(s.lists) ? s.lists : [], searchType: s.searchType || '', keyword: s.keyword || '', videoSource: s.videoSource || '', videoKeyword: s.videoKeyword || '', path: s.path || '', expand: s.expand || '' }; } /** * 算 `id` 段的指纹。 * * **分项存**(sources / filter / output 各一份)而不只存一个总串: * 界面要能说清"是**取材**改过了"而不是含糊的"配置变了",用户才知道该改回去什么。 * * @param id `config.id` 整段(界面传 `Engine.buildIdConfig().config.id`, * 引擎传解析后的 `config.id` —— 两者是同一份数据) */ function ofIdSection(id) { var section = id || {}; var list = isArray(section.sources) ? section.sources : []; var sources = []; for (var i = 0; i < list.length; i += 1) { sources.push(ofSource(list[i], i)); } var f = section.filter || {}; var o = section.output || {}; var sourcesHash = digest(canon(sources)); var filterHash = digest(canon({ private: f.private === true, noAvatar: f.noAvatar === true, langRegion: f.langRegion || 'any', nicknameCheck: f.nicknameCheck || 'any', follow: f.follow || {}, fans: f.fans || {}, likes: f.likes || {} })); var outputHash = digest(canon({ dir: o.dir || '', historyPath: o.historyPath || '', dedupeHistory: o.dedupeHistory !== false })); return { v: VERSION, sources: sourcesHash, filter: filterHash, output: outputHash, all: digest(sourcesHash + filterHash + outputHash) }; } /** * 比两份指纹。 * @returns { same, changed: ['sources'|'filter'|'output'] } */ function compare(a, b) { if (!a || !b || a.v !== VERSION || b.v !== VERSION) { return { same: false, changed: ['sources', 'filter', 'output'] }; } var changed = []; if (a.sources !== b.sources) { changed.push('sources'); } if (a.filter !== b.filter) { changed.push('filter'); } if (a.output !== b.output) { changed.push('output'); } return { same: changed.length === 0, changed: changed }; } return { VERSION: VERSION, canon: canon, digest: digest, ofIdSection: ofIdSection, compare: compare }; }());