// ==UserScript== // @name BLY行人 // @namespace https://label.bilinyun.net // @version 1.2 // @description 比邻云行人(2DPedestrian)质检助手:① 进题包全量预加载全部帧,消除首次切帧的灰屏转圈等待 ② 切帧去黑屏遮罩 ③ 帧级标注缓存 + 缺帧自动补请求 ④ 面板实时显示本帧框数与整包统计 ⑤ 整包框尺寸检查(宽≥20 高≥40,忽略框跳过,错误帧点击跳转)⑥ 一键复制「包号 + 有效帧数 + 框数」(有效帧 = 有框的帧;包号与帧数之间空一列) // @author CC // @match https://label.bilinyun.net/* // @grant none // @run-at document-start // @license CC // ==/UserScript== (function () { 'use strict'; /* ==================== 常量 ==================== */ const API_MARK = '/api/mark/image'; const API_STATIC = '/api/mark/contents/static'; const WARMUP_KEY = '_blyped_warmup'; // ---- 框尺寸规则(来自 BB 平台「行人验收框大小」脚本,单位:图像像素)---- // 不分主/非主行人,统一按此标准;标了「忽略」的框跳过检查 const MIN_W = 20; const MIN_H = 40; let WARMUP_ON = true; try { WARMUP_ON = localStorage.getItem(WARMUP_KEY) !== '0'; } catch (e) { } /* ==================== 全局状态 ==================== */ let CMAP = null; // 类目映射(从 projectClass 动态构建) let IMG_SIZE = null; // 图像宽高缓存 let AUTH_HDRS = null; // 页面真实请求里的鉴权头(用于兜底补请求) let FRAME_IDS = []; // 帧序号(1-based) → frameId const FRAME_CACHE = {}; // frameId → shapeList let PROJECT_INFO = {}; // taskNo / projectName 等 let lastFrameNo = null; let lastRenderKey = null; let PACK_STAT = null; // 整包统计结果 let WARMUP_RUNNING = false; let WARMUP_DONE = false; let warmupTried = false; let staticTried = false; /* ==================== 工具 ==================== */ function num(v) { const n = parseFloat(v); return isNaN(n) ? null : n; } function parseJson(s) { try { return JSON.parse(s) || {}; } catch (e) { return {}; } } function esc(s) { return String(s === null || s === undefined ? '' : s) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function fmt1(n) { if (n === null || n === undefined || isNaN(n)) return '?'; return (Math.round(n * 10) / 10).toFixed(1); } function taskIdFromUrl() { const m = /[?&]recordId=([^&]+)/.exec(location.search || ''); return m ? m[1] : null; } function frameIdFromUrl(u) { const m = /frame_id=([^&]+)/.exec(u || ''); return m ? m[1] : null; } function currentFrameNo() { const el = document.querySelector('.frame-counter-clickable'); if (!el) return null; const m = /(\d+)\s*\/\s*(\d+)/.exec(el.innerText || ''); return m ? parseInt(m[1], 10) : null; } /* ==================== 类目映射(自适应 projectClass) ==================== */ // 行人项目实测两个类目: // full_bbox → 行人主框(含 是否忽略 / 遮挡 / 截断) // Other pedestrian boxes → 其他行人框(含 忽略 / 遮挡 / 截断) function buildClassMap(pcStr) { let pc; if (!pcStr) return null; try { pc = (typeof pcStr === 'string') ? JSON.parse(pcStr) : pcStr; } catch (e) { return null; } if (!pc || !pc.id || !pc.id.length) return null; const ids = pc.id || []; const exps = pc.exportName || []; const names = pc.name || []; const attrs = pc.attributes || []; const m = { mainId: null, // 行人主框类目 id otherId: null, // 其他行人框类目 id idName: {}, // 类目 id → 名称 ignoreAttr: {}, // 类目 id → { attrId, values:Set(忽略值id) } occAttr: {}, // 类目 id → { attrId, values:{} } truncAttr: {}, // 类目 id → { attrId, values:{} } allIds: ids.slice() }; ids.forEach(function (id, i) { const ex = String(exps[i] || ''); const nm = String(names[i] || ''); m.idName[id] = nm || ex || id; if (/other/i.test(ex) || nm.indexOf('其他') !== -1) m.otherId = id; else if (!m.mainId) m.mainId = id; const collect = function (a, target) { const vals = {}; (a.attributes || []).forEach(function (v) { vals[v.id] = String(v.name || v.exportName || ''); }); target[id] = { attrId: a.id, values: vals }; }; (attrs[i] || []).forEach(function (a) { const ax = String(a.exportName || ''); const an = String(a.name || ''); // 是否忽略:主框是「Ignore / 0=是 1=否」,其他框是「ignore / 0=有效 1=忽略」 if (/ignore/i.test(ax) || an.indexOf('忽略') !== -1) { const set = {}; (a.attributes || []).forEach(function (v) { const vn = String(v.name || '').trim(); const ve = String(v.exportName || '').trim(); if (vn === '忽略' || ve === '忽略' || vn === '是' || ve === '是') set[v.id] = true; }); m.ignoreAttr[id] = { attrId: a.id, values: set }; } if (/occlusion/i.test(ax) || an.indexOf('遮挡') !== -1) collect(a, m.occAttr); if (/truncation/i.test(ax) || an.indexOf('截断') !== -1) collect(a, m.truncAttr); }); }); if (!m.mainId) m.mainId = m.otherId; return m; } function getMap() { return CMAP; } function isIgnored(box) { const M = getMap(); if (!M || !M.ignoreAttr) return false; const conf = M.ignoreAttr[box.typeId]; if (!conf) return false; const v = box.attrs ? box.attrs[conf.attrId] : null; return !!(v && conf.values[v]); } /* ==================== 标注归一化 ==================== */ // 比邻云 shapeList 元素:{ labelId, imageId, shapeJson, attrJson }(后两者是字符串化 JSON) function boxOf(s) { if (!s || s.labelId === undefined) return null; const sh = parseJson(s.shapeJson); const at = parseJson(s.attrJson); const g = sh.shape || {}; const lx = num(g.lx), ly = num(g.ly), rx = num(g.rx), ry = num(g.ry); return { labelId: s.labelId, typeId: at.type_id, attrs: at.attributes || {}, isRect: String(sh.shapeType) === '0', lx: lx, ly: ly, rx: rx, ry: ry, w: (lx !== null && rx !== null) ? (rx - lx) : null, h: (ly !== null && ry !== null) ? (ry - ly) : null, clsName: (getMap() && getMap().idName[s.attrJson ? at.type_id : '']) || '' }; } /* ==================== 图像真实尺寸 ==================== */ // 页面 里有 logo,不能直接用;优先从 Konva 舞台的 Image 节点取 function getImageSize() { let w = 0, h = 0; try { if (typeof Konva !== 'undefined' && Konva.stages && Konva.stages.length) { const nodes = Konva.stages[0].find('Image'); for (let i = 0; i < nodes.length; i++) { const im = nodes[i].image && nodes[i].image(); if (im && im.naturalWidth > 200 && im.naturalHeight > 200) { w = im.naturalWidth; h = im.naturalHeight; break; } } } } catch (e) { } if (!w) { const imgs = document.querySelectorAll('img'); for (let i = 0; i < imgs.length; i++) { const iw = imgs[i].naturalWidth, ih = imgs[i].naturalHeight; if (iw > 1000 && ih > 1000) { w = iw; h = ih; break; } } } if (!w) { IMG_SIZE = null; return null; } if (!IMG_SIZE || IMG_SIZE.w !== w || IMG_SIZE.h !== h) IMG_SIZE = { w: w, h: h }; return IMG_SIZE; } /* ==================== 本帧分析 ==================== */ let lastBoxes = null; function analyzeShapes(shapeList, size) { const boxes = []; (shapeList || []).forEach(function (s) { const b = boxOf(s); if (b && b.isRect) boxes.push(b); }); const ignored = boxes.filter(function (b) { return isIgnored(b); }); lastBoxes = { boxes: boxes, valid: boxes.length - ignored.length, ignored: ignored.length }; const rk = boxes.map(function (b) { return b.labelId + '|' + b.lx + ',' + b.ly + ',' + b.rx + ',' + b.ry + '|' + JSON.stringify(b.attrs); }).join(';') + '|' + currentFrameNo() + '|' + (size ? size.w + 'x' + size.h : '-'); if (rk === lastRenderKey) return; // 数据未变不重渲染,避免面板闪烁 lastRenderKey = rk; renderPanel(); } function showWaiting(no) { if (WARMUP_RUNNING) return; lastBoxes = null; lastRenderKey = null; renderPanel(no); } /* ==================== 面板 UI ==================== */ let panelContainer = null, panelBody = null, panelContent = null; let isMinimized = false, isDragging = false, dragOffsetX = 0, dragOffsetY = 0; let panelX = null, panelY = 60; function createPanel() { if (panelContainer && document.body && document.body.contains(panelContainer)) return; const exist = document.getElementById('_blyped_panel'); if (exist) { panelContainer = exist; panelBody = exist.querySelector('#_blyped_body'); panelContent = exist.querySelector('#_blyped_content'); return; } panelContainer = document.createElement('div'); panelContainer.id = '_blyped_panel'; if (panelX === null) panelX = Math.max(20, window.innerWidth - 372); panelContainer.style.cssText = 'position:fixed;z-index:999999;width:352px;background:#ffffff;' + 'border:1px solid #d1d5db;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,0.15);' + 'font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:13px;color:#1f2937;' + 'overflow:hidden;left:' + panelX + 'px;top:' + panelY + 'px;user-select:none;'; const titleBar = document.createElement('div'); titleBar.style.cssText = 'display:flex;align-items:center;justify-content:space-between;padding:8px 12px;' + 'background:#f3f4f6;cursor:move;border-bottom:1px solid #d1d5db;'; titleBar.innerHTML = '🚶 比邻云行人助手'; titleBar.addEventListener('mousedown', startDrag); const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:6px;'; btns.appendChild(createBtn('—', '#e5e7eb', '#d1d5db', function (e) { e.stopPropagation(); toggleMinimize(); })); btns.appendChild(createBtn('✕', '#fca5a5', '#f87171', function (e) { e.stopPropagation(); closePanel(); })); titleBar.appendChild(btns); panelContainer.appendChild(titleBar); panelBody = document.createElement('div'); panelBody.id = '_blyped_body'; panelBody.style.cssText = 'max-height:520px;overflow-y:auto;'; panelContent = document.createElement('div'); panelContent.id = '_blyped_content'; panelContent.style.cssText = 'padding:11px;background:#ffffff;'; panelBody.appendChild(panelContent); panelContainer.appendChild(panelBody); document.body.appendChild(panelContainer); } function createBtn(text, color, hover, onClick) { const b = document.createElement('span'); b.textContent = text; b.style.cssText = 'width:26px;height:26px;display:flex;align-items:center;justify-content:center;' + 'border-radius:6px;cursor:pointer;font-size:14px;font-weight:bold;color:#374151;background:' + color + ';'; b.addEventListener('mouseenter', function () { b.style.background = hover; }); b.addEventListener('mouseleave', function () { b.style.background = color; }); b.addEventListener('click', onClick); return b; } function toggleMinimize() { isMinimized = !isMinimized; if (panelBody) panelBody.style.maxHeight = isMinimized ? '0' : '520px'; } function closePanel() { if (panelContainer) { panelContainer.remove(); panelContainer = null; } } function startDrag(e) { if (e.button !== 0 || !panelContainer) return; isDragging = true; const r = panelContainer.getBoundingClientRect(); dragOffsetX = e.clientX - r.left; dragOffsetY = e.clientY - r.top; document.addEventListener('mousemove', onDrag); document.addEventListener('mouseup', stopDrag); e.preventDefault(); } function onDrag(e) { if (!isDragging || !panelContainer) return; panelX = Math.max(0, e.clientX - dragOffsetX); panelY = Math.max(0, e.clientY - dragOffsetY); panelContainer.style.left = panelX + 'px'; panelContainer.style.top = panelY + 'px'; } function stopDrag() { isDragging = false; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); } function row(label, value) { return '
' + '' + label + '' + '' + value + '
'; } function renderPanel(waitingNo) { createPanel(); if (!panelContent) return; const no = currentFrameNo(); const total = FRAME_IDS.length || (function () { const el = document.querySelector('.frame-counter-clickable'); const m = el ? /(\d+)\s*\/\s*(\d+)/.exec(el.innerText || '') : null; return m ? parseInt(m[2], 10) : 0; })(); let h = ''; // 顶部信息 h += '
'; if (PROJECT_INFO.projectName) h += row('项目', esc(PROJECT_INFO.projectName)); if (PROJECT_INFO.taskNo) h += row('包号', esc(PROJECT_INFO.taskNo)); h += row('当前帧', (no || '-') + ' / ' + (total || '-')); h += '
'; // 本帧状态 if (waitingNo) { h += '
' + '' + '
' + '
第 ' + waitingNo + ' 帧加载中…
' + '正在获取该帧标注数据
'; } else if (lastBoxes) { const ok = lastBoxes.ignored === 0; h += '
' + '' + (ok ? '✅' : 'ℹ️') + '' + '
' + '
本帧 ' + lastBoxes.boxes.length + ' 个框' + (lastBoxes.ignored ? '(忽略 ' + lastBoxes.ignored + ')' : '') + '
' + '参与统计 ' + lastBoxes.valid + ' 个
'; } else { h += '
本帧暂无标注数据
'; } // 整包统计 h += '
'; h += '
'; const st = PACK_STAT; h += cell('总帧', st ? st.frameTotal : (total || '-')); h += cell('有框帧数', st ? st.frameWithBox : '-'); h += cell('总框数', st ? st.boxTotal : '-'); h += '
'; // 尺寸异常列表(点击跳帧) if (st && st.sizeErrors && st.sizeErrors.length) { h += '
'; h += '
⚠ 尺寸不达标(点击跳帧):
'; st.sizeErrors.forEach(function (ef) { h += '
'; h += '
' + '第 ' + ef.frameNo + ' 帧' + ef.items.length + ' 个 →
'; ef.items.forEach(function (it) { h += '
• [' + esc(it.cls) + ' #' + it.labelId + '] ' + esc(it.msg) + '
'; }); h += '
'; }); h += '
'; } else if (st) { h += '
✓ 未发现尺寸异常(宽≥' + MIN_W + ' 高≥' + MIN_H + ',忽略框已跳过)
'; } // 功能区 h += '
'; h += ''; h += '
' + COPY_LABEL + '
'; h += '
🔍 整包检查(框尺寸)
'; h += '
'; h += '
'; panelContent.innerHTML = h; const cb = document.getElementById('_blyped_warm'); if (cb) cb.addEventListener('change', function () { WARMUP_ON = cb.checked; try { localStorage.setItem(WARMUP_KEY, WARMUP_ON ? '1' : '0'); } catch (e) { } if (WARMUP_ON && !WARMUP_DONE) warmupAllFrames(); }); const cpb = document.getElementById('_blyped_copy'); if (cpb) cpb.addEventListener('click', function () { copyPackInfo(); }); const sb = document.getElementById('_blyped_stat'); if (sb) sb.addEventListener('click', function () { runPackCheck(); }); panelContent.querySelectorAll('[data-blyped-frame]').forEach(function (el) { el.addEventListener('click', function () { goToFrame(el.getAttribute('data-blyped-frame')); }); }); // 只在帧号变化时回到顶部,避免统计结果被滚走 const key = 'f' + no; if (panelBody && key !== lastPanelFrame) { panelBody.scrollTop = 0; lastPanelFrame = key; } } let lastPanelFrame = null; function cell(label, value) { return '
' + '
' + label + '
' + '
' + esc(value) + '
'; } function setMsg(s) { const el = document.getElementById('_blyped_msg'); if (el) el.innerHTML = s; } /* ==================== 帧缓存与补请求 ==================== */ function requestFrame(no) { const fid = FRAME_IDS[no - 1]; if (!fid || !AUTH_HDRS) { showWaiting(no); return; } fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS }) .then(function (r) { return r.json(); }) .then(function (j) { if (j && (j.code === 200 || j.code === 0) && j.data && j.data.shapeList) { FRAME_CACHE[fid] = j.data.shapeList; analyzeShapes(j.data.shapeList, getImageSize()); } else { showWaiting(no); } }) .catch(function () { showWaiting(no); }); } let staticFetching = false; function fetchStatic() { const tid = taskIdFromUrl(); if (!tid || !AUTH_HDRS || staticFetching) return; staticFetching = true; fetch(API_STATIC + '?task_id=' + tid, { headers: AUTH_HDRS }) .then(function (r) { return r.json(); }) .then(function (j) { handleStaticResponse(j); }) .catch(function () { }) .then(function () { staticFetching = false; }); } function handleMarkResponse(url, json) { if (!json || (json.code !== 200 && json.code !== 0)) return; const d = json.data; if (!d || !d.shapeList) return; const fid = frameIdFromUrl(url); if (fid) FRAME_CACHE[fid] = d.shapeList; analyzeShapes(d.shapeList, getImageSize()); } function handleStaticResponse(json) { if (!json || (json.code !== 200 && json.code !== 0)) return; const d = json.data || {}; const list = d.image2dList || []; if (list.length) FRAME_IDS = list.map(function (f) { return f.frameId; }); const pi = d.projectInfo || {}; PROJECT_INFO = { taskNo: pi.taskNo ? String(pi.taskNo) : '', projectName: pi.projectName ? String(pi.projectName) : '' }; const m = buildClassMap(pi.projectClass); if (m) { CMAP = m; lastRenderKey = null; if (lastBoxes) renderPanel(); if (!warmupTried && WARMUP_ON && FRAME_IDS.length) { warmupTried = true; setTimeout(function () { warmupAllFrames(); }, 2500); } } } function routeResponse(url, json) { if (!url) return; if (url.indexOf(API_MARK) !== -1) handleMarkResponse(url, json); else if (url.indexOf(API_STATIC) !== -1) handleStaticResponse(json); } /* ==================== 整包检查(统计 + 框尺寸) ==================== */ function clsLabel(b) { const M = getMap(); return (M && M.idName && M.idName[b.typeId]) || '行人框'; } function sizeProblem(b) { if (b.w === null || b.h === null) return '无有效尺寸数据'; if (b.w < MIN_W || b.h < MIN_H) { return '宽 ' + fmt1(b.w) + 'px(需≥' + MIN_W + ') 高 ' + fmt1(b.h) + 'px(需≥' + MIN_H + ')'; } return null; } function frameStat(shapeList) { let total = 0, valid = 0; const errs = []; (shapeList || []).forEach(function (s) { const b = boxOf(s); if (!b || !b.isRect) return; total++; if (isIgnored(b)) return; // 忽略框计入总数,但不参与尺寸检查 valid++; const msg = sizeProblem(b); if (msg) errs.push({ labelId: b.labelId, cls: clsLabel(b), w: b.w, h: b.h, msg: msg }); }); return { total: total, valid: valid, errs: errs }; } async function getFrameList(fid) { let list = FRAME_CACHE[fid]; if (list) return list; if (!AUTH_HDRS) return []; try { const j = await (await fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS })).json(); list = (j && j.data && j.data.shapeList) || []; } catch (e) { list = []; } FRAME_CACHE[fid] = list; return list; } let CHECK_RUNNING = false; async function runPackCheck() { if (CHECK_RUNNING) return; if (!FRAME_IDS.length) { setMsg('⚠ 帧列表未就绪,请刷新页面'); return; } if (!AUTH_HDRS) { setMsg('⚠ 请先在工作台切一次帧'); return; } CHECK_RUNNING = true; setMsg('⏳ 整包检查中…'); let frameWithBox = 0, boxTotal = 0, boxValid = 0; const sizeErrors = []; for (let i = 0; i < FRAME_IDS.length; i++) { const list = await getFrameList(FRAME_IDS[i]); const s = frameStat(list); boxTotal += s.total; boxValid += s.valid; if (s.total > 0) frameWithBox++; if (s.errs.length) sizeErrors.push({ frameNo: i + 1, items: s.errs }); } PACK_STAT = { frameTotal: FRAME_IDS.length, frameWithBox: frameWithBox, boxTotal: boxTotal, boxValid: boxValid, boxIgnored: boxTotal - boxValid, sizeErrors: sizeErrors, badFrameCount: sizeErrors.length, badBoxCount: sizeErrors.reduce(function (a, f) { return a + f.items.length; }, 0) }; lastRenderKey = null; renderPanel(); setMsg(PACK_STAT.badBoxCount ? '⚠ ' + PACK_STAT.badBoxCount + ' 个框尺寸不达标(分布 ' + PACK_STAT.badFrameCount + ' 帧),见上方列表' : '✓ 框尺寸全部达标(宽≥' + MIN_W + ' 高≥' + MIN_H + ',忽略框已跳过)'); CHECK_RUNNING = false; } /* ==================== 复制「包号 + 帧数 + 框数」 ==================== */ function copyText(text) { if (navigator.clipboard && navigator.clipboard.writeText) return navigator.clipboard.writeText(text); return new Promise(function (resolve, reject) { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:0;'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); resolve(); } catch (e) { reject(e); } }); } const COPY_LABEL = '📋 复制「包号 + 有效帧数 + 框数」'; function setCopyBtn(t) { const b = document.getElementById('_blyped_copy'); if (b) b.innerText = t; } async function copyPackInfo() { if (!FRAME_IDS.length) { setMsg('⚠ 帧列表未就绪,请刷新页面'); return; } if (!AUTH_HDRS) { setMsg('⚠ 请先在工作台切一次帧'); return; } setCopyBtn('⏳ 统计中…'); if (!PACK_STAT) await runPackCheck(); const stat = PACK_STAT || { frameWithBox: 0, boxTotal: 0 }; const no = PROJECT_INFO.taskNo || '未知包号'; // 包号 → 空一列 → 有效帧数(有框的帧)→ 框数 try { await copyText(no + '\t\t' + stat.frameWithBox + '\t' + stat.boxTotal); setCopyBtn('✓ 已复制:' + no + ' · ' + stat.frameWithBox + ' 有效帧 · ' + stat.boxTotal + ' 框'); } catch (e) { setCopyBtn('⚠ 复制失败:' + (e && e.message ? e.message : e)); } setTimeout(function () { setCopyBtn(COPY_LABEL); }, 2500); } /* ==================== 体验优化:去黑屏 ==================== */ // 平台切帧会弹全屏遮罩(.block-mask) + 转圈(.pc-loading),实测约 400ms function installMaskCss() { if (document.getElementById('_blyped_mask_style')) return; const st = document.createElement('style'); st.id = '_blyped_mask_style'; st.textContent = '.block-mask{background-color:transparent !important;}' + '.pc-loading{display:none !important;}' + '._blyped_row:hover{background:#eef2ff !important;}' + '._blyped_row:active{background:#e0e7ff !important;}'; (document.head || document.documentElement).appendChild(st); } /* ==================== 进题包全量预热 ==================== */ // 平台切帧流程 loadAnnotatesDate() 第一步就 clearResource() 清空画布(灰屏来源), // 随后若 dataManager.getFrameObject(frameId) 命中则直接 return(秒开)。 // 所以这里进题包后主动把每一帧过一遍,把 dataManager 填满 —— 之后切帧就顺滑了。 function goToFrame(n) { const items = document.querySelectorAll('.frame-navigation .page-item'); for (let i = 0; i < items.length; i++) { if ((items[i].innerText || '').trim() === String(n)) { items[i].click(); return true; } } return false; } function frameDataReady(fid) { try { const dm = window.editor && window.editor.dataManager; if (!dm || typeof dm.getFrameObject !== 'function') return false; return !!dm.getFrameObject(fid); } catch (e) { return false; } } function waitFrameReady(fid, timeout) { return new Promise(function (resolve) { const t0 = Date.now(); const timer = setInterval(function () { if (frameDataReady(fid)) { clearInterval(timer); resolve(true); return; } if (Date.now() - t0 > (timeout || 4000)) { clearInterval(timer); resolve(false); } }, 80); }); } function warmupOverlay(show, cur, total) { let el = document.getElementById('_blyped_warm'); if (!show) { if (el) el.remove(); return; } if (!el) { el = document.createElement('div'); el.id = '_blyped_warm'; el.style.cssText = 'position:fixed;z-index:999997;left:50%;top:50%;transform:translate(-50%,-50%);' + 'background:rgba(17,24,39,0.92);color:#fff;padding:14px 22px;border-radius:9px;text-align:center;' + 'font-family:"Microsoft YaHei","PingFang SC",Arial,sans-serif;font-size:13px;box-shadow:0 8px 28px rgba(0,0,0,.35);'; document.body.appendChild(el); } el.innerHTML = '
📦 正在预加载全部帧…
' + '
' + (cur || 0) + ' / ' + (total || 0) + ' 完成后切帧不再等待
'; } async function warmupAllFrames() { if (WARMUP_RUNNING || WARMUP_DONE) return; if (!WARMUP_ON || !FRAME_IDS.length || !AUTH_HDRS) return; WARMUP_RUNNING = true; const startNo = currentFrameNo() || 1; const total = FRAME_IDS.length; warmupOverlay(true, 0, total); let done = 0; for (let n = 1; n <= total; n++) { const fid = FRAME_IDS[n - 1]; if (!frameDataReady(fid)) { goToFrame(n); await waitFrameReady(fid, 4000); } const cached = FRAME_CACHE[fid]; if (!cached && AUTH_HDRS) { try { const j = await (await fetch(API_MARK + '?frame_id=' + fid, { headers: AUTH_HDRS })).json(); if (j && j.data && j.data.shapeList) FRAME_CACHE[fid] = j.data.shapeList; } catch (e) { } } done++; warmupOverlay(true, done, total); } goToFrame(startNo); await waitFrameReady(FRAME_IDS[startNo - 1], 3000); warmupOverlay(false); WARMUP_DONE = true; WARMUP_RUNNING = false; lastRenderKey = null; renderPanel(); // 全部帧已在缓存里,顺手把整包检查(统计 + 尺寸)跑出来 runPackCheck(); } /* ==================== 鉴权头捕获 ==================== */ try { const origSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (k, v) { if (!AUTH_HDRS && /^blade-auth$/i.test(k)) { AUTH_HDRS = { 'Blade-Auth': v, 'Tenant-Id': localStorage.getItem('tenantId') || '' }; } return origSetHeader.apply(this, arguments); }; } catch (e) { } /* ==================== XHR / fetch 拦截 ==================== */ try { const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { this._blypedUrl = url; return origOpen.apply(this, arguments); }; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function () { const u = this._blypedUrl || ''; if (u.indexOf(API_MARK) !== -1 || u.indexOf(API_STATIC) !== -1) { this.addEventListener('load', function () { let j = null; try { j = JSON.parse(this.responseText); } catch (e) { return; } routeResponse(u, j); }); } return origSend.apply(this, arguments); }; } catch (e) { } try { const origFetch = window.fetch; if (origFetch) { window.fetch = function (input, init) { const url = (typeof input === 'string') ? input : ((input instanceof Request) ? input.url : ''); const hit = (url.indexOf(API_MARK) !== -1 || url.indexOf(API_STATIC) !== -1); if (!hit) return origFetch.apply(this, arguments); return origFetch.apply(this, arguments).then(function (response) { try { response.clone().json().then(function (data) { routeResponse(url, data); }).catch(function () { }); } catch (e) { } return response; }); }; } } catch (e) { } /* ==================== 帧切换监听 ==================== */ function handleFrameSwitch(no) { lastFrameNo = no; const fid = FRAME_IDS[no - 1]; const cached = (fid && FRAME_CACHE[fid]) ? FRAME_CACHE[fid] : null; if (cached) { analyzeShapes(cached, getImageSize()); } else { showWaiting(no); requestFrame(no); } } // 用 MutationObserver 即时感知帧切换,定时器只做兜底 function setupFrameWatcher() { const nav = document.querySelector('.frame-navigation'); if (!nav || nav.__blypedMo) return; nav.__blypedMo = new MutationObserver(function () { const n = currentFrameNo(); if (n !== null && n !== lastFrameNo) handleFrameSwitch(n); }); nav.__blypedMo.observe(nav, { childList: true, subtree: true, attributes: true, characterData: true }); } setInterval(function () { if (!staticTried && AUTH_HDRS && FRAME_IDS.length === 0) { staticTried = true; fetchStatic(); } setupFrameWatcher(); const no = currentFrameNo(); if (no !== null && no !== lastFrameNo) { handleFrameSwitch(no); return; } // 兜底:数据未变时 analyzeShapes 内部会因指纹相同直接 return,不会重建面板 if (lastBoxes) analyzeShapesFromCache(); }, 900); function analyzeShapesFromCache() { const no = currentFrameNo(); const fid = no ? FRAME_IDS[no - 1] : null; const list = fid ? FRAME_CACHE[fid] : null; if (list) analyzeShapes(list, getImageSize()); } /* ==================== 启动 ==================== */ function boot() { if (location.pathname.indexOf('/tool/image') === -1) return; installMaskCss(); createPanel(); renderPanel(); setTimeout(function () { if (AUTH_HDRS && !FRAME_IDS.length) fetchStatic(); }, 3000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } /* ==================== 调试导出 ==================== */ window.__BLYPED__ = { getState: function () { return { frameIds: FRAME_IDS.length, cached: Object.keys(FRAME_CACHE).length, auth: Boolean(AUTH_HDRS), curFrame: currentFrameNo(), warmupDone: WARMUP_DONE, cmap: CMAP ? { main: CMAP.idName[CMAP.mainId], other: CMAP.otherId ? CMAP.idName[CMAP.otherId] : null } : null, imgSize: getImageSize(), packStat: PACK_STAT }; }, analyze: analyzeShapes, frameStat: frameStat, boxOf: boxOf, isIgnored: isIgnored, sizeProblem: sizeProblem, minSize: { w: MIN_W, h: MIN_H }, runPackCheck: runPackCheck, copyPackInfo: copyPackInfo, setPackStat: function (s) { PACK_STAT = s; lastRenderKey = null; renderPanel(); }, setFrameIds: function (ids) { FRAME_IDS = ids || []; }, setCmap: function (m) { CMAP = m; }, closePanel: closePanel }; console.log('[比邻云行人助手] 脚本加载成功'); })();