// ==UserScript== // @name 手势归组校验工具 - 分组检查 // @namespace http://113.207.49.90/ // @version 2.3.0 // @description 自动校验:切帧即校验(含缓存帧);打开题包后自动统计整个题包半身框总数并在超过上限(默认33)时报错;检查归组、手势框<半身框<行人框包含关系、无需标注却有标注的帧 // @author 粟镇 // @match *://*/w/task.html* // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ============================================================ // 配置区(可在面板“设置”里改,改完自动记住) // ============================================================ const SETTINGS_KEY = 'sz-gesture-settings-v3'; const STORAGE_KEY = 'sz-gesture-package-stats-v3'; const DEFAULT_SETTINGS = { clsFull: 'full_bbox', clsHalf: 'half_bbox_xywh', clsHand: 'handbboxxywh', tolPx: 0, // 越界容差(像素);0 = 严格不能超出 totalOverride: '', // 读不到总帧数时手动填 halfLimit: 33 // 整个题包半身框总数上限,超过即报错;0 表示不限制 }; // 标签兜底:没有 class.pname 时按显示名判断 const LABEL_FULL = ['行人主框', '行人框', '全身框', '行人']; const LABEL_HALF = ['半身框', '半身']; const LABEL_HAND = ['手势框', '手势']; const CAPTURE_MAX = 300; // 最多保留的接口响应数 const CAPTURE_MAX_BYTES = 8 * 1024 * 1024; // 单条超过 8MB 不解析 const SCAN_DEPTH_MAX = 16; // ============================================================ // 状态 // ============================================================ let cachedData = null; let panel = null; let panelReady = false; let lastRequest = null; // 最近一次 get-mark-data 请求 let scanning = false; // 是否正在接口批量统计 let currentImage = { w: null, h: null }; let settings = Object.assign({}, DEFAULT_SETTINGS); let cachedFrameKey = null; // 当前 cachedData 对应的帧(图片路径) let cachedFrameNo = null; // 当前 cachedData 对应的帧号 let lastWatchedFrame = null; // 帧号监听:上次看到的帧号 let autoBatchRanSig = null; // 已自动统计过的题包,避免重复 let autoBatchRunning = false; // 捕获到的所有接口响应:{ url, method, json, frames:[...] } let captured = []; // 全题包统计:frames 以“帧唯一键”索引,避免翻页与接口统计重复计数 // { taskSig, totalFrames, frames: { key: {half, marks, status, no, src} } } let pkgStats = { taskSig: '', totalFrames: null, frames: {} }; // ============================================================ // 拦截 XHR / fetch // ============================================================ function hookXHR() { const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function (method, url) { this._szUrl = url; this._szMethod = method; this._szHeaders = {}; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function (name, value) { if (this._szHeaders) this._szHeaders[name] = value; return origSetHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function (body) { const self = this; const isMarkData = self._szUrl && self._szUrl.indexOf('get-mark-data') !== -1; if (isMarkData) { lastRequest = { url: self._szUrl, method: self._szMethod || 'GET', body: body, headers: self._szHeaders || {}, frameNoAtSend: resolveFrameNo(self._szUrl, body) }; } let handled = false; const onDone = function () { if (handled || self.readyState !== 4) return; handled = true; if (self.status !== 200) return; const resp = readJsonLoose(self.responseText, self.response); if (!resp) return; captureResponse(self._szUrl, self._szMethod, resp); if (isMarkData && resp.code === 0 && resp.data) { const md = resp.data.markData || {}; if (md.width) currentImage.w = md.width; if (md.height) currentImage.h = md.height; let frameNo = lastRequest.frameNoAtSend; if (frameNo === null || frameNo === undefined) frameNo = extractFrameNo(resp.data); if (frameNo === null || frameNo === undefined) { const ind = readFrameIndicator(); if (ind) frameNo = ind.cur; } setCachedData(resp.data, frameNo); recordFrame(resp.data.markData ? resp.data : { markData: md, mark_status: resp.data.mark_status }, frameNo, 'walk'); if (panelReady) autoRunCheck(); // 首次拿到数据后,自动统计整个题包(只跑一次) maybeAutoBatch(); } }; try { self.addEventListener('load', onDone); } catch (e) {} try { self.addEventListener('readystatechange', onDone); } catch (e) {} return origSend.apply(this, arguments); }; } function hookFetch() { if (typeof window === 'undefined' || !window.fetch) return; const origFetch = window.fetch; window.fetch = function () { const args = arguments; return origFetch.apply(this, args).then(function (resp) { try { const url = (typeof args[0] === 'string') ? args[0] : (args[0] && args[0].url); const method = (args[1] && args[1].method) || (args[0] && args[0].method) || 'GET'; resp.clone().text().then(function (txt) { const json = readJsonLoose(txt, null); if (json) captureResponse(url, method, json); }).catch(function () {}); } catch (e) {} return resp; }); }; } function readJsonLoose(text, fallbackObj) { if (typeof text === 'string' && text) { const t = text.trim(); if (t.length > CAPTURE_MAX_BYTES) return null; if (t.charAt(0) === '{' || t.charAt(0) === '[') { try { return JSON.parse(t); } catch (e) {} } } if (fallbackObj && typeof fallbackObj === 'object') return fallbackObj; return null; } // 捕获响应并就地解析出其中的“帧”数据 function captureResponse(url, method, json) { let frames = []; try { frames = collectFrames(json); } catch (e) { frames = []; } captured.push({ url: url || '', method: method || '', json: json, frames: frames, at: Date.now() }); while (captured.length > CAPTURE_MAX) captured.shift(); return frames; } // 深度遍历任意 JSON,抓出所有“含 marks 的帧对象” function collectFrames(root) { const out = []; const seen = new Set(); // 防止循环引用 const consumed = new Set(); // 已作为帧容器使用过的节点,不再向下挖,避免重复计数 const visited = []; (function walk(node, depth) { if (depth > SCAN_DEPTH_MAX || !node || typeof node !== 'object') return; if (seen.has(node) || consumed.has(node)) return; seen.add(node); if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) walk(node[i], depth + 1); return; } let marks = null, status, imagePath = null, container = null; if (Array.isArray(node.marks) && node.marks.length) { marks = node.marks; status = (node.mark_status !== undefined) ? node.mark_status : undefined; container = node; } else if (node.markData && Array.isArray(node.markData.marks) && node.markData.marks.length) { marks = node.markData.marks; status = (node.mark_status !== undefined) ? node.mark_status : node.markData.mark_status; imagePath = node.markData.imagePath || node.markData.imgUrl || null; container = node.markData; } else if (Array.isArray(node.mark_data) && node.mark_data.length) { marks = node.mark_data; status = node.mark_status; container = node; } if (marks && marks.some(isMarkLike)) { if (!imagePath) imagePath = node.imagePath || node.imgUrl || node.img || node.url || null; visited.push({ marks: marks, status: status, imagePath: imagePath }); consumed.add(container); } for (const k in node) { if (!Object.prototype.hasOwnProperty.call(node, k)) continue; if (k === 'marks' || k === 'mark_data') continue; const v = node[k]; if (v && typeof v === 'object') walk(v, depth + 1); } })(root, 0); // 同一响应内去重:先按图片路径,再用 markId 集合兜底 const bySig = {}, byMarks = {}; for (let i = 0; i < visited.length; i++) { const f = visited[i]; const sig = frameSignature(f); const mk = 'k:' + (f.marks || []).map(function (m) { return m.markId; }).filter(Boolean).join(','); if (bySig[sig] || (mk !== 'k:' && byMarks[mk])) continue; bySig[sig] = true; if (mk !== 'k:') byMarks[mk] = true; out.push(f); } return out; } function isMarkLike(o) { if (!o || typeof o !== 'object' || Array.isArray(o)) return false; const hasClass = (o.class && (o.class.pname || o.class.name)) || o.pselect; if (!hasClass) return false; return !!extractBox(o); } // 帧唯一键:优先图片路径(同一帧在任何接口里都一致),否则用 markId 组合 function frameSignature(f) { if (f.imagePath) return 'p:' + f.imagePath; const ids = (f.marks || []).map(function (m) { return m.markId; }).filter(Boolean).join(','); if (ids) return 'm:' + ids; return 'j:' + JSON.stringify((f.marks || []).slice(0, 6)).slice(0, 400); } // ============================================================ // 帧号 / 总帧数读取 // ============================================================ function readFrameIndicator() { if (!document.body) return null; const re = /^\s*(\d+)\s*[\/|·]\s*(\d+)\s*$/; let best = null; const consider = function (raw) { if (raw === null || raw === undefined) return; const t = String(raw).trim(); if (!t || t.length > 16) return; const m = t.match(re); if (!m) return; const cur = parseInt(m[1], 10), tot = parseInt(m[2], 10); if (cur >= 1 && tot >= cur && tot <= 200000) { if (!best || tot > best.tot) best = { cur: cur, tot: tot }; } }; const all = document.body.querySelectorAll('*'); for (let i = 0; i < all.length; i++) { const el = all[i]; if (panel && panel.contains(el)) continue; if (el.children && el.children.length > 2) continue; consider(el.textContent); } const inputs = document.body.querySelectorAll('input, textarea'); for (let i = 0; i < inputs.length; i++) { if (panel && panel.contains(inputs[i])) continue; consider(inputs[i].value); } return best; } function getTotalFrames() { const ind = readFrameIndicator(); if (ind) return ind.tot; const n = toNum(settings.totalOverride); return (n !== null && n > 0) ? n : null; } function resolveFrameNo(url, body) { const fromReq = pickFrameFromRequest(url, body); if (fromReq !== null) return fromReq; const ind = readFrameIndicator(); return ind ? ind.cur : null; } function pickFrameFromRequest(url, body) { const names = ['frame_id', 'frameId', 'frame_index', 'frameIndex', 'frame_no', 'frameNo', 'current_frame', 'currentFrame', 'frame', 'index', 'idx']; for (let i = 0; i < names.length; i++) { const n = toNum(pickParamValue(url, body, [names[i]])); if (n !== null) return n; } return null; } function extractFrameNo(data) { if (!data || typeof data !== 'object') return null; const keys = ['frame_id', 'frameId', 'frame_index', 'frameIndex', 'frame', 'frame_no', 'frameNo', 'current_frame', 'currentFrame', 'index', 'idx', 'seq']; for (let i = 0; i < keys.length; i++) { const n = toNum(data[keys[i]]); if (n !== null) return n; } return null; } function extractTotalFrames(data) { if (!data || typeof data !== 'object') return null; const keys = ['total_frame', 'totalFrame', 'total_frames', 'totalFrames', 'frame_count', 'frameCount', 'frames_total']; for (let i = 0; i < keys.length; i++) { const n = toNum(data[keys[i]]); if (n !== null && n > 0 && n <= 200000) return n; } return null; } // ============================================================ // 参数工具 // ============================================================ function toNum(v) { if (typeof v === 'number') return isNaN(v) ? null : v; if (typeof v === 'string' && v.trim() !== '') { const n = parseFloat(v); return isNaN(n) ? null : n; } return null; } function parseBody(body) { if (body === null || body === undefined || body === '') return null; if (typeof body === 'object') return body; if (typeof body !== 'string') return null; try { return JSON.parse(body); } catch (e) {} try { const sp = new URLSearchParams(body); const o = {}; sp.forEach(function (v, k) { o[k] = v; }); return o; } catch (e) {} return null; } function pickParamValue(url, body, names) { for (let i = 0; i < names.length; i++) { const name = names[i]; try { const u = new URL(url, location.href); if (u.searchParams.has(name)) return u.searchParams.get(name); } catch (e) {} const obj = parseBody(body); if (obj && obj[name] !== undefined && obj[name] !== null) return obj[name]; } return null; } function getTaskSignature() { let taskId = null; if (lastRequest) taskId = pickParamValue(lastRequest.url, lastRequest.body, ['task_id', 'taskId']); if (taskId === null) { try { const u = new URL(location.href); taskId = u.searchParams.get('task_id') || u.searchParams.get('taskId'); } catch (e) {} } if (taskId !== null && taskId !== undefined && taskId !== '') return 'task:' + taskId; if (cachedData && cachedData.markData && cachedData.markData.imagePath) { return 'img:' + cachedData.markData.imagePath.replace(/\/[^/]*$/, ''); } return 'path:' + location.pathname; } // ============================================================ // 标记分类 / 坐标提取 // ============================================================ function getClassNames(mark) { const cls = mark.class || {}; const names = []; if (cls.pname) names.push(String(cls.pname)); if (cls.name) names.push(String(cls.name)); if (mark.pselect) names.push(String(mark.pselect)); return names; } function classifyMark(mark) { const names = getClassNames(mark); const low = names.map(function (n) { return n.toLowerCase(); }); const eqAny = function (target) { return target && low.indexOf(String(target).toLowerCase()) !== -1; }; if (eqAny(settings.clsFull)) return 'full'; if (eqAny(settings.clsHalf)) return 'half'; if (eqAny(settings.clsHand)) return 'hand'; const hit = function (arr) { return names.some(function (n) { return arr.indexOf(n) !== -1; }); }; if (hit(LABEL_FULL)) return 'full'; if (hit(LABEL_HALF)) return 'half'; if (hit(LABEL_HAND)) return 'hand'; return null; } function extractBox(mark) { if (!mark) return null; const sources = [ mark.point, mark.points, mark, mark.bbox, mark.box, mark.rect, mark.position, mark.pos, mark.coords, mark.coord, mark.data ].filter(function (s) { return s && typeof s === 'object'; }); for (let i = 0; i < sources.length; i++) { const s = sources[i]; if (Array.isArray(s)) { if (s.length >= 2 && s.every(function (p) { return p && typeof p === 'object' && toNum(p.x) !== null; })) { const xs = [], ys = []; s.forEach(function (p) { xs.push(toNum(p.x)); ys.push(toNum(p.y)); }); return { x1: Math.min.apply(null, xs), y1: Math.min.apply(null, ys), x2: Math.max.apply(null, xs), y2: Math.max.apply(null, ys) }; } if (s.length >= 4) { const a = s.map(toNum); if (a[0] !== null && a[1] !== null && a[2] !== null && a[3] !== null) { if (a[2] > a[0] && a[3] > a[1]) return { x1: a[0], y1: a[1], x2: a[2], y2: a[3] }; return { x1: a[0], y1: a[1], x2: a[0] + a[2], y2: a[1] + a[3] }; } } continue; } const l = toNum(s.left !== undefined ? s.left : s.x1); const t = toNum(s.top !== undefined ? s.top : s.y1); const r = toNum(s.right !== undefined ? s.right : s.x2); const b = toNum(s.bottom !== undefined ? s.bottom : s.y2); if (l !== null && t !== null && r !== null && b !== null) return { x1: l, y1: t, x2: r, y2: b }; const x = toNum(s.x !== undefined ? s.x : s.left); const y = toNum(s.y !== undefined ? s.y : s.top); const w = toNum(s.w !== undefined ? s.w : s.width); const h = toNum(s.h !== undefined ? s.h : s.height); if (x !== null && y !== null && w !== null && h !== null) return { x1: x, y1: y, x2: x + w, y2: y + h }; const x1 = toNum(s.xmin), y1 = toNum(s.ymin), x2 = toNum(s.xmax), y2 = toNum(s.ymax); if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) return { x1: x1, y1: y1, x2: x2, y2: y2 }; const pts = s.points || s.pts || s.xy || s.polygon; if (Array.isArray(pts) && pts.length) { const xs = [], ys = []; pts.forEach(function (p) { const px = Array.isArray(p) ? toNum(p[0]) : toNum(p.x); const py = Array.isArray(p) ? toNum(p[1]) : toNum(p.y); if (px !== null && py !== null) { xs.push(px); ys.push(py); } }); if (xs.length) return { x1: Math.min.apply(null, xs), y1: Math.min.apply(null, ys), x2: Math.max.apply(null, xs), y2: Math.max.apply(null, ys) }; } } return null; } function isNormalized(box) { const maxV = Math.max(Math.abs(box.x1), Math.abs(box.y1), Math.abs(box.x2), Math.abs(box.y2)); return maxV <= 1.5; } function getTolerance() { return (typeof settings.tolPx === 'number' && !isNaN(settings.tolPx)) ? settings.tolPx : 0; } function intersectArea(a, b) { const x1 = Math.max(a.x1, b.x1), y1 = Math.max(a.y1, b.y1); const x2 = Math.min(a.x2, b.x2), y2 = Math.min(a.y2, b.y2); if (x2 <= x1 || y2 <= y1) return 0; return (x2 - x1) * (y2 - y1); } function isInside(outer, inner, tol) { return inner.x1 >= outer.x1 - tol && inner.y1 >= outer.y1 - tol && inner.x2 <= outer.x2 + tol && inner.y2 <= outer.y2 + tol; } function getOverflow(outer, inner) { let tol = getTolerance(); if (isNormalized(outer)) tol = tol / Math.max(currentImage.w || 1920, 1); const eps = 1e-6; const norm = isNormalized(outer); const unit = norm ? '' : 'px'; const fmt = function (v) { return norm ? v.toFixed(4) : v.toFixed(1); }; const over = []; if (inner.x1 < outer.x1 - tol - eps) over.push('左侧 ' + fmt(outer.x1 - inner.x1) + unit); if (inner.y1 < outer.y1 - tol - eps) over.push('上侧 ' + fmt(outer.y1 - inner.y1) + unit); if (inner.x2 > outer.x2 + tol + eps) over.push('右侧 ' + fmt(inner.x2 - outer.x2) + unit); if (inner.y2 > outer.y2 + tol + eps) over.push('下侧 ' + fmt(inner.y2 - outer.y2) + unit); return over; } // 归组标识:本平台用 groupId(配合 groupL),objectId 是“对象/轨迹”,不是归组键 function getGroupId(mark) { const v = mark.groupId !== undefined ? mark.groupId : (mark.group_id !== undefined ? mark.group_id : mark.group); if (v === undefined || v === null || v === '') return null; if (v === 0 || v === '0') return null; // 0 表示未归组 return String(v); } function pickParent(childBox, candidates) { const withBox = candidates.filter(function (c) { return c.box; }); if (!withBox.length) return null; let best = null, bestScore = -Infinity; for (let i = 0; i < withBox.length; i++) { const c = withBox[i]; const score = (isInside(c.box, childBox, getTolerance()) ? 1e12 : 0) + intersectArea(childBox, c.box); if (score > bestScore) { bestScore = score; best = c; } } if (best && bestScore <= 0) return null; return best; } // ============================================================ // 全题包统计 // ============================================================ function countClass(marks, kind) { let n = 0; for (let i = 0; i < (marks || []).length; i++) { if (classifyMark(marks[i]) === kind) n++; } return n; } // 帧唯一键:优先图片路径,其次帧号 function getFrameKey(data, frameNo) { const md = (data && data.markData) || {}; if (md.imagePath) return 'p:' + md.imagePath; if (md.imgUrl) return 'u:' + String(md.imgUrl).split('?')[0]; if (frameNo !== null && frameNo !== undefined) return 'n:' + frameNo; return null; } function recordFrame(data, frameNo, src) { const md = (data && data.markData) || {}; const marks = md.marks || []; if (!marks.length && data && data.mark_status === undefined) return; const key = getFrameKey(data, frameNo); if (!key) return; const sig = getTaskSignature(); if (pkgStats.taskSig !== sig) { pkgStats = { taskSig: sig, totalFrames: pkgStats.totalFrames || null, frames: {} }; } const tot = getTotalFrames() || extractTotalFrames(data); if (tot) pkgStats.totalFrames = tot; pkgStats.frames[key] = { half: countClass(marks, 'half'), marks: marks.length, status: data.mark_status, no: (frameNo !== null && frameNo !== undefined) ? frameNo : null, src: src || 'walk' }; saveStats(); if (panelReady) updatePackageView(); } // 记录当前帧数据对应的帧标识(图片路径 + 帧号) function setCachedData(data, frameNo) { cachedData = data; const md = (data && data.markData) || {}; const path = md.imagePath || md.imgUrl || null; cachedFrameKey = path ? String(path).split('?')[0] : null; cachedFrameNo = (frameNo !== null && frameNo !== undefined) ? frameNo : extractFrameNo(data); // 若帧号与页面指示器一致,同步监听基线,避免重复拉取 if (cachedFrameNo !== null && cachedFrameNo !== undefined) lastWatchedFrame = cachedFrameNo; } // 帧切换监听:靠帧号变化驱动自动校验。 // 平台对已看过的帧会走缓存、不发请求,只有轮询帧号才能保证切帧必校验。 function startFrameWatcher() { setInterval(frameWatcherTick, 700); } // 一次帧切换检查:返回 'ok'(已校验当前缓存帧)/ 'load'(需拉取)/ 'idle' function frameWatcherTick() { if (scanning || autoBatchRunning) return 'idle'; const ind = readFrameIndicator(); if (!ind) return 'idle'; if (ind.cur === lastWatchedFrame) return 'idle'; lastWatchedFrame = ind.cur; // 帧号变了:若当前缓存的数据就是这一帧,直接校验 if (cachedFrameNo === ind.cur) { if (panelReady) autoRunCheck(); return 'ok'; } autoLoadFrame(ind.cur); return 'load'; } let autoLoadBusy = false; async function autoLoadFrame(frameNo) { if (autoLoadBusy) return; if (!lastRequest || !cachedData) return; // 等页面自身首次加载完成再自动拉取 autoLoadBusy = true; try { // 优先从 DOM 帧导航条拿这一帧的 task_id const domList = readFrameListFromDom(); let taskId = null; if (domList && domList.list.length) { const hit = domList.list.filter(function (e) { return e.order === frameNo - 1 || e.order === frameNo; }); if (hit.length) taskId = hit[0].id; else if (domList.list[frameNo - 1]) taskId = domList.list[frameNo - 1].id; } if (taskId === null) return; // 拿不到 task_id 就不乱请求 const data = await requestMarkData(taskId); setCachedData(data, frameNo); recordFrame(data, frameNo, 'walk'); if (panelReady) autoRunCheck(); } catch (e) { // 自动拉取失败保持静默,不打扰标注 } finally { autoLoadBusy = false; } } function frameNoFromPath(p) { if (!p) return null; const m = String(p).match(/(\d+)(?=\.\w+$)/); return m ? parseInt(m[1], 10) : null; } // ============================================================ // 题包任务列表 → 逐帧请求 get-mark-data // ============================================================ function walkJson(node, visit, depth, key) { depth = depth || 0; if (depth > SCAN_DEPTH_MAX || !node || typeof node !== 'object') return; visit(node, key); if (Array.isArray(node)) { for (let i = 0; i < node.length; i++) walkJson(node[i], visit, depth + 1, null); return; } for (const k in node) { if (!Object.prototype.hasOwnProperty.call(node, k)) continue; const v = node[k]; if (v && typeof v === 'object') walkJson(v, visit, depth + 1, k); } } const TASK_ID_KEYS = ['task_id', 'taskId', 'taskID', 'tid']; const TASK_UUID_KEYS = ['task_uuid', 'taskUuid', 'uuid']; const TASK_ORDER_KEYS = ['order', 'sort', 'index', 'idx', 'frame', 'frame_no', 'frameNo', 'seq', 'no']; function firstKey(obj, keys) { for (let i = 0; i < keys.length; i++) { if (obj[keys[i]] !== undefined && obj[keys[i]] !== null && obj[keys[i]] !== '') return obj[keys[i]]; } return null; } // 判断一个对象是否像“帧/任务”条目(本平台:data.frame[] 只带 task_id 和状态位) function taskEntryOf(obj) { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return null; const id = firstKey(obj, TASK_ID_KEYS); if (id === null) return null; if (Array.isArray(id) || typeof id === 'object') return null; return { id: id, uuid: firstKey(obj, TASK_UUID_KEYS), image: null, // 帧列表不含图片,图片从 get-mark-data 里取 order: toNum(firstKey(obj, TASK_ORDER_KEYS)) }; } // 帧条目特征字段,用于给候选数组加分 const FRAME_FLAG_KEYS = ['done_status', 'mark_flag', 'is_save', 'is_acceptance', 'has_issues', 'mark_status']; // 从所有已捕获响应里找出最像“题包帧列表”的数组 function findTaskList() { const pageTotal = getTotalFrames(); let best = null; for (let i = 0; i < captured.length; i++) { const url = String(captured[i].url || ''); walkJson(captured[i].json, function (node, key) { if (!Array.isArray(node) || node.length < 2 || node.length > 20000) return; const entries = []; let flagHits = 0; for (let j = 0; j < node.length; j++) { const e = taskEntryOf(node[j]); if (e) entries.push(e); if (node[j] && typeof node[j] === 'object') { for (let f = 0; f < FRAME_FLAG_KEYS.length; f++) { if (node[j][FRAME_FLAG_KEYS[f]] !== undefined) { flagHits++; break; } } } } if (entries.length < 2) return; if (entries.length < node.length * 0.5) return; let score = entries.length + flagHits * 10; if (key === 'frame') score += 20000; // mark-conf 的 data.frame 最可信 if (pageTotal && entries.length === pageTotal) score += 100000; // 与页面总帧数一致,几乎可确定是全量 if (/packages?\/[^/]*\/tasks|mark-conf|package|batch|frame/i.test(url)) score += 5000; else if (/task/i.test(url)) score += 1000; if (!best || score > best.score) best = { score: score, entries: entries, url: url }; }); } if (!best) return null; const seen = {}; const list = []; best.entries.forEach(function (e) { const k = String(e.id); if (seen[k]) return; seen[k] = true; list.push(e); }); const hasOrder = list.some(function (e) { return e.order !== null; }); if (hasOrder) { list.sort(function (a, b) { const ao = a.order === null ? 1e9 : a.order, bo = b.order === null ? 1e9 : b.order; return ao - bo; }); } return { list: list, url: best.url }; } function buildMarkBody(taskId) { let base = parseBody(lastRequest ? lastRequest.body : null) || {}; if (typeof base !== 'object' || Array.isArray(base)) base = {}; const body = {}; for (const k in base) { if (Object.prototype.hasOwnProperty.call(base, k)) body[k] = base[k]; } body.task_id = taskId; body.time = Date.now(); return body; } // 从页面 URL 读题包参数(id/task_id、package_id、status、work_type、access) function readPageParams() { const q = {}; try { const u = new URL(location.href); u.searchParams.forEach(function (v, k) { q[k] = v; }); } catch (e) {} return q; } // 复用平台请求头(含 Access-Key 等鉴权头) function authHeaders(extra) { const h = Object.assign({}, (lastRequest && lastRequest.headers) || {}); // 采集不到时给常见默认值,保证基本可用 if (!h['X-Requested-With']) h['X-Requested-With'] = 'XMLHttpRequest'; h['Accept'] = h['Accept'] || 'application/json, text/javascript, */*; q=0.01'; return Object.assign(h, extra || {}); } function getTaskKey() { let tk = lastRequest ? pickParamValue(lastRequest.url, lastRequest.body, ['task_key', 'taskKey']) : null; if (tk === null) tk = readPageParams().task_key || null; return tk; } async function getJson(url, method) { try { const resp = await fetch(url, { method: method || 'GET', credentials: 'include', headers: authHeaders() }); if (!resp.ok) return { error: 'HTTP ' + resp.status }; const json = await resp.json(); captured.push({ url: url, method: method || 'GET', json: json, frames: [], at: Date.now() }); return { json: json }; } catch (e) { return { error: String(e && e.message || e) }; } } // 从 DOM 的帧导航条读取帧列表:平台渲染