// ==UserScript==
// @name 镂空拖挂头标注
// @namespace lh6v.trailer-head.mark-guard
// @version 1.0.1
// @description 2D 图像标注页实时校验:nut / pillar / base_nut / base / lockring_center / lockring 六个类别各限出现一次,总数不超过 6 个,可少不可多;不合规时拦截保存与提交。
// @author CC
// @match *://*/w/task.html*
// @match *://*/w/task-angle.html*
// @match *://*/w/task-check.html*
// @run-at document-start
// @grant none
// @noframes
// ==/UserScript==
/* eslint-disable no-empty */
(function () {
'use strict';
// ────────────────────────────────────────────────────────────────────────────
/* ==RULES-BEGIN== */
/** 允许出现的六个类别(顺序即面板展示顺序),pname 与服务端 mark-conf 返回的 class.pname 逐字一致 */
var CLASSES = [
{ pname: 'nut', label: 'nut(插梢上面的螺帽)' },
{ pname: 'pillar', label: 'pillar(插销)' },
{ pname: 'base_nut', label: 'base_nut(基座上的螺帽)' },
{ pname: 'base', label: 'base(基座)' },
{ pname: 'lockring_center', label: 'lockring_center(锁环中心)' },
{ pname: 'lockring', label: 'lockring(锁环)' }
];
var MAX_TOTAL = 6;
var ALLOWED = Object.create(null);
var LABEL = Object.create(null);
CLASSES.forEach(function (c) {
ALLOWED[c.pname] = true;
LABEL[c.pname] = c.label;
});
function labelOf(pname) {
return LABEL[pname] || pname;
}
/**
* 取一个标注物的类别名。
* 标注器「实时态」把 class 存成数组(canvas-common.js: mark.class = [classes[i]]),
* 「保存/接口态」是对象 {pname, preset_id}(submitClick 里做了归一化)。两种都要兼容。
*/
function pnameOf(mark) {
if (!mark) return '';
var c = mark.class;
if (Array.isArray(c)) {
if (!c.length) return '';
var last = c[c.length - 1];
if (!last) return '';
if (typeof last === 'string') return last;
return last.pname || '';
}
if (typeof c === 'string') return c;
if (c && typeof c === 'object') return c.pname || '';
return '';
}
/**
* 校验一组 marks。
* 规则:① 每个允许的类别最多出现一次;② 总数不超过 6;③ 不允许出现六类之外的类别(含未选择类别)。
* 可以少。
*/
function validate(marks) {
var arr = Array.isArray(marks) ? marks : [];
var counts = Object.create(null);
CLASSES.forEach(function (c) { counts[c.pname] = 0; });
var items = arr.map(function (m, i) {
return {
index: i + 1,
pname: pnameOf(m),
markId: m && m.markId,
type: m && m.type
};
});
var unknown = [];
items.forEach(function (it) {
if (ALLOWED[it.pname]) counts[it.pname]++;
else unknown.push(it);
});
var issues = [];
CLASSES.forEach(function (c) {
if (counts[c.pname] > 1) {
var idx = items
.filter(function (it) { return it.pname === c.pname; })
.map(function (it) { return it.index; });
issues.push({
kind: 'duplicate',
pname: c.pname,
indexes: idx,
text: '「' + labelOf(c.pname) + '」重复出现 ' + idx.length + ' 次(第 ' + idx.join('、') + ' 个标注物)'
});
}
});
unknown.forEach(function (it) {
issues.push({
kind: 'unknown',
pname: it.pname,
indexes: [it.index],
text: it.pname
? '第 ' + it.index + ' 个标注物的类别「' + it.pname + '」不属于允许的 6 个类别'
: '第 ' + it.index + ' 个标注物还没有选择类别'
});
});
if (items.length > MAX_TOTAL) {
issues.push({
kind: 'overflow',
pname: '',
indexes: items.map(function (it) { return it.index; }),
text: '标注物共 ' + items.length + ' 个,超过上限 ' + MAX_TOTAL + ' 个'
});
}
return {
ok: issues.length === 0,
total: items.length,
counts: counts,
items: items,
issues: issues,
badIndexes: (function () {
var s = Object.create(null);
issues.forEach(function (is) {
is.indexes.forEach(function (i) { s[i] = true; });
});
return Object.keys(s).map(Number).sort(function (a, b) { return a - b; });
})()
};
}
/* ==RULES-END== */
// ────────────────────────────────────────────────────────────────────────────
// 2. 平台适配层(唯一与本站点耦合的部分)
// ────────────────────────────────────────────────────────────────────────────
var MARK_DATA_URL_RE = /\/v2\/tasks\/mark-data(\?|$)/;
function jq() {
return window.jQuery || window.$ || null;
}
/** 标注器实时数据:window.InitCanvasInfo.canvasCommon.bg_img.marks(service-mark.js 里挂到 window 的) */
function readLiveMarks() {
try {
var ci = window.InitCanvasInfo;
var cc = ci && ci.canvasCommon;
var bg = cc && cc.bg_img;
if (bg && Array.isArray(bg.marks)) return bg.marks;
} catch (e) {}
try {
var ci2 = window.InitCanvasInfo;
var cc2 = ci2 && ci2.canvasNormal;
var bg2 = cc2 && cc2.bg_img;
if (bg2 && Array.isArray(bg2.marks)) return bg2.marks;
} catch (e) {}
return null;
}
function liveConnected() {
return readLiveMarks() !== null;
}
/** 高亮右侧标注物列表里出问题的条目 */
function locateBadMarks(badIndexes) {
var $ = jq();
if (!$ || !badIndexes || !badIndexes.length) return;
$('#toolBarList li').removeClass('lh6v-bad');
var first = null;
badIndexes.forEach(function (i) {
var $li = $('#toolBarList li[data-index="' + i + '"]');
if ($li.length) {
$li.addClass('lh6v-bad');
if (!first) first = $li;
}
});
if (first && first[0] && first[0].scrollIntoView) {
try { first[0].scrollIntoView({ block: 'nearest' }); } catch (e) {}
}
}
function clearBadMarks() {
var $ = jq();
if ($) $('#toolBarList li').removeClass('lh6v-bad');
}
function hideLoaders() {
var $ = jq();
if (!$) return;
try { $('#load').hide(); } catch (e) {}
try { $('#loadup').hide(); } catch (e) {}
}
// ────────────────────────────────────────────────────────────────────────────
// 3. 配置
// ────────────────────────────────────────────────────────────────────────────
var CFG_KEY = 'lh6v.cfg.v1';
var CFG = {
blockSave: true, // 拦截「暂时保存」等手动保存
blockSubmit: true, // 拦截「提交 / 继续提交」
blockAuto: false, // 拦截自动保存(默认关闭,避免丢数据)
locate: true, // 拦截后自动定位并高亮问题标注物
allowForce: true, // 是否在拦截弹窗里保留「强制保存」入口
x: null,
y: null,
collapsed: false
};
function loadCfg() {
try {
var raw = localStorage.getItem(CFG_KEY);
if (!raw) return;
var o = JSON.parse(raw);
Object.keys(CFG).forEach(function (k) {
if (o[k] !== undefined) CFG[k] = o[k];
});
} catch (e) {}
}
function saveCfg() {
try { localStorage.setItem(CFG_KEY, JSON.stringify(CFG)); } catch (e) {}
}
loadCfg();
// ────────────────────────────────────────────────────────────────────────────
// 4. 界面(Shadow DOM,避免与本页 CSS 互相污染)
// ────────────────────────────────────────────────────────────────────────────
var host = null, root = null;
var el = {};
var toastTimer = null;
var PANEL_CSS = [
':host{all:initial}',
'*{box-sizing:border-box;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}',
'.wrap{position:fixed;z-index:2147483000;width:268px;background:#fff;border:1px solid #d8dee6;border-radius:10px;',
'box-shadow:0 8px 28px rgba(15,23,42,.18);font-size:12px;color:#1f2937;overflow:hidden;user-select:none}',
'.hd{display:flex;align-items:center;gap:6px;padding:8px 10px;background:#1f2937;color:#fff;cursor:move}',
'.hd .t{flex:1;font-size:12px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.hd .b{border:0;background:transparent;color:#cbd5e1;cursor:pointer;font-size:13px;line-height:1;padding:2px 3px;border-radius:4px}',
'.hd .b:hover{background:rgba(255,255,255,.15);color:#fff}',
'.st{padding:7px 10px;font-weight:600;display:flex;align-items:center;gap:6px;border-bottom:1px solid #eef2f7}',
'.st.ok{background:#ecfdf5;color:#047857}',
'.st.bad{background:#fef2f2;color:#b91c1c}',
'.st.warn{background:#fffbeb;color:#b45309}',
'.st .cnt{margin-left:auto;font-weight:500;font-size:11px;opacity:.85}',
'.rows{padding:4px 0}',
'.row{display:flex;align-items:center;gap:6px;padding:4px 10px;line-height:1.35}',
'.row .nm{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}',
'.row .pin{width:16px;text-align:center;font-weight:700}',
'.row.v1{color:#047857}',
'.row.v1 .pin{color:#059669}',
'.row.v0{color:#9ca3af}',
'.row.v0 .pin{color:#d1d5db}',
'.row.vbad{background:#fef2f2;color:#b91c1c;font-weight:600}',
'.row.vbad .pin{color:#dc2626}',
'.row .idx{font-size:10px;color:#9ca3af;font-weight:400}',
'.row.vbad .idx{color:#ef4444}',
'.msg{margin:2px 8px 6px;padding:6px 8px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;color:#b91c1c;font-size:11px;line-height:1.5;max-height:132px;overflow:auto;user-select:text}',
'.msg div+div{margin-top:3px}',
'.ft{display:flex;align-items:center;gap:8px;padding:6px 10px;border-top:1px solid #eef2f7;background:#f9fafb;font-size:11px;color:#6b7280}',
'.ft a{color:#2563eb;cursor:pointer;text-decoration:none}',
'.ft a:hover{text-decoration:underline}',
'.set{padding:8px 10px;border-top:1px solid #eef2f7;background:#f9fafb;display:none}',
'.set.on{display:block}',
'.set label{display:flex;align-items:center;gap:6px;padding:3px 0;cursor:pointer;color:#374151}',
'.set input{accent-color:#2563eb}',
'.toast{position:fixed;z-index:2147483001;left:50%;bottom:48px;transform:translateX(-50%);background:rgba(17,24,39,.94);',
'color:#fff;padding:8px 14px;border-radius:8px;font-size:12px;max-width:70vw;line-height:1.5;box-shadow:0 6px 20px rgba(0,0,0,.3)}',
'.mask{position:fixed;z-index:2147483002;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center}',
'.dlg{width:460px;max-width:92vw;max-height:82vh;background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.35);overflow:hidden;display:flex;flex-direction:column}',
'.dlg .dh{padding:12px 16px;background:#fef2f2;border-bottom:1px solid #fecaca;color:#b91c1c;font-weight:700;font-size:14px;display:flex;align-items:center;gap:8px}',
'.dlg .db{padding:14px 16px;overflow:auto;font-size:13px;line-height:1.65;color:#374151;user-select:text}',
'.dlg .db ul{margin:6px 0 0;padding-left:20px}',
'.dlg .db li{margin:4px 0}',
'.dlg .db .hint{margin-top:10px;color:#6b7280;font-size:12px}',
'.dlg .df{padding:10px 16px;border-top:1px solid #eef2f7;display:flex;gap:8px;align-items:center;justify-content:flex-end;background:#f9fafb}',
'.dlg button{border:1px solid #d1d5db;background:#fff;color:#374151;padding:6px 14px;border-radius:6px;cursor:pointer;font-size:13px}',
'.dlg button:hover{background:#f3f4f6}',
'.dlg button.pri{background:#2563eb;border-color:#2563eb;color:#fff}',
'.dlg button.pri:hover{background:#1d4ed8}',
'.dlg .force{margin-right:auto;font-size:12px;color:#9ca3af;text-decoration:underline;cursor:pointer;background:none;border:0;padding:0}',
'.dlg .force:hover{color:#b91c1c}'
].join('');
function buildPanel() {
host = document.createElement('div');
host.id = 'lh6v-host';
host.style.cssText = 'all:initial;display:block';
root = host.attachShadow({ mode: 'open' });
var style = document.createElement('style');
style.textContent = PANEL_CSS;
root.appendChild(style);
var wrap = document.createElement('div');
wrap.className = 'wrap';
wrap.innerHTML = [
'
',
' 六类唯一性校验',
' ',
' ',
'
',
'',
'
·正在连接标注器…
',
'
',
'
',
'
',
'
',
'
'
].join('');
root.appendChild(wrap);
// 初次定位
wrap.style.left = (CFG.x != null ? CFG.x : Math.max(8, window.innerWidth - 288)) + 'px';
wrap.style.top = (CFG.y != null ? CFG.y : 96) + 'px';
el.wrap = wrap;
el.st = wrap.querySelector('[data-st]');
el.stIcon = wrap.querySelector('[data-st-icon]');
el.stText = wrap.querySelector('[data-st-text]');
el.stCnt = wrap.querySelector('[data-st-cnt]');
el.rows = wrap.querySelector('[data-rows]');
el.msg = wrap.querySelector('[data-msg]');
el.set = wrap.querySelector('[data-set]');
el.diag = wrap.querySelector('[data-diag]');
el.mode = wrap.querySelector('[data-mode]');
el.body = wrap.querySelector('[data-body]');
// 六行固定结构,后续只改 class / 文本
CLASSES.forEach(function (c) {
var row = document.createElement('div');
row.className = 'row v0';
row.setAttribute('data-pname', c.pname);
row.innerHTML =
'○' +
'' + c.label + '' +
'';
el.rows.appendChild(row);
});
// 拖动
var dragging = null;
wrap.querySelector('[data-drag]').addEventListener('mousedown', function (ev) {
if (ev.target.closest('button')) return;
dragging = { dx: ev.clientX - wrap.offsetLeft, dy: ev.clientY - wrap.offsetTop };
ev.preventDefault();
});
window.addEventListener('mousemove', function (ev) {
if (!dragging) return;
var x = Math.min(Math.max(0, ev.clientX - dragging.dx), window.innerWidth - 60);
var y = Math.min(Math.max(0, ev.clientY - dragging.dy), window.innerHeight - 30);
wrap.style.left = x + 'px';
wrap.style.top = y + 'px';
});
window.addEventListener('mouseup', function () {
if (!dragging) return;
dragging = null;
CFG.x = parseInt(wrap.style.left, 10);
CFG.y = parseInt(wrap.style.top, 10);
saveCfg();
});
// 按钮
wrap.addEventListener('click', function (ev) {
var t = ev.target.closest('[data-act]');
if (t) {
var act = t.getAttribute('data-act');
if (act === 'fold') {
CFG.collapsed = !CFG.collapsed;
saveCfg();
applyCollapsed();
} else if (act === 'set') {
el.set.classList.toggle('on');
} else if (act === 'recheck') {
tick(true);
}
return;
}
var cb = ev.target.closest('input[data-cfg]');
if (cb) {
CFG[cb.getAttribute('data-cfg')] = cb.checked;
saveCfg();
}
});
// 同步 checkbox
wrap.querySelectorAll('input[data-cfg]').forEach(function (inp) {
inp.checked = !!CFG[inp.getAttribute('data-cfg')];
});
applyCollapsed();
(document.body || document.documentElement).appendChild(host);
}
function applyCollapsed() {
if (!el.body) return;
el.body.style.display = CFG.collapsed ? 'none' : '';
}
function toast(text, ms) {
if (!root) return;
var old = root.querySelector('.toast');
if (old) old.remove();
var d = document.createElement('div');
d.className = 'toast';
d.textContent = text;
root.appendChild(d);
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { d.remove(); }, ms || 2600);
}
function highlightCss() {
if (document.getElementById('lh6v-page-css')) return;
var s = document.createElement('style');
s.id = 'lh6v-page-css';
s.textContent =
'#toolBarList li.lh6v-bad{outline:2px solid #dc2626 !important;outline-offset:-2px;' +
'background:rgba(220,38,38,.12) !important;animation:lh6v-blink 1s ease-in-out 3}' +
'@keyframes lh6v-blink{0%,100%{background:rgba(220,38,38,.12)}50%{background:rgba(220,38,38,.4)}}';
(document.head || document.documentElement).appendChild(s);
}
// 拦截弹窗
var dlgNode = null;
function closeDialog() {
if (dlgNode) { dlgNode.remove(); dlgNode = null; }
}
function showBlockDialog(res, onForce) {
closeDialog();
var mask = document.createElement('div');
mask.className = 'mask';
var items = res.issues.map(function (i) { return '' + escapeHtml(i.text) + ''; }).join('');
var dlg = document.createElement('div');
dlg.className = 'dlg';
dlg.innerHTML =
'⚠ 标注数据不合规,已阻止保存
' +
'' +
'
本图的标注物必须满足:六个类别(' + CLASSES.map(function (c) { return c.pname; }).join(' / ') +
')各最多一个,总数不超过 ' + MAX_TOTAL + ' 个,可少不可多。
' +
'
' +
'
当前共 ' + res.total + ' 个标注物。请删除或修正后再保存。
' +
'
' +
'' +
(CFG.allowForce ? '' : '') +
'' +
'' +
'
';
mask.appendChild(dlg);
root.appendChild(mask);
dlgNode = mask;
dlg.addEventListener('click', function (ev) {
if (ev.target.closest('[data-ok]')) { closeDialog(); return; }
if (ev.target.closest('[data-locate]')) { locateBadMarks(res.badIndexes); return; }
if (ev.target.closest('[data-force]')) { closeDialog(); onForce(); return; }
});
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
});
}
// ────────────────────────────────────────────────────────────────────────────
// 5. 面板刷新
// ────────────────────────────────────────────────────────────────────────────
var lastRes = null;
function render(res, connected) {
lastRes = res;
// 状态条
var cls, icon, text;
if (!connected) {
cls = 'warn'; icon = '?';
text = '未读到标注器数据';
} else if (res.ok) {
cls = 'ok'; icon = '✓';
text = res.total === 0 ? '暂无标注物(合规)' : '合规';
} else {
cls = 'bad'; icon = '✕';
text = '不合规 · ' + res.issues.length + ' 项问题';
}
el.st.className = 'st ' + cls;
el.stIcon.textContent = icon;
el.stText.textContent = text;
el.stCnt.textContent = connected ? res.total + ' / ' + MAX_TOTAL : '';
// 六行
CLASSES.forEach(function (c) {
var row = el.rows.querySelector('.row[data-pname="' + c.pname + '"]');
if (!row) return;
var n = res.counts[c.pname] || 0;
var idx = res.items
.filter(function (it) { return it.pname === c.pname; })
.map(function (it) { return it.index; });
var v;
if (!connected) v = 'v0';
else if (n === 0) v = 'v0';
else if (n === 1) v = 'v1';
else v = 'vbad';
row.className = 'row ' + v;
row.querySelector('.pin').textContent = n === 0 ? '○' : (n === 1 ? '●' : '✕');
row.querySelector('.idx').textContent = n === 0 ? '未标注' : ('×' + n + ' 第' + idx.join('/') + '个');
});
// 问题清单
if (connected && !res.ok) {
el.msg.style.display = '';
el.msg.innerHTML = res.issues.map(function (i) {
return '· ' + escapeHtml(i.text) + '
';
}).join('');
} else {
el.msg.style.display = 'none';
el.msg.innerHTML = '';
}
// 诊断
el.diag.textContent =
'数据源 ' + (connected ? 'window.InitCanvasInfo.canvasCommon.bg_img.marks' : '未连接') +
' | 保存钩子 ' + (ajaxPatched ? '已安装' : '未安装');
el.mode.textContent = '实时校验';
}
var rafPending = false;
function tick(force) {
if (!el.rows) return;
var marks = readLiveMarks();
var connected = marks !== null;
var res = validate(marks || []);
render(res, connected);
if (force && !connected) toast('没读到标注器数据,请确认当前页是 /w/task.html 标注页');
return res;
}
// ────────────────────────────────────────────────────────────────────────────
// 6. AJAX 拦截
// ────────────────────────────────────────────────────────────────────────────
var ajaxPatched = false;
var lastGesture = 0;
['mousedown', 'pointerdown', 'touchstart', 'keydown'].forEach(function (ev) {
window.addEventListener(ev, function () { lastGesture = Date.now(); }, true);
});
function isManual() {
return Date.now() - lastGesture < 3000;
}
function parseBody(data) {
if (!data) return null;
if (typeof data === 'string') {
try { return JSON.parse(data); } catch (e) { return null; }
}
if (typeof data === 'object') return data;
return null;
}
/**
* 返回 null 表示放行;返回 {res, mode:'block'|'warn'} 表示校验不通过。
*/
function inspect(settings) {
var type = String(settings.type || settings.method || 'GET').toUpperCase();
if (type !== 'POST') return null;
var url = String(settings.url || '');
if (!MARK_DATA_URL_RE.test(url)) return null;
var body = parseBody(settings.data);
if (!body || !body.markData || !Array.isArray(body.markData.marks)) return null;
var res = validate(body.markData.marks);
if (res.ok) {
if (CFG.locate) clearBadMarks();
return { res: res, mode: 'ok' };
}
var action = String(body.action || '');
var manual = isManual();
var auto = !manual;
var block;
if (action === 'submit') block = CFG.blockSubmit;
else if (auto) block = CFG.blockAuto;
else block = CFG.blockSave;
return { res: res, mode: block ? 'block' : 'warn', action: action, auto: auto };
}
function makeStub(settings, verdict) {
var stub = {
readyState: 0,
status: 0,
statusText: 'blocked-by-lh6v',
abort: function () {},
done: function () { return stub; },
fail: function (cb) { return stub; },
always: function (cb) { return stub; },
then: function () { return stub; }
};
// 走一遍 error 回调,让调用方(commonAjax)自己收尾隐藏 loading
setTimeout(function () {
try {
if (typeof settings.error === 'function') {
settings.error({ status: 0, statusText: 'blocked', responseText: '' }, 'blocked', '');
}
} catch (e) {}
hideLoaders();
var $ = jq();
if ($) { try { $('#load').hide(); } catch (e) {} }
}, 0);
return stub;
}
function installAjaxHook() {
if (ajaxPatched) return true;
var $ = jq();
if (!$ || typeof $.ajax !== 'function') return false;
var orig = $.ajax;
var wrapped = function (url, options) {
var self = this;
var args = arguments;
var settings;
if (typeof url === 'string') settings = Object.assign({}, options, { url: url });
else settings = Object.assign({}, url);
var v = inspect(settings);
if (v && v.mode === 'block') {
var res = v.res;
hideLoaders();
if (CFG.locate) locateBadMarks(res.badIndexes);
showBlockDialog(res, function () {
// 强制保存:直接走原始 $.ajax,绕开本脚本的校验
orig.apply(self, args);
});
return makeStub(settings, v);
}
if (v && v.mode === 'warn') {
if (CFG.locate) locateBadMarks(v.res.badIndexes);
toast('⚠ 自动保存:' + v.res.issues[0].text, 5000);
}
return orig.apply(this, arguments);
};
$.ajax = wrapped;
ajaxPatched = true;
return true;
}
// 等 jQuery 出现(本页 jQuery 在 body 末尾、bundle 之前加载)
var hookTimer = setInterval(function () {
if (installAjaxHook()) {
clearInterval(hookTimer);
hookTimer = null;
tick(true);
}
}, 50);
setTimeout(function () {
if (hookTimer) { clearInterval(hookTimer); hookTimer = null; }
installAjaxHook();
}, 30000);
// ────────────────────────────────────────────────────────────────────────────
// 7. 启动
// ────────────────────────────────────────────────────────────────────────────
function boot() {
if (host) return;
if (!document.body) return; // 等 body 出现
highlightCss();
buildPanel();
tick(true);
setInterval(function () {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(function () { rafPending = false; tick(false); });
}, 450);
}
var bootTimer = setInterval(function () {
boot();
if (host) { clearInterval(bootTimer); bootTimer = null; }
}, 60);
setTimeout(function () {
if (bootTimer) { clearInterval(bootTimer); bootTimer = null; }
boot();
}, 20000);
// 供人工排查
window.__lh6v = {
version: '1.0.1',
cfg: CFG,
classes: CLASSES.slice(),
validate: validate,
pnameOf: pnameOf,
readLiveMarks: readLiveMarks,
tick: function () { return tick(true); }
};
})();