// ==UserScript== // @name SZREID行人脚本 // @namespace http://118.196.97.105:8080 // @version 1.1 // @description REID行人验收:整包批量检查(无需标注帧无对象 + 框大小≥20×40 + person_id/appearance_id 规则校验)+ 复制数据 + 错误定位跳转,切帧不刷新 // @author CC // @match *:8080/* // @grant none // @run-at document-start // @license CC // ==/UserScript== (function () { 'use strict'; const API_PATH = '/api/task/get-mark-data'; const MARK_CONF_PATH = '/v2/tasks/mark-conf'; const CONCURRENCY = 6; const STORE_KEY = '_gc_reid_result_v1'; // 检测规则 const MIN_W = 20; // 框最小宽度(像素) const MIN_H = 40; // 框最小高度(像素) const AID_SUFFIX = '_A01'; // appearance_id 固定后缀 // ---- 工具 ---- function extractFrameNo(imgUrl) { if (!imgUrl) return '?'; // REID 格式: .../img/clip_0001_01_camera1_... → 帧号 + camera const m1 = imgUrl.match(/\/img\/clip_(\d+)_(\d+)_camera(\d+)/i); if (m1) return m1[1] + '(cam' + m1[3] + ')'; // REID 简略: clip_0001_... const m1b = imgUrl.match(/\/img\/clip_(\d+)/i); if (m1b) return m1b[1]; // 通用格式: /img/00002.jpeg const m2 = imgUrl.match(/\/img\/(\d+)\.jpe?g/i) || imgUrl.match(/(\d+)\.jpe?g/i); return m2 ? m2[1] : '?'; } function getTaskKey() { try { return new URLSearchParams(location.search).get('task_key') || ''; } catch (e) { return ''; } } function getToken() { try { return localStorage.getItem('token') || ''; } catch (e) { return ''; } } function esc(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } // attrs 值:attrs 里字段值都是数组,取 index 0 function attrVal(mk, key) { try { const v = mk.attrs && mk.attrs[key]; if (v === undefined || v === null) return null; if (Array.isArray(v)) { const x = v[0]; return (x === undefined || x === null) ? null : String(x); } return String(v); } catch (e) { return null; } } function boxSize(mk) { const p = mk.point; if (!p) return null; const l = parseFloat(p.left), t = parseFloat(p.top), r = parseFloat(p.right), b = parseFloat(p.bottom); if ([l, t, r, b].some(isNaN)) return null; return { w: r - l, h: b - t }; } // ---- 单帧校验:无标注帧有对象 + 尺寸 + person/appearance ---- function validateFrame(data) { const md = (data && data.markData) || {}; const marks = md.marks || []; const frameNo = extractFrameNo(md.imgUrl); const markStatus = data && data.mark_status; const errs = []; // {type, msg, mark} // 检测0:无需标注帧不允许有对象 if (markStatus === 1 && marks.length > 0) { errs.push({ type: 'no_mark_obj', msg: '无需标注的帧存在 ' + marks.length + ' 个对象(mark_status=1 不允许出现任何框)', mark: null }); } marks.forEach(function (mk) { const label = (mk.class && mk.class.pname) || mk.pselect || mk.type || '框'; const size = boxSize(mk); // 检测1:框大小 if (size) { if (size.w < MIN_W || size.h < MIN_H) { errs.push({ type: 'size', msg: '框尺寸不达标:宽 ' + size.w.toFixed(1) + '(需≥' + MIN_W + ')高 ' + size.h.toFixed(1) + '(需≥' + MIN_H + ')', mark: mk }); } } // 检测2:person_id / appearance_id const pid = attrVal(mk, 'person_id'); if (pid !== null && pid !== '') { if (/\s/.test(pid)) { errs.push({ type: 'pid_space', msg: 'person_id 含空格:「' + pid + '」(不允许有空格)', mark: mk }); } else { const aid = attrVal(mk, 'appearance_id'); const expected = pid + AID_SUFFIX; if (aid !== expected) { errs.push({ type: 'aid', msg: 'appearance_id 不匹配:person_id=' + pid + ' 期望 "' + expected + '",实际 "' + (aid === null ? '空' : aid) + '"', mark: mk }); } } } }); return { frameNo: frameNo, marks: marks, errs: errs, pass: errs.length === 0 }; } // ---- 整包检查 ---- let packageState = null; let checking = false; async function fetchMarkConf() { const taskId = new URLSearchParams(location.search).get('id'); const token = getToken(); const resp = await fetch(MARK_CONF_PATH + '?status=' + (new URLSearchParams(location.search).get('status') || '0') + '&task_id=' + taskId + '&work_type=4&access=' + (new URLSearchParams(location.search).get('access') || '1'), { method: 'GET', headers: { 'Access-Key': token } }); const d = await resp.json(); if (!d.data || !d.data.frame) throw new Error('mark-conf 未返回 frame 数据'); return d.data; } async function fetchFrameData(taskId, taskKey) { const token = getToken(); const resp = await fetch(API_PATH, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Access-Key': token, 'X-GC-Internal': '1' }, body: JSON.stringify({ time: Date.now(), task_id: taskId, task_key: taskKey, is_preview: 0 }) }); const d = await resp.json(); if (d.code !== 0 || !d.data) throw new Error('task ' + taskId + ' 返回异常'); return d.data; } async function runBatch(frames, taskKey) { const results = new Array(frames.length); let idx = 0; const workers = []; for (let w = 0; w < CONCURRENCY; w++) { workers.push((async () => { while (true) { const i = idx++; if (i >= frames.length) return; const f = frames[i]; try { results[i] = { frame: f, data: await fetchFrameData(f.taskId, taskKey) }; } catch (e) { results[i] = { frame: f, error: String(e.message || e) }; } updateProgress(i + 1, frames.length); } })()); } await Promise.all(workers); return results; } function updateProgress(done, total) { const el = document.getElementById('_gc_progress'); if (el) el.textContent = '检查中 ' + done + '/' + total; } async function runPackageCheck() { if (checking) return; checking = true; setCheckingUI(true); try { const conf = await fetchMarkConf(); const frames = conf.frame.map(function (f) { return { taskId: f.task_id, picUrl: f.pic_url, frameNo: extractFrameNo(f.pic_url) }; }); const taskKey = conf.task_key || getTaskKey(); const results = await runBatch(frames, taskKey); // 汇总:每帧的错误 const frameErrors = []; // {frameNo, taskId, errs: []} let rectTotal = 0; const rectFrameSet = {}; const typeCount = { size: 0, pid_space: 0, aid: 0, no_mark_obj: 0 }; results.forEach(function (r) { if (r.error) return; const data = r.data; const md = (data && data.markData) || {}; const marks = md.marks || []; const totalNums = md.totalNums || {}; const rectCount = (Number(totalNums.rect) || 0); if (rectCount > 0) { rectTotal += rectCount; rectFrameSet[r.frame.taskId] = true; } const v = validateFrame(data); if (!v.pass) { v.errs.forEach(function (e) { typeCount[e.type] = (typeCount[e.type] || 0) + 1; }); frameErrors.push({ frameNo: v.frameNo, taskId: r.frame.taskId, errs: v.errs }); } }); packageState = { packageId: conf.package_id, taskKey: taskKey, frameTotal: frames.length, rectTotal: rectTotal, rectFrameCount: Object.keys(rectFrameSet).length, frameErrors: frameErrors, typeCount: typeCount, checkedAt: Date.now() }; saveState(); renderBatchResult(packageState); } catch (e) { showMessage('整包检查失败:' + (e.message || e), true); } finally { checking = false; setCheckingUI(false); } } // ---- 状态持久化 ---- function saveState() { if (!packageState) return; try { localStorage.setItem(STORE_KEY, JSON.stringify(packageState)); } catch (e) {} } function restoreState() { try { const raw = localStorage.getItem(STORE_KEY); if (!raw) return; const s = JSON.parse(raw); if (s && String(s.packageId) === String(getCurrentPackageId())) { packageState = s; renderBatchResult(s); } } catch (e) {} } function getCurrentPackageId() { try { return new URLSearchParams(location.search).get('package_id') || ''; } catch (e) { return ''; } } // ---- 跳帧 ---- function jumpToTask(taskId) { const url = new URL(location.href); url.searchParams.set('id', taskId); location.href = url.toString(); } // ---- 复制数据:包号 + 2 空白单元格 + 框数 + 帧数 ---- function copyText(text) { return new Promise(function (resolve, reject) { try { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(text).then(resolve, function () { fallbackCopy(text, resolve, reject); }); } else { fallbackCopy(text, resolve, reject); } } catch (e) { fallbackCopy(text, resolve, reject); } }); } function fallbackCopy(text, resolve, reject) { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); if (ok) resolve(); else reject(new Error('execCommand copy 失败')); } catch (e) { reject(e); } } async function copySummaryData() { if (!packageState) { showMessage('先执行整包检查,正在自动检查…', false); await runPackageCheck(); } if (!packageState) return; const s = packageState; // 包号 + 2 个空白单元格 + 框数 + 帧数(5 格) const text = s.packageId + '\t\t\t' + s.rectTotal + '\t' + s.rectFrameCount; try { await copyText(text); showMessage('已复制:' + text.replace(/\t/g, ' | '), false); } catch (e) { showMessage('复制失败:' + (e.message || e), true); } } // ---- 悬浮窗 UI ---- let panelContainer = null; let panelBody = null; let panelContent = null; let isMinimized = false; let isDragging = false; let dragOffsetX = 0; let dragOffsetY = 0; let panelX = 20; let panelY = 60; function createPanel() { const exist = document.getElementById('_gc_reid_panel'); if (exist) { panelContainer = exist; panelBody = exist.querySelector('div[id="_gc_reid_body"]'); panelContent = exist.querySelector('div[id="_gc_reid_content"]'); return; } if (panelContainer) return; panelContainer = document.createElement('div'); panelContainer.id = '_gc_reid_panel'; panelContainer.style.cssText = 'position:fixed;z-index:999999;width:380px;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 = 'REID行人验收|整包检查'; titleBar.addEventListener('mousedown', startDrag); panelContainer.appendChild(titleBar); const btnGroup = document.createElement('div'); btnGroup.style.cssText = 'display:flex;gap:6px;'; const minBtn = createBtn('—', '#e5e7eb', '#d1d5db', function (e) { e.stopPropagation(); toggleMinimize(); }); const closeBtn = createBtn('✕', '#fca5a5', '#f87171', function (e) { e.stopPropagation(); closePanel(); }); btnGroup.appendChild(minBtn); btnGroup.appendChild(closeBtn); titleBar.appendChild(btnGroup); const opBar = document.createElement('div'); opBar.style.cssText = 'padding:10px 12px;background:#f9fafb;border-bottom:1px solid #e5e7eb;'; const checkBtn = document.createElement('button'); checkBtn.id = '_gc_reid_check_btn'; checkBtn.textContent = '整包检查'; checkBtn.style.cssText = 'width:100%;padding:10px 10px;font-size:14px;font-weight:bold;border:none;border-radius:6px;cursor:pointer;background:#3b82f6;color:#fff;'; checkBtn.addEventListener('click', runPackageCheck); const copyBtn = document.createElement('button'); copyBtn.id = '_gc_reid_copy_btn'; copyBtn.textContent = '复制数据'; copyBtn.style.cssText = 'width:100%;margin-top:8px;padding:12px 10px;font-size:14px;font-weight:bold;border:none;border-radius:6px;cursor:pointer;background:#059669;color:#fff;'; copyBtn.addEventListener('click', copySummaryData); opBar.appendChild(checkBtn); opBar.appendChild(copyBtn); panelContainer.appendChild(opBar); panelBody = document.createElement('div'); panelBody.id = '_gc_reid_body'; panelBody.style.cssText = 'max-height:500px;overflow-y:auto;transition:max-height 0.25s ease;'; panelContainer.appendChild(panelBody); panelContent = document.createElement('div'); panelContent.id = '_gc_reid_content'; panelContent.style.cssText = 'padding:12px;background:#ffffff;'; panelBody.appendChild(panelContent); document.body.appendChild(panelContainer); } function createBtn(text, color, hoverColor, onClick) { const btn = document.createElement('span'); btn.textContent = text; btn.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 + ';'; btn.addEventListener('mouseenter', function () { btn.style.background = hoverColor; }); btn.addEventListener('mouseleave', function () { btn.style.background = color; }); btn.addEventListener('click', onClick); return btn; } function toggleMinimize() { isMinimized = !isMinimized; if (isMinimized) { panelBody.style.maxHeight = '0'; panelBody.style.padding = '0'; } else { panelBody.style.maxHeight = '500px'; panelBody.style.padding = ''; } } function closePanel() { if (panelContainer) { panelContainer.remove(); panelContainer = null; } } function startDrag(e) { if (e.button !== 0) return; isDragging = true; const rect = panelContainer.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; panelContainer.style.cursor = 'grabbing'; panelContainer.style.transition = 'none'; 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; if (panelContainer) { panelContainer.style.cursor = ''; } document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); } function showMessage(msg, isError) { createPanel(); if (!panelContent) return; let m = document.getElementById('_gc_reid_msg'); if (!m) { m = document.createElement('div'); m.id = '_gc_reid_msg'; m.style.cssText = 'padding:8px 10px;margin-bottom:10px;border-radius:8px;font-size:12px;line-height:1.5;'; panelContent.insertBefore(m, panelContent.firstChild); } m.style.background = isError ? '#fef2f2' : '#ecfdf5'; m.style.border = '1px solid ' + (isError ? '#fca5a5' : '#6ee7b7'); m.style.color = isError ? '#dc2626' : '#059669'; m.innerHTML = esc(msg); setTimeout(function () { if (m.parentNode) m.parentNode.removeChild(m); }, 4000); } function setCheckingUI(on) { const btn = document.getElementById('_gc_reid_check_btn'); if (btn) { btn.textContent = on ? '检查中…' : '整包检查'; btn.style.opacity = on ? '0.6' : '1'; } } // ---- 整包结果渲染 ---- const TYPE_LABEL = { size: '尺寸不达标', pid_space: 'person_id含空格', aid: 'appearance_id不匹配', no_mark_obj: '无需标注有对象' }; const TYPE_COLOR = { size: '#d97706', pid_space: '#dc2626', aid: '#dc2626', no_mark_obj: '#dc2626' }; function renderBatchResult(s) { createPanel(); if (!panelContent) return; const errCount = s.frameErrors ? s.frameErrors.length : 0; const ok = errCount === 0; let html = ''; // 状态卡 html += '
'; html += '' + (ok ? '✅' : '⚠️') + ''; html += '
'; html += '
整包检查:' + (ok ? '全部通过' : errCount + ' 帧异常') + '
'; html += '包 ' + s.packageId + ' · 共 ' + s.frameTotal + ' 帧 · ' + (s.checkedAt ? new Date(s.checkedAt).toLocaleTimeString() : '-') + ''; html += '
'; // 汇总数据 html += '
'; html += '
'; html += cell('包号', s.packageId); html += cell('框数', s.rectTotal); html += cell('有框帧数', s.rectFrameCount); html += '
'; // 错误分类统计 if (!ok) { const tc = s.typeCount || {}; html += '
'; Object.keys(TYPE_LABEL).forEach(function (t) { if (tc[t] > 0) { html += '' + TYPE_LABEL[t] + ' ×' + tc[t] + ''; } }); html += '
'; } html += '
'; // 错误帧列表 if (errCount > 0) { html += '
⚠ 异常帧(点击跳转):
'; s.frameErrors.forEach(function (ef) { html += '
'; html += '
帧 ' + esc(ef.frameNo) + '' + ef.errs.length + ' 个问题 →
'; ef.errs.forEach(function (e) { html += '
• ' + esc(e.msg) + '
'; }); html += '
'; }); } else { html += '
未发现异常帧。所有框尺寸达标,person_id/appearance_id 规则全部正确。
'; } panelContent.innerHTML = html; panelContent.querySelectorAll('[data-gc-task]').forEach(function (el) { el.addEventListener('click', function () { jumpToTask(el.getAttribute('data-gc-task')); }); }); panelBody.scrollTop = 0; } function cell(label, value) { return '
' + label + '
' + esc(value) + '
'; } // ---- 初始化 ---- function initPanel() { createPanel(); if (!packageState) restoreState(); if (!packageState && panelContent) { panelContent.innerHTML = '
点击「整包检查」获取全帧数据并校验(框大小 ≥' + MIN_W + '×' + MIN_H + ' / person_id ↔ appearance_id 规则)。
'; } } // ---- 数据拦截:仅记录不刷新 ---- let lastFrameData = null; const origOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (method, url) { this._url = url; return origOpen.apply(this, arguments); }; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function (body) { if (this._url && this._url.indexOf(API_PATH) !== -1) { this.addEventListener('load', function () { try { const resp = JSON.parse(this.responseText); if (resp.code === 0 && resp.data) lastFrameData = resp.data; } catch (e) {} }); } return origSend.apply(this, arguments); }; const origFetch = window.fetch; if (origFetch) { window.fetch = function (input, init) { const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : ''); if (url.indexOf(API_PATH) !== -1) { let isInternal = false; try { const h = init && init.headers; if (h) { if (h instanceof Headers) isInternal = h.get('X-GC-Internal') === '1'; else if (typeof h === 'object') isInternal = h['X-GC-Internal'] === '1'; } } catch (e) {} if (!isInternal) { return origFetch.apply(this, arguments).then(function (response) { const clone = response.clone(); clone.json().then(function (data) { if (data.code === 0 && data.data) lastFrameData = data.data; }).catch(function () {}); return response; }); } } return origFetch.apply(this, arguments); }; } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPanel); } else { initPanel(); } console.log('[REID行人验收] 脚本加载成功'); })();