| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- /**
- * 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
- };
- }());
|