// ==UserScript== // @name SZ手势脚本 // @namespace http://118.196.97.105:8080 // @version 3.2 // @description 手势验收:整包批量检查(无需标注帧无对象 + 结束帧必须有手势框)+ 常驻汇总(包号/框数/帧数/初始帧/结束帧)+ 一键跳转 + 复制数据 // @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_hand_result_v3'; const HAND_PNAME = 'handbboxxywh'; // 手势框 pname(一个包只有一个,且有属性) // ---- 工具 ---- function extractFrameNo(imgUrl) { if (!imgUrl) return '?'; const m = imgUrl.match(/\/img\/(\d+)\.jpe?g/i) || imgUrl.match(/(\d+)\.jpe?g/i); return m ? m[1] : '?'; } function markLabel(mk) { if (!mk) return '未知对象'; return (mk.class && mk.class.pname) || mk.pselect || mk.type || '未知对象'; } function isHandBox(mk) { return !!(mk && (mk.class && mk.class.pname === HAND_PNAME)); } 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, '"'); } // ---- 整包检查 ---- let packageState = null; // {packageId, taskKey, frameTotal, frames, results, errorFrames, rectTotal, rectFrameCount, firstBoxFrame, lastBoxFrame, lastFrameHand} 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=3&task_id=' + taskId + '&work_type=4&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 errorFrames = []; let rectTotal = 0; const rectFrameSet = {}; let firstBoxFrame = null; let lastBoxFrame = null; let lastFrameHasHand = false; 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 markStatus = data && data.mark_status; const rectCount = (Number(totalNums.rect) || 0); if (rectCount > 0) { rectTotal += rectCount; rectFrameSet[r.frame.taskId] = true; if (firstBoxFrame === null) firstBoxFrame = r.frame; lastBoxFrame = r.frame; } // 手势框判定:结束帧必须含 handbboxxywh if (rectCount > 0 && marks.some(isHandBox)) { lastFrameHasHand = true; // 只要有手势框即满足(一般只有一帧有) } // 无需标注帧有对象 → 异常 if (markStatus === 1 && marks.length > 0) { const types = {}; marks.forEach(function (mk) { const t = mk.type || '?'; types[t] = (types[t] || 0) + 1; }); errorFrames.push({ taskId: r.frame.taskId, frameNo: r.frame.frameNo, count: marks.length, types: types }); } }); // 结束帧必须含手势框(结束帧 = 最后一个有框帧;若整包无框则无结束帧) let lastFrameHand = null; // {pass, msg} if (lastBoxFrame) { const lfResult = results.find(function (r) { return r.frame.taskId === lastBoxFrame.taskId; }); const lfMarks = (lfResult && lfResult.data && lfResult.data.markData && lfResult.data.markData.marks) || []; const hasHand = lfMarks.some(isHandBox); lastFrameHand = { pass: hasHand, frameNo: lastBoxFrame.frameNo }; } packageState = { packageId: conf.package_id, taskKey: taskKey, frameTotal: frames.length, frames: frames, results: results, errorFrames: errorFrames, rectTotal: rectTotal, rectFrameCount: Object.keys(rectFrameSet).length, firstBoxFrame: firstBoxFrame, lastBoxFrame: lastBoxFrame, lastFrameHand: lastFrameHand, checkedAt: Date.now() }; saveState(); renderBatchResult(packageState); } catch (e) { showMessage('整包检查失败:' + (e.message || e), true); } finally { checking = false; setCheckingUI(false); } } // ---- 状态持久化(跳转刷新后恢复常驻数据)---- function saveState() { if (!packageState) return; try { const slim = { packageId: packageState.packageId, frameTotal: packageState.frameTotal, errorFrames: packageState.errorFrames, rectTotal: packageState.rectTotal, rectFrameCount: packageState.rectFrameCount, firstBoxFrame: packageState.firstBoxFrame ? { taskId: packageState.firstBoxFrame.taskId, frameNo: packageState.firstBoxFrame.frameNo } : null, lastBoxFrame: packageState.lastBoxFrame ? { taskId: packageState.lastBoxFrame.taskId, frameNo: packageState.lastBoxFrame.frameNo } : null, lastFrameHand: packageState.lastFrameHand, checkedAt: packageState.checkedAt }; localStorage.setItem(STORE_KEY, JSON.stringify(slim)); } 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 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; 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); } } // ---- 跳帧 ---- function jumpToTask(taskId) { const url = new URL(location.href); url.searchParams.set('id', taskId); location.href = url.toString(); } function jumpFirst() { if (!packageState || !packageState.firstBoxFrame) { showMessage('无初始帧(整包无框)', true); return; } jumpToTask(packageState.firstBoxFrame.taskId); } function jumpLast() { if (!packageState || !packageState.lastBoxFrame) { showMessage('无结束帧(整包无框)', true); return; } jumpToTask(packageState.lastBoxFrame.taskId); } // ---- 悬浮窗 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_hand_panel'); if (exist) { panelContainer = exist; panelBody = exist.querySelector('div[id="_gc_hand_body"]'); panelContent = exist.querySelector('div[id="_gc_hand_content"]'); return; } if (panelContainer) return; panelContainer = document.createElement('div'); panelContainer.id = '_gc_hand_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 = '手势验收|整包检查'; 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 row1 = document.createElement('div'); row1.style.cssText = 'display:flex;gap:8px;'; const checkBtn = document.createElement('button'); checkBtn.id = '_gc_check_btn'; checkBtn.textContent = '整包检查'; checkBtn.style.cssText = 'flex:1;padding:8px 10px;font-size:13px;font-weight:bold;border:none;border-radius:6px;cursor:pointer;background:#3b82f6;color:#fff;'; checkBtn.addEventListener('click', runPackageCheck); const jumpRow = document.createElement('div'); jumpRow.style.cssText = 'display:flex;gap:8px;margin-top:8px;'; const firstBtn = document.createElement('button'); firstBtn.id = '_gc_first_btn'; firstBtn.textContent = '跳初始帧'; firstBtn.style.cssText = 'flex:1;padding:7px 10px;font-size:12px;font-weight:bold;border:none;border-radius:6px;cursor:pointer;background:#8b5cf6;color:#fff;'; firstBtn.addEventListener('click', jumpFirst); const lastBtn = document.createElement('button'); lastBtn.id = '_gc_last_btn'; lastBtn.textContent = '跳结束帧'; lastBtn.style.cssText = 'flex:1;padding:7px 10px;font-size:12px;font-weight:bold;border:none;border-radius:6px;cursor:pointer;background:#8b5cf6;color:#fff;'; lastBtn.addEventListener('click', jumpLast); jumpRow.appendChild(firstBtn); jumpRow.appendChild(lastBtn); const copyBtn = document.createElement('button'); copyBtn.id = '_gc_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); row1.appendChild(checkBtn); opBar.appendChild(row1); opBar.appendChild(jumpRow); opBar.appendChild(copyBtn); panelContainer.appendChild(opBar); panelBody = document.createElement('div'); panelBody.id = '_gc_hand_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_hand_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_msg'); if (!m) { m = document.createElement('div'); m.id = '_gc_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_check_btn'); if (btn) { btn.textContent = on ? '检查中…' : '整包检查'; btn.style.opacity = on ? '0.6' : '1'; } } // ---- 整包结果渲染(常驻)---- function renderBatchResult(s) { createPanel(); if (!panelContent) return; const errCount = s.errorFrames ? s.errorFrames.length : 0; const ok = errCount === 0 && (!s.lastFrameHand || s.lastFrameHand.pass); let html = ''; // 总状态卡 html += '