F12打开开发者模式,上方栏目选择控制台,输入需要进行的脚本。
注意备份!!!
返修助手(被制裁)
AI配置教程在同栏目中底部商品页面,请自行获取
质检相关
3.0
// ==UserScript==
// @name 极速自动领取·佑(可收缩小球+行选择)
// @namespace https://github.com/yournamespace
// @version 6.0
// @description 紧凑卡片,运行时收缩为动画小球,自定义领取行,后台极速,防伪通知
// @author 佑
// @match https://labelx.alibaba-inc.com/corpora/labeling/labelingTask?projectId=1023
// @match https://labelx.alibaba-inc.com/corpora/labeling/checkTask?projectId=1023
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// 网址校验
const TARGET_PATTERN = /^https:\/\/labelx\.alibaba-inc\.com\/corpora\/labeling\/(labelingTask|checkTask)\?projectId=1023/;
if (!TARGET_PATTERN.test(location.href)) return;
// ========== 配置 ==========
const CLICK_INTERVAL_MS = 80;
const SUCCESS_KEYWORDS = ['领取成功', '成功领取', '已领取'];
const EXCLUDE_KEYWORDS = ['分人员', '多人', '批量', '团队'];
// ==========================
// 隐蔽防伪数据
const _0x4f = [0x4f51];
const _0x62 = ['NWwyYzZJQ0Y3N3lhNUx5Wg=='];
const AUTHOR = String.fromCharCode(_0x4f[0]);
const ENCRYPTED_AUTHOR = atob(_0x62[0]);
const QUOTES = [
'✨ 此即,智慧之殿堂!—— 纳西妲',
'🍖 牛福永恒,吃饱喝好!',
'⚡ 稻光,亦是永恒!—— 雷电将军',
'🎲 愿风神忽悠你!—— 温迪',
'🌊 牛福如水,福泽绵长。',
'🔥 天动万象!—— 钟离',
'🍜 牛福面馆,今日特供。',
'🌙 月光点亮的时候,牛福会保佑你。'
];
// 状态变量
let successCount = 0;
let consecutiveSuccess = 0;
let attemptCount = 0;
let isRunning = false;
let worker = null;
let fallbackInterval = null;
let observer = null;
let processedMessages = new WeakSet();
let selectedRow = 1; // 默认领取第1行
// UI 元素
let mainCard = null;
let ballView = null; // 收缩后的小球
let successDisplay = null;
let comboDisplay = null;
let attemptDisplay = null;
let statusDot = null;
let statusText = null;
let rowSelect = null;
// ---------- 智能行识别 ----------
function getClaimButtonsByRow() {
const selectors = [
'button', '[role="button"]', '.ant-v5-btn', '.ant-btn', '.el-button',
'[class*="领取"]', '[class*="claim"]'
];
const all = document.querySelectorAll(selectors.join(','));
const validButtons = [];
const seen = new Set();
for (const el of all) {
if (seen.has(el)) continue;
seen.add(el);
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) continue;
if (el.disabled === true) continue;
if (el.classList.contains('disabled')) continue;
if (getComputedStyle(el).pointerEvents === 'none') continue;
const text = (el.innerText || el.textContent || '').trim();
if (!text.includes('领取')) continue;
if (EXCLUDE_KEYWORDS.some(kw => text.includes(kw))) continue;
validButtons.push({ el, text, y: rect.y });
}
if (validButtons.length === 0) return [];
// 按 Y 坐标排序
validButtons.sort((a, b) => a.y - b.y);
// 分行(容差 10px)
const rows = [];
let currentRow = [];
let lastY = validButtons[0].y;
for (const btn of validButtons) {
if (Math.abs(btn.y - lastY) <= 10) {
currentRow.push(btn);
} else {
if (currentRow.length) rows.push(currentRow);
currentRow = [btn];
lastY = btn.y;
}
}
if (currentRow.length) rows.push(currentRow);
return rows;
}
function clickReceive() {
const rows = getClaimButtonsByRow();
if (rows.length === 0) return;
// 根据选择的行号获取按钮(索引从0开始)
const rowIndex = Math.min(selectedRow - 1, rows.length - 1);
const targetRow = rows[rowIndex];
if (!targetRow || targetRow.length === 0) return;
// 取该行第一个按钮
const btn = targetRow[0].el;
try {
btn.click();
attemptCount++;
if (attemptDisplay) attemptDisplay.textContent = attemptCount;
} catch (e) {}
}
// ---------- 轻量成功通知 ----------
function showLightNotification() {
const old = document.getElementById('light-claim-notify');
if (old) old.remove();
const quote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
const notify = document.createElement('div');
notify.id = 'light-claim-notify';
notify.style.cssText = `
position: fixed; top: 20px; right: 20px; z-index: 2147483647;
background: #1e2436; color: #f0e6d2;
border-left: 5px solid #d4af37;
border-radius: 10px; padding: 10px 14px;
font-family: system-ui, sans-serif; font-size: 13px;
box-shadow: 0 8px 20px rgba(0,0,0,0.5);
animation: slideDown 0.3s ease-out;
pointer-events: none; max-width: 260px;
`;
notify.innerHTML = `
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
<span>👤 ${AUTHOR}</span>
<span style="font-size:11px; opacity:0.7; margin-left:auto;">防伪</span>
</div>
<div style="font-family:monospace; font-size:11px; opacity:0.9; word-break:break-all; margin-bottom:6px;">
🔐 ${ENCRYPTED_AUTHOR}
</div>
<div style="font-style:italic; color:#ffd966;">
“${quote}”
</div>
`;
if (!document.querySelector('#notify-style')) {
const style = document.createElement('style');
style.id = 'notify-style';
style.textContent = `@keyframes slideDown{from{opacity:0;transform:translateY(-30px);}to{opacity:1;transform:translateY(0);}}`;
document.head.appendChild(style);
}
document.body.appendChild(notify);
setTimeout(() => { if (notify.parentNode) notify.remove(); }, 5000);
}
// ---------- 成功监听 ----------
function setupObserver() {
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue;
const msg = node.classList?.contains('ant-message-notice')
? node
: node.querySelector?.('.ant-message-notice');
if (msg && !processedMessages.has(msg)) {
processedMessages.add(msg);
const txt = msg.innerText || '';
if (SUCCESS_KEYWORDS.some(k => txt.includes(k))) {
successCount++;
consecutiveSuccess++;
updateStats();
showLightNotification();
console.log(`✅ 成功 ${successCount} 连击 ${consecutiveSuccess}`);
}
}
}
}
});
obs.observe(document.body, { childList: true, subtree: true });
return obs;
}
// ---------- 降级定时器 ----------
function startFallback() {
if (fallbackInterval) clearInterval(fallbackInterval);
fallbackInterval = setInterval(() => {
try { clickReceive(); } catch(e) {}
}, CLICK_INTERVAL_MS);
console.log('⚠️ 使用降级 setInterval 模式');
}
function stopFallback() {
if (fallbackInterval) {
clearInterval(fallbackInterval);
fallbackInterval = null;
}
}
// ---------- Worker 驱动 ----------
function startWorker() {
const workerCode = `
let intervalId = null;
self.onmessage = function(e) {
if (e.data === 'start') {
if (intervalId !== null) return;
intervalId = setInterval(() => postMessage('click'), ${CLICK_INTERVAL_MS});
} else if (e.data === 'stop') {
if (intervalId !== null) {
clearInterval(intervalId);
intervalId = null;
}
}
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
if (e.data === 'click') {
try { clickReceive(); } catch(e) {}
}
};
worker.onerror = (err) => {
console.error('❌ Worker 错误,切换降级', err);
stopWorker();
startFallback();
};
worker.postMessage('start');
console.log('🚀 Worker 模式启动');
}
function stopWorker() {
if (worker) {
worker.postMessage('stop');
worker.terminate();
worker = null;
}
}
// ---------- 启动/停止 ----------
function start() {
if (isRunning) return;
if (!observer) observer = setupObserver();
isRunning = true;
consecutiveSuccess = 0;
attemptCount = 0;
updateStats();
setCardCollapsed(true); // 收缩为小球
try {
startWorker();
} catch (e) {
console.warn('⚠️ Worker 创建失败,使用降级', e);
startFallback();
}
console.log(`▶️ 启动 | 行 ${selectedRow} | 间隔 ${CLICK_INTERVAL_MS}ms`);
}
function stop() {
if (!isRunning) return;
stopWorker();
stopFallback();
isRunning = false;
setCardCollapsed(false); // 展开卡片
console.log(`⏹️ 停止 | 总成功 ${successCount} | 尝试 ${attemptCount}`);
}
function toggle() {
isRunning ? stop() : start();
}
// 小球点击时也切换停止/启动
function ballClickHandler() {
stop(); // 停止并展开
}
// 卡片展开/收缩切换
function setCardCollapsed(collapsed) {
if (!mainCard || !ballView) return;
if (collapsed) {
mainCard.style.display = 'none';
ballView.style.display = 'flex';
updateBallStats();
} else {
mainCard.style.display = 'block';
ballView.style.display = 'none';
updateStats();
}
}
function updateStats() {
if (successDisplay) successDisplay.textContent = successCount;
if (comboDisplay) comboDisplay.textContent = consecutiveSuccess;
if (attemptDisplay) attemptDisplay.textContent = attemptCount;
// 同时更新小球内数字
updateBallStats();
}
function updateBallStats() {
if (!ballView) return;
const ballSuccess = ballView.querySelector('#ball-success');
const ballCombo = ballView.querySelector('#ball-combo');
if (ballSuccess) ballSuccess.textContent = successCount;
if (ballCombo) ballCombo.textContent = consecutiveSuccess;
}
// ---------- 创建 UI:紧凑卡片 + 小球 ----------
function createUI() {
// 主卡片(紧凑版)
const card = document.createElement('div');
card.id = 'claim-master-card';
card.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 2147483646;
width: 250px; background: #1a2332; color: #f0e6d2;
border-radius: 20px; padding: 16px 14px;
font-family: system-ui, 'Microsoft YaHei', sans-serif;
box-shadow: 0 10px 25px rgba(0,0,0,0.6), 0 0 0 2px #d4af37;
cursor: pointer; user-select: none;
transition: all 0.25s;
border: 1px solid rgba(212,175,55,0.5);
`;
card.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<div style="background: #d4af37; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #1a1e2a; font-weight: bold; font-size: 18px;">👤</div>
<div style="flex:1;">
<div style="font-weight:700; font-size:15px;">佑·牛福</div>
<div style="font-size:10px; opacity:0.7; display:flex; align-items:center; gap:4px;">
<span>防伪</span>
<span style="background:#2a2f42; padding:2px 6px; border-radius:10px; font-family:monospace;">🔐 ${ENCRYPTED_AUTHOR.slice(0,8)}…</span>
</div>
</div>
</div>
<!-- 行选择下拉 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; background: rgba(0,0,0,0.2); border-radius: 12px; padding: 6px 10px;">
<span style="font-size:12px; opacity:0.8;">领取行</span>
<select id="row-selector" style="background: #2a2f42; color: #f0e6d2; border: 1px solid #d4af37; border-radius: 8px; padding: 4px 8px; font-size: 13px; cursor: pointer;">
<option value="1" selected>第 1 行</option>
<option value="2">第 2 行</option>
<option value="3">第 3 行</option>
</select>
</div>
<!-- 统计区域 -->
<div style="display:flex; justify-content:space-around; background:rgba(0,0,0,0.2); border-radius:14px; padding:10px 4px; margin-bottom:12px;">
<div style="text-align:center;">
<div style="font-size:11px; opacity:0.7;">成功</div>
<div id="master-success-count" style="font-size:24px; font-weight:bold; color:#ffd966;">0</div>
</div>
<div style="width:1px; background:rgba(255,255,255,0.15);"></div>
<div style="text-align:center;">
<div style="font-size:11px; opacity:0.7;">连击</div>
<div id="master-combo-count" style="font-size:24px; font-weight:bold; color:#ff6ec7;">0</div>
</div>
<div style="width:1px; background:rgba(255,255,255,0.15);"></div>
<div style="text-align:center;">
<div style="font-size:11px; opacity:0.7;">尝试</div>
<div id="master-attempt-count" style="font-size:24px; font-weight:bold; color:#4ecdc4;">0</div>
</div>
</div>
<!-- 状态栏 -->
<div style="display:flex; align-items:center; justify-content:space-between;">
<div style="display:flex; align-items:center; gap:5px;">
<span id="master-status-dot" style="width:10px; height:10px; border-radius:50%; background:#95a5a6;"></span>
<span id="master-status-text" style="font-size:13px;">待命中</span>
</div>
<div style="font-size:18px;">⚡</div>
</div>
<div style="margin-top:10px; font-size:10px; text-align:center; opacity:0.5;">
点击卡片开始/停止
</div>
`;
// 悬浮小球(运行时显示)
const ball = document.createElement('div');
ball.id = 'claim-floating-ball';
ball.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
width: 60px; height: 60px; background: #1a2332;
border-radius: 50%; display: none;
align-items: center; justify-content: center;
box-shadow: 0 8px 20px rgba(0,0,0,0.6), 0 0 0 2px #d4af37;
cursor: pointer; user-select: none;
animation: pulseGlow 1.8s infinite ease-in-out;
border: 1px solid rgba(212,175,55,0.6);
color: #f0e6d2;
`;
ball.innerHTML = `
<div style="text-align: center;">
<div style="font-size: 18px; font-weight: bold; color: #ffd966;" id="ball-success">0</div>
<div style="font-size: 10px; opacity: 0.7;">连击 <span id="ball-combo">0</span></div>
<div style="font-size: 14px; margin-top: 2px;">⚡</div>
</div>
`;
// 动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes pulseGlow {
0% { box-shadow: 0 8px 20px rgba(0,0,0,0.6), 0 0 0 2px #d4af37; transform: scale(1); }
50% { box-shadow: 0 8px 30px rgba(212,175,55,0.6), 0 0 0 4px rgba(212,175,55,0.3); transform: scale(1.05); }
100% { box-shadow: 0 8px 20px rgba(0,0,0,0.6), 0 0 0 2px #d4af37; transform: scale(1); }
}
`;
document.head.appendChild(style);
document.body.appendChild(card);
document.body.appendChild(ball);
// 保存引用
mainCard = card;
ballView = ball;
successDisplay = card.querySelector('#master-success-count');
comboDisplay = card.querySelector('#master-combo-count');
attemptDisplay = card.querySelector('#master-attempt-count');
statusDot = card.querySelector('#master-status-dot');
statusText = card.querySelector('#master-status-text');
rowSelect = card.querySelector('#row-selector');
// 事件绑定
card.addEventListener('click', (e) => {
// 如果点击的是下拉框,不触发卡片切换
if (e.target.tagName === 'SELECT' || e.target.tagName === 'OPTION') return;
toggle();
});
ball.addEventListener('click', ballClickHandler);
rowSelect.addEventListener('change', (e) => {
selectedRow = parseInt(e.target.value, 10);
console.log(`📌 切换领取行:第 ${selectedRow} 行`);
});
rowSelect.addEventListener('click', (e) => e.stopPropagation()); // 防止触发展开
// 悬停效果(卡片)
card.addEventListener('mouseenter', () => {
if (!isRunning) {
card.style.transform = 'scale(1.02)';
card.style.boxShadow = '0 14px 30px rgba(0,0,0,0.7), 0 0 0 2px #d4af37';
}
});
card.addEventListener('mouseleave', () => {
card.style.transform = 'scale(1)';
card.style.boxShadow = '0 10px 25px rgba(0,0,0,0.6), 0 0 0 2px #d4af37';
});
}
// 更新卡片运行状态样式(小球已处理,这里只更新卡片内文字)
function updateCardState(running) {
if (!statusDot || !statusText) return;
if (running) {
statusText.textContent = '抢领中';
statusDot.style.background = '#2ecc71';
statusDot.style.boxShadow = '0 0 10px #2ecc71';
} else {
statusText.textContent = '待命中';
statusDot.style.background = '#95a5a6';
statusDot.style.boxShadow = 'none';
}
}
// 覆盖 setCardCollapsed,同时更新状态文字
const originalSetCollapsed = setCardCollapsed;
setCardCollapsed = function(collapsed) {
originalSetCollapsed(collapsed);
updateCardState(isRunning);
};
// ---------- 初始化 ----------
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
createUI();
console.log(`✅ 脚本已加载 | 当前页:${location.pathname.includes('checkTask') ? '质检' : '标注'}`);
});
} else {
createUI();
console.log(`✅ 脚本已加载 | 当前页:${location.pathname.includes('checkTask') ? '质检' : '标注'}`);
}
}
init();
// 暴露控制接口
window.quickClaim = { start, stop, toggle };
})();
借入篡改猴,悬新增浮框,后台自动
2.0
(function() {
const CLICK_INTERVAL_MS = 80;
const SUCCESS_KEYWORDS = ['领取成功', '成功领取', '已领取'];
const _0x4f = [0x4f51];
const _0x62 = ['NWwyYzZJQ0Y3N3lhNUx5Wg=='];
const AUTHOR = String.fromCharCode(_0x4f[0]);
const ENCRYPTED_AUTHOR = atob(_0x62[0]);
const QUOTES = [
'✨ 此即,智慧之殿堂!—— 纳西妲',
'🍖 牛福永恒,吃饱喝好!',
'⚡ 稻光,亦是永恒!—— 雷电将军',
'🎲 愿风神忽悠你!—— 温迪',
'🌊 牛福如水,福泽绵长。',
'🔥 天动万象!—— 钟离',
'🍜 牛福面馆,今日特供。',
'🌙 月光点亮的时候,牛福会保佑你。'
];
let successCount = 0;
let intervalId = null;
let isRunning = false;
let processedMessages = new WeakSet();
let lastModalTime = 0;
const MODAL_COOLDOWN_MS = 2000;
function showAntiFakeModal() {
const now = Date.now();
if (now - lastModalTime < MODAL_COOLDOWN_MS) return;
lastModalTime = now;
const oldModal = document.getElementById('anti-fake-modal');
if (oldModal) oldModal.remove();
const randomQuote = QUOTES[Math.floor(Math.random() * QUOTES.length)];
const modal = document.createElement('div');
modal.id = 'anti-fake-modal';
modal.style.cssText = `
position: fixed; bottom: 90px; right: 20px; z-index: 1000000;
background: rgba(20, 25, 40, 0.9); color: #f0e6d2;
border: 1px solid #c9aa7b; border-radius: 16px;
padding: 16px 20px; font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
box-shadow: 0 8px 24px rgba(0,0,0,0.5); backdrop-filter: blur(6px);
min-width: 260px; pointer-events: none;
animation: fadeSlideIn 0.3s ease-out;
`;
modal.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
<span style="font-size: 22px;">👤</span>
<span style="font-weight: bold; letter-spacing: 1px;">作者:${AUTHOR}</span>
<span style="margin-left: auto; font-size: 12px; opacity: 0.7;">防伪认证</span>
</div>
<div style="background: #2a2f42; border-radius: 8px; padding: 8px 12px; margin-bottom: 12px; font-family: monospace; font-size: 13px; border-left: 4px solid #d4af37;">
🔐 ${ENCRYPTED_AUTHOR}
</div>
<div style="font-style: italic; color: #ffd966; text-align: right; border-top: 1px dashed #5a5f72; padding-top: 10px;">
“${randomQuote}”
</div>
<div style="margin-top: 6px; font-size: 12px; text-align: right; opacity: 0.6;">
🎉 累计成功 ${successCount} 次
</div>
`;
const style = document.createElement('style');
style.textContent = `
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
`;
if (!document.querySelector('#anti-fake-style')) {
style.id = 'anti-fake-style';
document.head.appendChild(style);
}
document.body.appendChild(modal);
setTimeout(() => { if (modal.parentNode) modal.remove(); }, 6000);
}
function clickReceive() {
const btns = document.querySelectorAll('button');
for (let btn of btns) {
const text = (btn.innerText || btn.textContent || '').trim();
if (text === '领取' || text.includes('领取')) {
btn.click();
return;
}
}
}
function setupObserver() {
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
const msg = node.classList?.contains('ant-message-notice')
? node
: node.querySelector?.('.ant-message-notice');
if (msg && !processedMessages.has(msg)) {
processedMessages.add(msg);
const txt = msg.innerText || '';
if (SUCCESS_KEYWORDS.some(k => txt.includes(k))) {
successCount++;
console.log(`✅ 领取成功!累计成功: ${successCount} 次`);
showAntiFakeModal();
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
}
let observer = null;
function start() {
if (intervalId) return;
if (!observer) observer = setupObserver();
isRunning = true;
intervalId = setInterval(() => {
try { clickReceive(); } catch(e) {}
}, CLICK_INTERVAL_MS);
console.log(`🚀 极速自动领取已启动,间隔 ${CLICK_INTERVAL_MS} ms`);
updateButton(true);
}
function stop() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
isRunning = false;
console.log(`🛑 已停止,累计成功领取 ${successCount} 次`);
updateButton(false);
}
function toggle() { isRunning ? stop() : start(); }
const btn = document.createElement('div');
btn.id = 'quick-claim';
btn.innerText = '▶️ 开始抢领';
btn.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 999999;
padding: 10px 16px; border-radius: 30px; background: #2ecc71;
color: #fff; font-size: 14px; font-weight: bold; text-align: center;
cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-family: sans-serif; border: none; user-select: none;
`;
btn.onclick = toggle;
document.body.appendChild(btn);
function updateButton(running) {
btn.innerText = running ? '⏹️ 停止抢领' : '▶️ 开始抢领';
btn.style.backgroundColor = running ? '#e74c3c' : '#2ecc71';
}
window.quickClaim = { start, stop, toggle };
console.log('✅ 极速抢领脚本已加载 (80ms/次)');
})();
新增按钮,随启随用。
新增彩蛋。优化逻辑
1.0
(function() {
let intervalId = setInterval(function() {
let receiveButtons = Array.from(document.querySelectorAll('button, a, [role="button"]')).filter(btn => btn.innerText.includes('领取') && !btn.disabled);
if (receiveButtons.length) {
receiveButtons.forEach(btn => {
console.log('点击领取按钮:', btn);
btn.click();
});
} else {
console.log('未找到可领取的按钮');
}
}, 3000);
// 可以提供一个停止函数
window.stopAutoClick = function() { clearInterval(intervalId); console.log('停止自动点击'); };
console.log('已启动自动点击领取,每3秒一次,调用 window.stopAutoClick() 可停止');
})();
最初版本,最稳定
标注相关
6.0
// ==UserScript==
// @name LabelX 智能标注助手
// @namespace http://tampermonkey.net/
// @version 55.0
// @description 优化进度更新,支持同步后端设置
// @author 定制版
// @match *://labelx.alibaba-inc.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @connect 111.228.5.19
// ==/UserScript==
(async function() {
'use strict';
// ---------- 配置 ----------
const API = 'http://111.228.5.19:7792/api.php'; // 请修改为实际地址
const STORAGE_TOKEN = 'anno_token';
const STORAGE_USER = 'anno_username';
const STORAGE_RULES = 'labelx_custom_rules';
const STORAGE_SETTINGS = 'labelx_settings';
// ---------- 全局状态 ----------
let backendOk = false, loggedIn = false, user = null, isAdmin = false;
let token = localStorage.getItem(STORAGE_TOKEN) || '';
let savedUser = localStorage.getItem(STORAGE_USER) || '';
let run = false, pause = false, stop = false, idx = 0, total = 0, items = [];
let CFG = { dItem: 120, dFill: 70, dClick: 70 };
let Settings = {
localDelayItem: 20, localDelayFill: 15, localDelayClick: 15,
autoSelectValid: true, autoClearOnInvalid: true, autoSubmit: false
};
// ---------- 默认规则 ----------
const DEFAULT_RULES = {
protectWords: [
// 成语典故
"一心一意","三心二意","七上八下","九牛一毛","一心二用","一模一样","一举一动",
"一五一十","一朝一夕","一步登天","一步一趋","一清二楚","一传十","一代宗师","一年一度",
"十年寒窗","七年之痒","四书五经","五十弦翻塞外声","一千零一夜","海底两万里",
"八百里分麾下炙","十之八九","六六大顺","九九归一","天下第一关","十三朝古都",
"一瘸一拐","一草一木","一针一线","一颦一笑","一生一世","一龙一猪","一张一弛",
"一唱一和","一板一眼","三心二意朝三暮四","一心不能二用","一步一步","一草一木",
// 历史地名专有名词
"明十三陵","九寨沟","三里屯","万里长城","三国演义","七里香","五月天",
"三只松鼠","零一万物","五八同城","文一路","二环路","三全路",
// 诗词引用
"两个黄鹂鸣翠柳","一行白鹭上青天","两个黄鹂鸣翠柳",
// 口语"一/两/三"开头
"一下","一下下","一点点","一些","一直","一定","一起","一般","一会儿","一同","一方面",
"一块儿","一块两块","两块","两毛","两次","两边","两三","两三天","两三个",
"三下","三四个","三五个",
// 特定词
"零花钱","零配件","零碎","零售","零度","零分","零基础",
// 不一定类
"不一定","不一定吧","不一定啊","不一定对","不一定行",
// 序号
"第一个","第一个是","第一","第二","第三个","第四个","第五个",
// 叠词
"一个个","一张张","一条条","一遍遍","一次次",
// 虚指泛指
"一两句","说两句","玩两把","有两下","有一点","有一点湿","有一点好",
"有一点热","有一点大","有一点小","一男子","两个闲钱",
"一到路上","一气之下","玩两天","过两天再说","晚一点",
// 星期/农历
"星期三","星期二","星期四","星期五","星期六","礼拜五","礼拜天","礼拜一","礼拜六",
"正月初八","腊月初八","八月十五","端午","中秋","正月","腊月","初一","十五",
// 特定量词组(半字开头)
"半盖","半片","半拉","半边天","半天","半辈子","半路","半推半就","一人一半","半生半熟",
// 修饰性描述
"有一天下午","一天清晨","有一年夏天","他一个人生活","两个世界的人","两颗星",
// 语气程度
"百分之百支持","百分之一万没问题","放一百个心","精神上百分之百支持",
// 历史年号/节日
"十月革命","洪武十三年","万历十五年","七七事变","十一黄金周","五一劳动节",
// 其他专有名词
"英语四级","科目三考试","二次元","三位一体","三维空间","六度空间理论","第一学历",
"四大天王","四大发明","午时三刻","一年三熟","零一万物"
],
asrFix: {
"0售": "零售",
"三点零五": "3.05",
"三点二五": "3.25",
"三点零二": "3.02",
"五点七": "5.7",
"九点三": "9.3",
"六点八": "6.8",
"八点五": "8.5"
},
strongAIPatterns: [
"您拨(叫|打)的(用户|电话|号码)","通话移转|呼叫转移|无法接听|暂时无法接通","请在提示音后|滴声后.*留言|录制.*留言|语音留言",
"重录请按|重新录制|按.*重录","退订回T|回复TD退订","智能语音助手|语音助理|智能助理","挂断即可|完成后挂断|转至|尝试联系的用户",
"请核对小票以及货品避免取错","高德地图持续|高德地图导航","小爱同学|小爱助理","天猫精灵|叮咚叮咚",
"对不起您拨的号码不存在|号码不存在","远声场|背景.*人声|难以.*听清","多人.*重叠|同时.*说话"
],
invalidKeywords: [
"您拨叫的用户正忙","您拨打的电话正在通话中","通话移转","您拨打的电话已关机","您拨打的电话暂时无法接通",
"您拨打的电话已停机","来电提醒","正在通话中","已转接","呼转","呼叫","受限","阅权","请勿",
"您呼叫的用户","呼叫转移","无法接通","无法接听","请在提示音后录制留言","提示音后留言",
"录制留言","语音留言","留言录音","提示音","还呗来电","环呗来电","安逸包来电","爱医宝来电","安逸花来电",
"重录请按","智能助理","语音助理","机主","本机机主","机主声明",
"小爱同学","小爱助理","天猫精灵","叮咚叮咚",
"高德地图持续为您导航","高德地图导航开始",
"对不起您拨的号码不存在","号码不存在","快已转至语音留言",
"远声场难以听清","背景音乐太吵","背景人声嘈杂","多人同时说话听不清",
"纯音乐没有人声","全是伴奏听不到词","这是首歌","这首歌是.*演唱"
],
digitMap: { "零":"0","一":"1","二":"2","三":"3","四":"4","五":"5","六":"6","七":"7","八":"8","九":"9","两":"2","幺":"1" },
unitMap: { "十":10, "百":100, "千":1000, "万":10000, "亿":100000000 },
quantifierList: ["个","只","条","张","本","支","块","次","趟","遍","顿","件","台","辆","部","首","句","家","所","间","座","扇","页","章","节","岁","年","月","天","小时","分钟","秒钟","元","角","分","笔","单"],
conversionPatterns: {"money_units":["块","毛","分"],"date_units":["年","月","日","号","点","分","秒"],"phone_replacements":{"杠":"-","点":"."},"pure_point":"点","range_separators":["到","至"],"percent_prefix":"百分之","fraction_separator":"分之","protected_digits":["一","二","两"],"dot_fix":"点","two_to_er":"二","exclude_digits":["一","二"],"punctuation_map":{".":"。",",":",","?":"?","!":"!",";":";",":":":","(":"(",")":")","<":"《",">":"》"}}
};
let RULES = JSON.parse(JSON.stringify(DEFAULT_RULES));
RULES.strongAIPatterns = RULES.strongAIPatterns.map(p => new RegExp(p));
// UI 元素
let st, bar, txt, btnS, btnT;
let curOrig = '', curTrans = '', curValid = true, curTA = null, curIdx = 0;
let isAutoFilling = false;
// ---------- 初始化设置和规则 ----------
function loadSettings() {
const saved = GM_getValue(STORAGE_SETTINGS);
if (saved) try { Object.assign(Settings, JSON.parse(saved)); } catch(e) {}
applyDelays();
}
function saveSettings() { GM_setValue(STORAGE_SETTINGS, JSON.stringify(Settings)); applyDelays(); }
function applyDelays() {
CFG.dItem = Settings.localDelayItem;
CFG.dFill = Settings.localDelayFill;
CFG.dClick = Settings.localDelayClick;
}
function loadRules() {
const saved = GM_getValue(STORAGE_RULES);
if (saved) {
try {
const parsed = JSON.parse(saved);
RULES = { ...DEFAULT_RULES, ...parsed };
RULES.strongAIPatterns = RULES.strongAIPatterns.map(p => new RegExp(p));
} catch(e) {}
}
}
function saveRulesToStorage() {
const toStore = JSON.parse(JSON.stringify(RULES));
toStore.strongAIPatterns = RULES.strongAIPatterns.map(p => p.source);
GM_setValue(STORAGE_RULES, JSON.stringify(toStore));
}
// ---------- 辅助函数 ----------
function escapeHtml(text) {
if (text === null || text === undefined) return '';
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
const canSave = () => backendOk && loggedIn && token;
const Q_KEY = 'anno_offline_queue';
const getQ = () => { try { return JSON.parse(localStorage.getItem(Q_KEY)||'[]'); } catch { return []; } };
const saveQ = (q) => { if (q.length>500) q=q.slice(-500); localStorage.setItem(Q_KEY, JSON.stringify(q)); };
const addToQ = (d) => { const q=getQ(); q.push(d); saveQ(q); };
async function flushQ() {
if (!canSave()) return;
const q=getQ(); if(!q.length) return;
const ok=[];
for(let i=0;i<q.length;i++) {
try {
await new Promise((res,rej)=>GM_xmlhttpRequest({
method:'POST', url:API+'?action=save', data:JSON.stringify(q[i]), headers:{'Content-Type':'application/json','X-Auth-Token':token},
onload: r=>{ try{ JSON.parse(r.responseText).success?res():rej(); }catch{rej();} }, onerror:rej
}));
ok.push(i);
}catch{break;}
}
if(ok.length) saveQ(q.filter((_,i)=>!ok.includes(i)));
}
async function saveToDB(orig, trans, valid) {
if(!canSave()) { addToQ({original_text:orig, transcribed_text:trans, is_valid:valid?1:0}); return; }
GM_xmlhttpRequest({ method:'POST', url:API+'?action=save', data:JSON.stringify({original_text:orig, transcribed_text:trans, is_valid:valid?1:0}), headers:{'Content-Type':'application/json','X-Auth-Token':token} });
}
// 配额缓存,减少网络请求
var cachedQuota = -1, lastQuotaCheck = 0;
async function checkQuota() {
if(!canSave()) return -1;
var now = Date.now();
// 缓存30秒内的配额结果
if (now - lastQuotaCheck < 30000 && cachedQuota >= 0) return cachedQuota;
return new Promise(function(resolve){
GM_xmlhttpRequest({ method:'GET', url:API+'?action=check_quota', headers:{'X-Auth-Token':token}, onload:function(r){
try{ var j=JSON.parse(r.responseText); cachedQuota = j.success?j.available:-1; lastQuotaCheck=Date.now(); resolve(cachedQuota); }
catch{ resolve(-1); }
}, onerror:function(){ resolve(-1); }});
});
}
function setTextarea(tx, v) {
if(!tx) return;
const d=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value');
const descriptorSet = d ? d.set : null;
if(descriptorSet) descriptorSet.call(tx,v); else tx.value=v;
tx.dispatchEvent(new Event('input',{bubbles:true}));
tx.dispatchEvent(new Event('change',{bubbles:true}));
tx.dispatchEvent(new Event('blur',{bubbles:true}));
}
// ---------- 中文数字解析(核心)- 符合ITN标注规范 ----------
function parseChineseNumber(s) {
let t = s.replace(/两/g, '二');
if (/^[一二]$/.test(t)) return null;
// 预处理:处理 "十" 在开头或单独成数的情况 (十=10, 十三=13)
if (t === '十') return 10;
if (/^十[零一二三四五六七八九]$/.test(t)) {
// 十三、十四等
const digit = RULES.digitMap[t[1]];
return 10 + parseInt(digit);
}
// 检查是否包含无效字符(非数字、非单位)
if (!/^[零一二三四五六七八九十百千万亿]+$/.test(t)) return null;
// 处理纯个位数字序列(如 "三七九九"),不应转换
if (/^[零一二三四五六七八九]+$/.test(t)) return null;
let result = 0, temp = 0, section = 0;
let lastWasUnit = false;
for (let i = 0; i < t.length; i++) {
const c = t[i];
const digit = RULES.digitMap[c];
if (digit !== undefined) {
const digitVal = parseInt(digit);
// 如果前一个字符是十,当前的数字是其组成部分(如十一=11,二十一=21)
if (lastWasUnit && t[i-1] === '十') {
section += digitVal; // 十=10, 加 1 = 11
temp = 0;
} else {
temp = digitVal;
if (i === t.length - 1) section += temp;
}
lastWasUnit = false;
} else if (RULES.unitMap[c]) {
const unit = RULES.unitMap[c];
if (unit >= 10000) {
section = (section + (temp || 1)) * unit;
result += section;
section = 0;
} else {
section += (temp || 1) * unit;
}
temp = 0;
lastWasUnit = true;
} else return null;
}
return result + section + temp;
}
// 获取带单位的数字表示(如:3亿5000万)
function getMixedRepresentation(numStr) {
const value = parseChineseNumber(numStr);
if (value === null) return null;
if (value >= 100000000) {
const yi = Math.floor(value / 100000000);
const remainder = value % 100000000;
if (remainder > 0) {
if (remainder >= 10000) {
const wan = Math.floor(remainder / 10000);
const rem = remainder % 10000;
if (rem > 0) return yi + '亿' + wan + '万' + rem;
else return yi + '亿' + wan + '万';
} else return yi + '亿' + remainder;
} else return yi + '亿';
} else if (value >= 10000) {
const wan = Math.floor(value / 10000);
const remainder = value % 10000;
if (remainder > 0) return wan + '万' + remainder;
else return wan + '万';
}
return value.toString();
}
function isApproximateNumber(str) {
// 概数不转换:几、多、来、余、相邻数字组合
if (/[几多来余]/.test(str)) return true;
// 相邻数字组成的概数:三两百、两三千、七八十万
if (/^[零一二三四五六七八九][零一二三四五六七八九][百千万亿]?$/.test(str)) return true;
// 十+几/多/来/余
if (/^十[几多来余]?$/.test(str)) return true;
// 初七八(农历日期)
if (/^初[零一二三四五六七八九]$/.test(str)) return true;
return false;
}
function localConvert(text) {
if (!text) return '';
let t = text;
// ========== 1. 保护词占位 ==========
const placeholders = [];
RULES.protectWords.sort((a,b)=>b.length-a.length).forEach((w,i)=>{
t = t.replace(new RegExp(w, 'g'), m=>{ const ph=`__P${i}__`; placeholders.push({ph, orig:m}); return ph; });
});
// ========== 2. ASR修正 ==========
for (const [w,c] of Object.entries(RULES.asrFix)) t = t.replace(new RegExp(w, 'g'), c);
// ========== 3.5 特殊编码(身份证/车牌/航班号/火车车次) ==========
// 身份证:尾号零八五八二叉 → 尾号08582X
t = t.replace(/(尾号)([零一二三四五六七八九十幺叉]+)/g, (m, prefix, num) => prefix + digitByDigit(num));
// 车牌:粤B五九幺六幺 → 粤B59161
t = t.replace(/([\u4e00-\u9fa5][A-Z])([零一二三四五六七八九十幺]+)/g, (m, plate, num) => plate + digitByDigit(num));
// 航班号:MU五七零二 → MU5702
t = t.replace(/(MU|CA|CZ|HU|9C|HO|BK|ZH|KN|UQ|JD|TV|GT|DG|MF|FU|KN)([零一二三四五六七八九十幺]+)/g, (m, code, num) => code + digitByDigit(num));
// 火车车次:高铁G一百零一 → 高铁G101,K八十八次 → K88次
t = t.replace(/(高铁?|动车?|普快?|K|T|Z|G|D|C|Y)[A-Z]?([零一二三四五六七八九十百千万]+)(次|号)?/g, (m, type, num, suffix) => {
const v = parseChineseNumber(num);
return type + (v !== null ? v : digitByDigit(num)) + (suffix || '');
});
// 二号线/幺四九路 → 2号线/149路(线路识别)
t = t.replace(/([零一二三四五六七八九十百千万亿]+)(号线|路|公交)/g, (m, num, suffix) => {
const v = parseChineseNumber(num);
return (v !== null ? v : digitByDigit(num)) + suffix;
});
// ========== 3. 通信号码(幺→1,叉→X,杠→-,点→.,加→+) ==========
// 逐位转换:幺→1,叉→X(末位)
function digitByDigit(str) {
var out = '';
for (var i = 0; i < str.length; i++) {
var ch = str[i];
if (ch === '幺') out += '1';
else if (ch === '叉' && i === str.length - 1) out += 'X';
else if (RULES.digitMap[ch] !== undefined) out += RULES.digitMap[ch];
else out += ch;
}
return out;
}
// 验证码+数字 → 验证码4029
t = t.replace(/(验证码?\s*)([零一二三四五六七八九十两幺叉]+)/g, (m, prefix, num) => {
if (/[百千万亿]/.test(num)) return m;
return prefix + digitByDigit(num);
});
// 固话区号杠号码 → 010-888转606
t = t.replace(/([零一二三]+杠[零一二三]+杠)([零一二三四五六七八九十两幺]+转?[零一二三四五六七八九十两幺]*)/g, (m, area, rest) => {
return digitByDigit(area) + digitByDigit(rest);
});
// IP地址:一九二点一六八点零点一 → 192.168.0.1
t = t.replace(/([零一二三四五六七八九十]+点)+([零一二三四五六七八九十]+)/g, (m) => digitByDigit(m));
// 国际区号:加八六幺三八 → +86138
t = t.replace(/加([零一二三四五六七八九十两幺]+)/g, (m, num) => '+' + digitByDigit(num));
// 纯电话号码(幺开头+数字序列)→ 15910036
t = t.replace(/幺[零一二三四五六七八九幺]+/g, m => digitByDigit(m));
// 固话区号-号码(杠连接)
t = t.replace(/(\d+)杠(\d+)/g, '$1-$2');
// IP地址的点
t = t.replace(/(\d+)点(\d+)/g, '$1.$2');
// 国际区号加号
t = t.replace(/加(\d+)/g, '+$1');
// 保留"转"(分机号)
// ========== 4. 分数:分之 → / ==========
// 三分之二 → 2/3,五分之四 → 4/5
t = t.replace(/([零一二三四五六七八九两十百千万亿]+)分之([零一二三四五六七八九两十百千万亿]+)/g, (m, denom, numer) => {
const d = parseChineseNumber(denom);
const n = parseChineseNumber(numer);
if (d !== null && n !== null && d !== 0) return n + '/' + d;
return m;
});
// ========== 5. 百分数:百分之 → % ==========
// 百分之二十 → 20%,百分之三十四点五八 → 34.58%
t = t.replace(/百分之([零一二三四五六七八九两十百千万亿\d.]+)/g, (m, num) => {
const v = parseChineseNumber(num);
if (v !== null) return v + '%';
// 如果包含小数点,尝试直接转换
if (/\d/.test(num)) return num + '%';
return m;
});
// ========== 6. 千分数:千分之 → ‰ ==========
t = t.replace(/千分之([零一二三四五六七八九两十百千万亿\d.]+)/g, (m, num) => {
const v = parseChineseNumber(num);
if (v !== null) return v + '‰';
return m;
});
// ========== 7. 比例:比 → : ==========
// 一比三 → 1:3,零点五比二 → 0.5:2
t = t.replace(/([零一二三四五六七八九两十百千万亿\d.]+)比([零一二三四五六七八九两十百千万亿\d.]+)/g, (m, a, b) => {
const va = parseChineseNumber(a);
const vb = parseChineseNumber(b);
if (va !== null && vb !== null) return va + ':' + vb;
return m;
});
// ========== 8. 日期转换 ==========
// 年份:零八/年 → 08/年,二零二四年 → 2024年
t = t.replace(/([零一二三四五六七八九]{4,})年/g, (m, y) => {
let out = '';
for (let c of y) out += RULES.digitMap[c] !== undefined ? RULES.digitMap[c] : c;
return out + '年';
});
// 月份:十月 → 10月,十月一号 → 10月1号
t = t.replace(/十([一二三四五六七八九])月/g, (m, d) => (10 + parseInt(RULES.digitMap[d])) + '月');
t = t.replace(/十月/g, '10月');
t = t.replace(/([零一二三四五六七八九])月/g, (m, d) => (RULES.digitMap[d] !== undefined ? RULES.digitMap[d] : d) + '月');
// 日号:支持十到九十九号
t = t.replace(/([零一二三四五六七八九十百千万亿]+)[号日](?![号日])/g, function(m, num) {
if (/^[十]$/.test(num)) return '10';
var v = parseChineseNumber(num);
return v !== null ? v : m;
});
// ========== 9. 时间转换 ==========
// 半点:两点半 → 2点半
t = t.replace(/([零一二三四五六七八九十百千万亿]+)点半/g, (m, h) => {
const hour = parseChineseNumber(h);
return (hour !== null ? hour : h) + '点半';
});
// 整点:下午两点 → 下午2点,晚上十二点 → 晚上12点,整点钟/整点
t = t.replace(/(?:上午?|下午?|早上?|晚上?|中午?|凌晨?|傍晚?|半夜?)(?:十)?([一二三四五六七八九])点(钟|整)?/g, (m, h, suffix) => {
return m.replace(h + '点' + (suffix||''), (RULES.digitMap[h] !== undefined ? RULES.digitMap[h] : h) + '点' + (suffix||''));
});
t = t.replace(/(?:上午?|下午?|早上?|晚上?|中午?|凌晨?|傍晚?|半夜?)十点(钟|整)?/g, m => {
return m.replace('十点' + (m.includes('点钟')?'钟':(m.includes('整')?'整':'')), '10点' + (m.includes('点钟')?'钟':(m.includes('整')?'整':'')));
});
t = t.replace(/(?:上午?|下午?|早上?|晚上?|中午?|凌晨?|傍晚?|半夜?)(?:二十|三十)点(钟|整)?/g, m => {
const hour = m.includes('二十') ? '20' : '30';
const suffix = m.includes('点钟') ? '点钟' : (m.includes('整') ? '整' : '');
return m.replace(/[二三]十点/, hour + '点').replace(/点钟|整/g, suffix);
});
// 一刻:九点一刻 → 9点一刻
t = t.replace(/([零一二三四五六七八九十百千万亿]+)点一刻/g, (m, h) => {
const hour = parseChineseNumber(h);
return (hour !== null ? hour : h) + '点一刻';
});
// 几点几分(不带量词)
t = t.replace(/([零一二三四五六七八九十]+)点([零一二三四五六七八九]+)分/g, (m, h, min) => {
const hour = parseChineseNumber(h);
const minute = parseChineseNumber(min);
return (hour !== null ? hour : h) + '点' + (minute !== null ? String(minute).padStart(2, '0') : min) + '分';
});
// 带小时/分钟/秒钟的
t = t.replace(/([零一二三四五六七八九十百千万亿]+)(个)?小时/g, (m, num, skip) => {
if (/^[一二]$/.test(num)) return m; // 一小时/二小时保留
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '小时';
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)(个)?分钟/g, (m, num, skip) => {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '分钟';
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)(个)?秒钟/g, (m, num, skip) => {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '秒钟';
});
// ========== 10. 数字范围(到/至) ==========
// 三到六个 → 3~6个(无量词时用~)
// 三个到六个 → 3个到6个(有量词保留到)
t = t.replace(/([零一二三四五六七八九十百千万亿]+)到([零一二三四五六七八九十百千万亿]+)/g, (m, a, b) => {
const va = getMixedRepresentation(a);
const vb = getMixedRepresentation(b);
if (va !== null && vb !== null) return va + '~' + vb;
return m;
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)至([零一二三四五六七八九十百千万亿]+)/g, (m, a, b) => {
const va = getMixedRepresentation(a);
const vb = getMixedRepresentation(b);
if (va !== null && vb !== null) return va + '~' + vb;
return m;
});
// ========== 11. 量词转换 ==========
const quantClass = RULES.quantifierList.join('');
// 一、二、两 + 量词 → 保留汉字
t = t.replace(new RegExp(`([一二两])([${quantClass}])`, 'g'), (m, num, unit) => num + unit);
// 概数+量词不转换(几十、几百等)
t = t.replace(new RegExp(`([几多成上打]+)([零一二三四五六七八九十百千万亿]*)([${quantClass}])`, 'g'), (m, prefix, num, unit) => prefix + num + unit);
// 三及以上 + 量词 → 转换为数字
t = t.replace(new RegExp(`([零三四五六七八九十百千万亿]+)([${quantClass}])`, 'g'), (m, num, unit) => {
// 概数不转换(几、多、成等开头的组合)
if (/^[几多成上打]/.test(num)) return num + unit;
// 十+量词(如:十来个)不转换
if (/^十[零一二三四五六七八九]?$/.test(num)) return num + unit;
// 万/亿级数字保持单位
if (/[万亿]/.test(num)) {
const mixed = getMixedRepresentation(num);
if (mixed !== null) return mixed + unit;
}
const v = parseChineseNumber(num);
return v !== null ? v + unit : m;
});
// ========== 12. 货币 ==========
// 五块六毛 → 5块6毛,三块五 → 3块5
t = t.replace(/([零一二三四五六七八九十百千万亿]+)块([零一二三四五六七八九]+)毛/g, (m, y, m2) => {
const yVal = parseChineseNumber(y);
const mVal = parseChineseNumber(m2);
return (yVal !== null ? yVal : y) + '块' + (mVal !== null ? mVal : m2) + '毛';
});
// 五块五 → 5块5,五块五毛五 → 5块5毛5
t = t.replace(/([零一二三四五六七八九十百千万亿]+)块零?([零一二三四五六七八九]+)/g, (m, y, f) => {
const yVal = parseChineseNumber(y);
const fVal = parseChineseNumber(f);
return (yVal !== null ? yVal : y) + '块' + (fVal !== null ? fVal : f);
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)块/g, (m, y) => {
const v = parseChineseNumber(y);
return v !== null ? v + '块' : m;
});
// ========== 13. 度量衡 ==========
// 保留单位,只转换数字
// 五千米 → 5千米,三十八度五 → 38度5
t = t.replace(/([零一二三四五六七八九十百千万亿\d.]+)(米|千米|厘米|公里|克|千克|毫克|升|毫升|毫升|吨|摄氏度|度|焦耳|千焦|瓦|千瓦|马力|赫兹|千赫|兆赫)/g, (m, num, unit) => {
if (/[零一二三四五六七八九十]/.test(num)) {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + unit;
}
return m;
});
// ========== 14. 小数 ==========
// 一点七五 → 1.75(带量词或单位时转换)
t = t.replace(/([零一二三四五六七八九十百千万亿]+)点([零一二三四五六七八九]+)/g, (m, int, dec) => {
// 整数部分:直接用 digitMap 转换(不用 parseChineseNumber,因为 "一" 会返回 null)
let intOut = '';
for (let c of int) intOut += RULES.digitMap[c] !== undefined ? RULES.digitMap[c] : c;
// 小数部分:逐位转换
let decOut = '';
for (let c of dec) decOut += RULES.digitMap[c] !== undefined ? RULES.digitMap[c] : c;
return intOut + '.' + decOut;
});
// ========== 15. 万/亿级大数字 ==========
// 三亿五千万 → 3亿5000万,一万二千 → 12000(完整转换)
t = t.replace(/([零一二三四五六七八九十百千万亿]+)亿([零一二三四五六七八九]+)?万?/g, (m, yi, rest) => {
const yiVal = parseChineseNumber(yi);
if (yiVal !== null) {
if (rest && parseChineseNumber(rest + '万')) {
// 三亿五千万 → 3亿5000万
return yiVal + '亿' + parseChineseNumber(rest + '万');
}
return yiVal + '亿';
}
return m;
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)万([零一二三四五六七八九]+)?/g, (m, wan, rest) => {
const wanVal = parseChineseNumber(wan);
if (wanVal !== null) {
if (rest) {
return wanVal + '万' + parseChineseNumber(rest);
}
return wanVal + '万';
}
return m;
});
// ========== 16.5 地址转换(路名保护,门牌楼栋转换) ==========
// 楼层:十八层 → 18层,负一层/地下层 → 负1层/地下1层
t = t.replace(/(负|地下)([零一二三四五六七八九十百千万亿]+)层/g, (m, prefix, num) => {
const v = parseChineseNumber(num);
return prefix + (v !== null ? v : num) + '层';
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)层/g, (m, num) => {
if (/^[一二]$/.test(num)) return m; // 一层保留汉字
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '层';
});
// 房间号:三零二室 → 302室,十八楼三零二室 → 18楼302室
t = t.replace(/([零一二三四五六七八九十]+)室/g, (m, num) => {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '室';
});
// 楼栋号:五号楼 → 5号楼,二单元 → 2单元
t = t.replace(/([零一二三四五六七八九十百千万亿]+)号楼/g, (m, num) => {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '号楼';
});
t = t.replace(/([零一二三四五六七八九十百千万亿]+)单元/g, (m, num) => {
const v = parseChineseNumber(num);
return (v !== null ? v : num) + '单元';
});
// 号牌:二十二号 → 22号,甲十一号 → 甲11号
t = t.replace(/(甲|乙|丙)?([零一二三四五六七八九十百千万亿]+)号/g, (m, prefix, num) => {
const v = parseChineseNumber(num);
return (prefix || '') + (v !== null ? v : num) + '号';
});
// 邮编:六五零零一一 → 650011
if (/\d/.test(t) === false) { // 只有中文数字时
t = t.replace(/([零一二三四五六七八九]{6})/g, m => digitByDigit(m));
t = t.replace(/([零一二三四五六七八九]{4})路/g, (m, num) => digitByDigit(num) + '路');
}
// ========== 16. 剩余中文数字整体转换 ==========
// 先处理概数词(几十、几百等)
t = t.replace(/[几多成上打]+[零一二三四五六七八九十百千万亿]*/g, m => m);
// 转换普通数字
t = t.replace(/[零一二三四五六七八九两十百千万亿]+/g, m => {
// 概数不转换
if (isApproximateNumber(m)) return m;
// 单独的一、二不转换(量词前会保留)
if (/^[一二]$/.test(m)) return m;
// 万/亿保持
if (/^[万亿]+$/.test(m)) return m;
const v = parseChineseNumber(m);
return v !== null ? v.toString() : m;
});
// ========== 17. 恢复保护词 ==========
placeholders.forEach(({ph, orig})=> t = t.replace(new RegExp(ph, 'g'), orig));
// ========== 18. 标点 ==========
t = t.trim().replace(/[\s\n\r]/g, '');
if (!/[。?!]$/.test(t)) {
if (/[吗谁什么哪怎么怎样多少几啥呢]|有没有|是不是|行不行|可不可以|能否|是否/.test(t)) t += '?';
else t += '。';
}
return t;
}
function isValidText(text) {
const trimmed = text.trim();
if (/^(你好|您好|喂|嗨|在吗|哈喽)[!!。??]*$/.test(trimmed)) return false;
for (let p of RULES.strongAIPatterns) if (p.test(text)) return false;
for (let kw of RULES.invalidKeywords) if (trimmed.includes(kw)) {
if (kw.length < 5) return false;
if (trimmed.replace(new RegExp(kw, 'g'), '').trim().length <= 5) return false;
}
const letters = text.match(/[a-zA-Z]/g) || [];
return letters.length / text.length <= 0.5;
}
// ---------- 条目处理 ----------
function extractItemData(item, index) {
let ta=null, rg=null;
for(let w of item.querySelectorAll('.labelRender-item-answer-wrap')) {
const ti=w.querySelector('.labelRender-item-answer-title');
if(!ti) continue;
if(ti.innerText.includes('转写文本')) ta=w.querySelector('textarea.ant-v5-input');
else if(ti.innerText.includes('是否有效')) rg=w;
}
if(!ta||!rg) return null;
const od=item.querySelector('.dt-text-container > div');
const orig=od?od.innerText.trim():'';
if(!orig) return null;
return { index, element:item, textarea:ta, radioGroup:rg, originalText:orig, hasExistingData: ta.value.trim() && rg.querySelector('input[type="radio"]:checked') };
}
// 处理单个条目
async function processItem(item) {
try {
if(stop) return false;
const orig = item.originalText;
const valid = isValidText(orig);
const target = valid ? localConvert(orig) : '';
isAutoFilling = true;
setTextarea(item.textarea, target);
await new Promise(r=>setTimeout(r, CFG.dFill));
if (Settings.autoSelectValid) {
const targetLabel = valid ? '有效' : '无效';
const labels = item.radioGroup.querySelectorAll('label.ant-v5-radio-wrapper');
let clicked = false;
for (let l of labels) {
const sp = l.querySelector('.ant-v5-radio-label');
if (sp && sp.innerText.trim() === targetLabel) {
if (!l.classList.contains('ant-v5-radio-wrapper-checked')) l.click();
await new Promise(r=>setTimeout(r, CFG.dClick));
clicked = true;
break;
}
}
// 备用方案:直接设置radio
if (!clicked) {
const radios = Array.from(item.radioGroup.querySelectorAll('input[type="radio"]'));
const radio = radios.find(r => r.value === targetLabel);
if (radio && !radio.checked) {
radio.checked = true;
radio.dispatchEvent(new Event('change',{bubbles:true}));
radio.dispatchEvent(new Event('click',{bubbles:true}));
await new Promise(r=>setTimeout(r, CFG.dClick));
}
}
}
isAutoFilling = false;
saveToDB(orig, target, valid);
return true;
} catch(e) {
console.error('处理条目失败:', e);
return true;
}
}
// ---------- UI 更新 ----------
function updUI() {
if(!st||!bar||!txt) return;
const pct = total ? (idx/total*100) : 0;
bar.style.width = pct+'%';
txt.textContent = idx + ' / ' + total;
// 状态文字(隐藏百分比)
const newStatus = !run ? '⚪ 待命中' : (pause ? '⏸ 已暂停' : '▶ 运行中');
if (st.textContent !== newStatus) st.textContent = newStatus;
const newBtnText = run ? (pause ? '▶ 继续' : '⏸ 暂停') : '▶ 开始';
if (btnS.textContent !== newBtnText) btnS.textContent = newBtnText;
}
function scrollToTop() {
// 使用 requestAnimationFrame 确保在下一帧执行
requestAnimationFrame(function() {
// 方法1: window 滚动
window.scrollTo(0, 0);
window.scrollTo({top: 0, left: 0, behavior: 'instant'});
// 方法2: document 元素
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
document.body.scrollTo(0, 0);
// 方法3: 滚动到第一个条目
var firstItem = document.querySelector('.labelRender-item');
if (firstItem) {
firstItem.scrollIntoView({block: 'start', behavior: 'instant'});
firstItem.scrollIntoView(true);
}
// 方法4: 尝试滚动容器(如果有)
var container = document.querySelector('.dt-text-container, .scroll-container, main, [role="main"]');
if (container && container.scrollTo) {
container.scrollTo(0, 0);
}
});
}
async function mainLoop() {
if(!canSave()) { alert('请先登录'); showLoginPanel(); return; }
if(await checkQuota()===0) { alert('配额已用完'); return; }
await flushQ();
run=true; pause=false; stop=false;
items=Array.from(document.querySelectorAll('.labelRender-item'));
total=items.length; idx=0;
injectFeedbackButtons();
// 预提取所有数据并计算转换结果,避免在循环中重复计算
const pending = [];
for (let i=0; i<total; i++) {
const d = extractItemData(items[i], i);
if (d && !d.hasExistingData) {
// 预计算转换结果
d.valid = isValidText(d.originalText);
d.target = d.valid ? localConvert(d.originalText) : '';
pending.push(d);
}
}
if (pending.length===0) { run=false; updUI(); return; }
// 主循环
for (let i=0; i<pending.length; i++) {
if (stop) break;
if (pause) { await new Promise(r=>setTimeout(r,100)); if(!pause) i--; continue; }
try {
await processItem(pending[i]);
} catch(e) {
console.error('处理失败:', e);
}
idx = i+1;
updUI();
if (CFG.dItem > 0) await new Promise(r=>setTimeout(r, CFG.dItem));
}
// 完成后滚动到顶部
run=pause=stop=false; updUI();
scrollToTop();
await flushQ();
}
function injectFeedbackButtons() {
// 优化:使用DocumentFragment批量注入,减少DOM回流
const existingBtns = document.querySelectorAll('.fb-btn');
const existingIdxs = new Set();
existingBtns.forEach(function(btn){
var idx = btn.dataset.idx;
if(idx) existingIdxs.add(parseInt(idx));
});
const fragment = document.createDocumentFragment();
document.querySelectorAll('.labelRender-item').forEach(function(item, i){
if(existingIdxs.has(i)) return;
item.style.position = 'relative';
var btn = document.createElement('button');
btn.className = 'fb-btn';
btn.dataset.idx = i;
btn.innerHTML = '📝';
btn.title = '反馈';
btn.style.cssText = 'position:absolute;top:6px;right:6px;z-index:100;padding:2px 8px;background:#1890ff;color:#fff;border:none;border-radius:12px;font-size:11px;cursor:pointer;';
btn.onclick = function(e){ e.stopPropagation(); showFeedbackPanel(i); };
fragment.appendChild(btn);
});
// 一次性添加到DOM
if (fragment.children.length > 0) {
const firstItem = document.querySelector('.labelRender-item');
if (firstItem) firstItem.parentNode.appendChild(fragment);
}
}
// ---------- 登录面板 ----------
function showLoginPanel() {
var oldPanel = document.getElementById('lb-panel');
if(oldPanel) oldPanel.remove();
oldPanel = document.getElementById('login-panel');
if(oldPanel) oldPanel.remove();
const panel=document.createElement('div'); panel.id='login-panel';
panel.innerHTML = `<style>#login-panel{position:fixed;top:10px;right:10px;z-index:99999;background:#fff;border-radius:20px;padding:24px;width:280px;box-shadow:0 15px 30px rgba(0,0,0,0.15);}</style>
<h3>🔐 登录 / 注册</h3><input id="login-username" placeholder="用户名" style="width:100%;padding:10px;margin-bottom:10px;border-radius:14px;border:1px solid #ccc;"><br>
<input id="login-password" type="password" placeholder="密码" style="width:100%;padding:10px;margin-bottom:10px;border-radius:14px;border:1px solid #ccc;"><br>
<div style="display:flex;gap:10px;"><button id="login-submit" style="flex:1;padding:10px;background:#3b82f6;color:#fff;border:none;border-radius:14px;">登录</button><button id="register-submit" style="flex:1;padding:10px;background:#fff;border:1px solid #3b82f6;color:#3b82f6;border-radius:14px;">注册</button></div>
<div id="login-error" style="color:red;margin-top:10px;"></div>`;
document.body.appendChild(panel);
document.getElementById('login-submit').onclick=async ()=>{
const u=document.getElementById('login-username').value.trim(), p=document.getElementById('login-password').value, err=document.getElementById('login-error');
if(!u||!p) { err.textContent='请输入用户名和密码'; return; }
GM_xmlhttpRequest({ method:'POST', url:API+'?action=login', data:JSON.stringify({username:u, password:p}), headers:{'Content-Type':'application/json'},
onload: r=>{ try{ const j=JSON.parse(r.responseText); if(j.success){ token=j.token; user={username:u}; localStorage.setItem(STORAGE_TOKEN,token); localStorage.setItem(STORAGE_USER,u); loggedIn=true; panel.remove(); showMainPanel(); } else err.textContent=j.message; }catch{ err.textContent='响应异常'; } },
onerror: ()=>err.textContent='网络错误' });
};
document.getElementById('register-submit').onclick=async ()=>{
const u=document.getElementById('login-username').value.trim(), p=document.getElementById('login-password').value, err=document.getElementById('login-error');
if(!u||!p) { err.textContent='请输入用户名和密码'; return; }
if(p.length<6) { err.textContent='密码至少6位'; return; }
GM_xmlhttpRequest({ method:'POST', url:API+'?action=register', data:JSON.stringify({username:u, password:p}), headers:{'Content-Type':'application/json'},
onload: r=>{ try{ const j=JSON.parse(r.responseText); err.style.color=j.success?'#10b981':'#ef4444'; err.textContent=j.message; }catch{ err.textContent='响应异常'; } },
onerror: ()=>{ err.style.color='#ef4444'; err.textContent='网络错误'; } });
};
}
// ---------- 自动同步服务器规则 ----------
async function syncRulesFromBackend() {
if (!backendOk) return;
try {
var resp = await new Promise(function(res){GM_xmlhttpRequest({
method:'GET',url:API+'?action=get_full_runtime_config',
onload:function(r){try{res(JSON.parse(r.responseText));}catch{res({success:false});}},
onerror:function(){res({success:false});}
});});
if (resp.success && resp.config) {
var cfg = resp.config;
RULES.protectWords = cfg.protect_words || RULES.protectWords;
RULES.asrFix = cfg.asr_fix || RULES.asrFix;
RULES.strongAIPatterns = (cfg.strong_ai_patterns || []).map(function(p){return new RegExp(p);});
RULES.invalidKeywords = cfg.invalid_keywords || RULES.invalidKeywords;
// 同步速度设置
if (cfg.delay_item) Settings.localDelayItem = cfg.delay_item;
if (cfg.delay_fill) Settings.localDelayFill = cfg.delay_fill;
if (cfg.delay_click) Settings.localDelayClick = cfg.delay_click;
applyDelays();
saveRulesToStorage();
saveSettings();
}
} catch(e) {}
}
// ---------- 检查管理员身份 ----------
async function checkAdminStatus() {
if (!backendOk || !token) return;
try {
var resp = await new Promise(function(res){GM_xmlhttpRequest({
method:'GET', url:API+'?action=admin_get_all_config',
headers:{'X-Auth-Token': token},
onload: function(r){try{res(JSON.parse(r.responseText));}catch{res({success:false,noAdmin:true});}},
onerror: function(){res({success:false,noAdmin:true});}
});});
isAdmin = resp.success && !resp.noAdmin;
} catch(e) { isAdmin = false; }
}
// ---------- 保存规则到服务器(仅管理员) ----------
async function saveRulesToServer() {
if (!backendOk || !token || !isAdmin) {
alert('需要管理员权限才能保存规则到服务器');
return;
}
if (!confirm('确定要将当前规则保存到服务器数据库吗?')) return;
try {
var dataToSave = {
protect_words: RULES.protectWords,
asr_fix: RULES.asrFix,
strong_ai_patterns: RULES.strongAIPatterns.map(function(p){return p.source;}),
invalid_keywords: RULES.invalidKeywords,
digit_map: RULES.digitMap,
unit_map: RULES.unitMap,
quantifier_list: RULES.quantifierList,
conversion_patterns: RULES.conversionPatterns || {}
};
var resp = await new Promise(function(res){GM_xmlhttpRequest({
method:'POST', url:API+'?action=admin_update_config',
headers:{'Content-Type':'application/json','X-Auth-Token':token},
data: JSON.stringify(dataToSave),
onload: function(r){try{res(JSON.parse(r.responseText));}catch{res({success:false});}},
onerror: function(){res({success:false});}
});});
if (resp.success) {
alert('规则已成功保存到服务器数据库!');
} else {
alert('保存失败: ' + (resp.message || '未知错误'));
}
} catch(e) { alert('保存出错'); }
}
// ---------- 主面板 ----------
function showMainPanel() {
var oldPanel = document.getElementById('login-panel');
if(oldPanel) oldPanel.remove();
oldPanel = document.getElementById('lb-panel');
if(oldPanel) oldPanel.remove();
const p=document.createElement('div'); p.id='lb-panel';
p.innerHTML = `<style>#lb-panel{position:fixed;top:10px;right:10px;z-index:99999;width:200px;background:#fff;border-radius:16px;box-shadow:0 8px 20px rgba(0,0,0,0.12);border:1px solid rgba(0,0,0,0.08);font-family:system-ui;overflow:hidden;transition:height 0.3s ease;}#lb-panel.dragging{background:#fff!important;}#lb-panel.minimized .body,#lb-panel.minimized .progress-bar,#lb-panel.minimized .buttons,#lb-panel.minimized .lb-text-row{display:none !important;}#lb-panel.minimized{height:46px;}.header{padding:12px 14px;cursor:move;display:flex;justify-content:space-between;border-bottom:1px solid rgba(0,0,0,0.05);}.title{font-weight:600;color:#1e293b;font-size:13px;}.actions span{cursor:pointer;margin-left:6px;font-size:12px;}.body{padding:14px;}.progress-bar{height:5px;background:#e2e8f0;border-radius:3px;margin:10px 0;overflow:hidden;}.progress-fill{height:100%;width:0%;background:linear-gradient(90deg,#3b82f6,#60a5fa);border-radius:3px;transition:width 0.15s ease;}.lb-text-row{display:flex;justify-content:center;gap:8px;font-size:11px;color:#64748b;}.buttons{display:flex;gap:6px;margin-top:12px;}.buttons button{flex:1;padding:7px;border:none;border-radius:10px;background:#f1f5f9;border:1px solid rgba(0,0,0,0.05);cursor:pointer;font-size:12px;}.buttons .primary{background:#3b82f6;color:#fff;}</style>
<div class="header"><div class="title">🤖 标注助手 (${user.username})</div><div class="actions"><span id="lb-minimize" title="最小化">−</span><span id="lb-settings" title="设置">⚙️</span><span id="lb-close" title="关闭">✕</span></div></div>
<div class="body"><div id="lb-status">⚪ 待命中</div><div class="progress-bar"><div class="progress-fill" id="lb-bar"></div></div><div class="lb-text-row"><span id="lb-text" style="text-align:center;font-size:12px;">0 / 0</span></div><div class="buttons"><button id="lb-start" class="primary">▶ 开始</button><button id="lb-stop" disabled>⏹ 停止</button></div></div>`;
document.body.appendChild(p);
st=document.getElementById('lb-status'); bar=document.getElementById('lb-bar'); txt=document.getElementById('lb-text');
btnS=document.getElementById('lb-start'); btnT=document.getElementById('lb-stop');
document.getElementById('lb-settings').onclick=showSettingsPanel;
document.getElementById('lb-close').onclick=()=>{ p.style.display='none'; };
document.getElementById('lb-minimize').onclick=()=>{ p.classList.toggle('minimized'); };
btnS.onclick=()=>{ if(!run){ idx=0; mainLoop(); } else { pause=!pause; updUI(); } };
btnT.onclick=()=>{ if(run){ stop=true; pause=false; updUI(); } };
// 登录成功后自动从服务器同步规则和速度设置
syncRulesFromBackend();
let drag=false, startX,startY,startLeft,startTop;
p.querySelector('.header').addEventListener('mousedown', e=>{
if(e.target.closest('.actions')) return;
drag=true; p.classList.add('dragging');
const rect=p.getBoundingClientRect();
startX=e.clientX; startY=e.clientY; startLeft=rect.left; startTop=rect.top;
p.style.left=startLeft+'px'; p.style.right='auto'; p.style.top=startTop+'px';
e.preventDefault();
});
document.addEventListener('mousemove', e=>{
if(!drag) return;
let left=startLeft+e.clientX-startX, top=startTop+e.clientY-startY;
left=Math.max(0, Math.min(left, window.innerWidth-p.offsetWidth));
top=Math.max(0, Math.min(top, window.innerHeight-p.offsetHeight));
p.style.left=left+'px'; p.style.top=top+'px';
});
document.addEventListener('mouseup', ()=>{ drag=false; p.classList.remove('dragging'); });
initManualClear();
updUI();
}
// ---------- 设置面板(含规则管理) ----------
function showSettingsPanel() {
const overlay=document.createElement('div'); overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:100002;display:flex;align-items:center;justify-content:center;';
const panel=document.createElement('div'); panel.style.cssText='background:#fff;border-radius:16px;padding:24px;width:900px;max-height:85vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.3);';
panel.innerHTML = `
<style>
.sp-tabs{display:flex;gap:8px;border-bottom:2px solid #e2e8f0;padding-bottom:12px;margin-bottom:20px;}
.sp-tab{padding:10px 24px;border:none;border-radius:12px 12px 0 0;cursor:pointer;background:#f1f5f9;color:#64748b;font-weight:500;transition:all 0.2s;}
.sp-tab:hover{background:#e2e8f0;}
.sp-tab.active{background:#3b82f6;color:#fff;}
.sp-content{display:none;}
.sp-content.active{display:block;}
.sp-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px;}
.sp-section{background:#f8fafc;border-radius:16px;padding:16px;}
.sp-section h4{margin:0 0 12px 0;color:#334155;font-size:14px;font-weight:600;}
.sp-row{display:flex;align-items:center;gap:8px;margin:8px 0;}
.sp-row label{flex:1;color:#475569;font-size:13px;}
.sp-row input[type="number"]{width:60px;padding:4px 8px;border:1px solid #e2e8f0;border-radius:6px;}
.sp-sync-btn{padding:10px 20px;background:#10b981;color:#fff;border:none;border-radius:10px;cursor:pointer;font-weight:500;margin-top:12px;width:100%;}
.sp-sync-btn:hover{background:#059669;}
.sp-rules-list{border:1px solid #e2e8f0;border-radius:12px;padding:12px;background:#fff;max-height:180px;overflow-y:auto;min-height:100px;}
.sp-rule{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border-radius:16px;margin:3px;font-size:12px;}
.sp-rule.protect{background:#dbeafe;color:#1e40af;}
.sp-rule.invalid{background:#fee2e2;color:#991b1b;}
.sp-rule.pattern{background:#d1fae5;color:#065f46;}
.sp-rule.asr{background:#fef3c7;color:#92400e;}
.sp-rule .del{background:none;border:none;cursor:pointer;color:inherit;opacity:0.5;padding:0 2px;font-size:11px;}
.sp-rule .del:hover{opacity:1;}
.sp-add-row{display:flex;gap:6px;margin-top:10px;}
.sp-add-row input{flex:1;padding:8px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;}
.sp-add-row button{padding:8px 16px;border:none;border-radius:8px;cursor:pointer;font-weight:500;background:#3b82f6;color:#fff;}
.sp-add-row button:hover{background:#2563eb;}
.sp-asr-table{width:100%;border-collapse:collapse;font-size:12px;}
.sp-asr-table th,.sp-asr-table td{padding:6px 8px;text-align:left;border-bottom:1px solid #e2e8f0;}
.sp-asr-table th{background:#f1f5f9;font-weight:600;color:#64748b;}
.sp-footer{display:flex;justify-content:space-between;align-items:center;margin-top:20px;padding-top:16px;border-top:1px solid #e2e8f0;}
.sp-footer .info{color:#94a3b8;font-size:12px;}
.sp-footer .btns{display:flex;gap:10px;}
.sp-footer button{padding:10px 20px;border:none;border-radius:10px;cursor:pointer;font-weight:500;}
</style>
<h2>⚙️ 设置与规则</h2>
<div class="sp-tabs">
<button class="sp-tab active" data-tab="settings">⚡ 运行参数</button>
<button class="sp-tab" data-tab="rules-protect">🔒 保护词</button>
<button class="sp-tab" data-tab="rules-invalid">❌ 无效词</button>
<button class="sp-tab" data-tab="rules-pattern">🧠 AI正则</button>
<button class="sp-tab" data-tab="rules-asr">🎯 ASR修正</button>
</div>
<!-- 运行参数 -->
<div class="sp-content active" data-content="settings">
<div class="sp-grid">
<div class="sp-section">
<h4>⚡ 速度设置</h4>
<div class="sp-row"><label>条目间隔</label><input type="number" id="set-dItem" value="${Settings.localDelayItem}"> ms</div>
<div class="sp-row"><label>填充等待</label><input type="number" id="set-dFill" value="${Settings.localDelayFill}"> ms</div>
<div class="sp-row"><label>点击等待</label><input type="number" id="set-dClick" value="${Settings.localDelayClick}"> ms</div>
<button id="syncSpeedBtn" style="margin-top:10px;padding:6px 12px;background:#10b981;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;">📥 同步后端设置</button>
</div>
<div class="sp-section">
<h4>🎯 自动化设置</h4>
<div class="sp-row"><label>自动勾选有效性</label><input type="checkbox" id="set-autoValid" ${Settings.autoSelectValid?'checked':''}></div>
<div class="sp-row"><label>手动无效清空</label><input type="checkbox" id="set-autoClear" ${Settings.autoClearOnInvalid?'checked':''}></div>
<div class="sp-row"><label>自动提交</label><input type="checkbox" id="set-autoSubmit" ${Settings.autoSubmit?'checked':''}></div>
</div>
</div>
<button id="syncServerBtn" class="sp-sync-btn">🔄 从服务器同步规则</button>
</div>
<!-- 保护词 -->
<div class="sp-content" data-content="rules-protect">
<p style="color:#64748b;margin-bottom:12px;font-size:13px;">保护词:转写时完全保留,不会进行任何转换</p>
<div id="sp-protect-list" class="sp-rules-list"></div>
<div class="sp-add-row">
<input type="text" id="sp-protect-input" placeholder="输入保护词...">
<button data-add="protect">添加</button>
</div>
</div>
<!-- 无效关键词 -->
<div class="sp-content" data-content="rules-invalid">
<p style="color:#64748b;margin-bottom:12px;font-size:13px;">无效关键词:包含这些关键词的条目将被标记为无效</p>
<div id="sp-invalid-list" class="sp-rules-list"></div>
<div class="sp-add-row">
<input type="text" id="sp-invalid-input" placeholder="输入无效关键词...">
<button data-add="invalid">添加</button>
</div>
</div>
<!-- 强AI正则 -->
<div class="sp-content" data-content="rules-pattern">
<p style="color:#64748b;margin-bottom:12px;font-size:13px;">强AI正则:匹配正则表达式的内容强制判定为AI生成</p>
<div id="sp-pattern-list" class="sp-rules-list"></div>
<div class="sp-add-row">
<input type="text" id="sp-pattern-input" placeholder="输入正则表达式...">
<button data-add="pattern">添加</button>
</div>
</div>
<!-- ASR修正 -->
<div class="sp-content" data-content="rules-asr">
<p style="color:#64748b;margin-bottom:12px;font-size:13px;">ASR修正:将错误识别的语音文本自动修正为正确内容</p>
<div style="max-height:200px;overflow-y:auto;">
<table class="sp-asr-table">
<thead><tr><th style="width:40%;">错误文本</th><th style="width:40%;">正确文本</th><th style="width:20%;"></th></tr></thead>
<tbody id="sp-asr-list"></tbody>
</table>
</div>
<div class="sp-add-row" style="margin-top:12px;">
<input type="text" id="sp-asr-wrong" placeholder="错误文本">
<span style="color:#94a3b8;">→</span>
<input type="text" id="sp-asr-correct" placeholder="正确文本">
<button data-add="asr">添加</button>
</div>
</div>
<div class="sp-footer">
<span class="info" id="sp-counts"></span>
<div class="btns">
<button id="saveToServerBtn" style="background:#10b981;color:#fff;display:none;" title="仅管理员可用">☁️ 保存到服务器</button>
<button id="cancelSettingsBtn" style="background:#e2e8f0;color:#475569;">取消</button>
<button id="saveSettingsBtn" style="background:#3b82f6;color:#fff;">💾 保存</button>
<button id="logoutSettingsBtn" style="background:#ef4444;color:#fff;">退出登录</button>
</div>
</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
// 切换标签
panel.querySelectorAll('.sp-tab').forEach(tab => {
tab.addEventListener('click', () => {
panel.querySelectorAll('.sp-tab').forEach(t => t.classList.remove('active'));
panel.querySelectorAll('.sp-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
panel.querySelector('[data-content="' + tab.dataset.tab + '"]').classList.add('active');
});
});
// 渲染规则列表
function renderProtect() {
document.getElementById('sp-protect-list').innerHTML = RULES.protectWords.length ?
RULES.protectWords.map(w => '<span class="sp-rule protect">' + escapeHtml(w) + '<button class="del" data-type="protect" data-word="' + escapeHtml(w) + '">✕</button></span>').join('') :
'<p style="color:#94a3b8;text-align:center;padding:30px;">暂无保护词</p>';
}
function renderInvalid() {
document.getElementById('sp-invalid-list').innerHTML = RULES.invalidKeywords.length ?
RULES.invalidKeywords.map(w => '<span class="sp-rule invalid">' + escapeHtml(w) + '<button class="del" data-type="invalid" data-word="' + escapeHtml(w) + '">✕</button></span>').join('') :
'<p style="color:#94a3b8;text-align:center;padding:30px;">暂无无效关键词</p>';
}
function renderPattern() {
var list = document.getElementById('sp-pattern-list');
if (!RULES.strongAIPatterns.length) {
list.innerHTML = '<p style="color:#94a3b8;text-align:center;padding:30px;">暂无强AI正则</p>';
return;
}
list.innerHTML = RULES.strongAIPatterns.map(function(p, i) {
return '<span class="sp-rule pattern">' + escapeHtml(p.source) + '<button class="del" data-type="pattern" data-idx="' + i + '">✕</button></span>';
}).join('');
}
function renderAsr() {
const entries = Object.entries(RULES.asrFix);
document.getElementById('sp-asr-list').innerHTML = entries.length ?
entries.map(function(e){var w=e[0],c=e[1];return '<tr><td>'+escapeHtml(w)+'</td><td>'+escapeHtml(c)+'</td><td><button class="del" data-type="asr" data-wrong="'+escapeHtml(w)+'" style="color:#ef4444;background:none;border:none;cursor:pointer;padding:2px 6px;">删除</button></td></tr>';}).join('') :
'<tr><td colspan="3" style="text-align:center;color:#94a3b8;padding:30px;">暂无ASR修正</td></tr>';
}
function updateCounts() {
document.getElementById('sp-counts').textContent = '🔒' + RULES.protectWords.length + ' ❌' + RULES.invalidKeywords.length + ' 🧠' + RULES.strongAIPatterns.length + ' 🎯' + Object.keys(RULES.asrFix).length;
}
function renderAll() { renderProtect(); renderInvalid(); renderPattern(); renderAsr(); updateCounts(); }
renderAll();
// 添加规则
panel.querySelectorAll('[data-add]').forEach(function(btn){
btn.addEventListener('click', function(){
var type = this.dataset.add;
if (type === 'protect') {
var val = document.getElementById('sp-protect-input').value.trim();
if (val && RULES.protectWords.indexOf(val) === -1) { RULES.protectWords.push(val); renderProtect(); updateCounts(); document.getElementById('sp-protect-input').value = ''; }
} else if (type === 'invalid') {
var val = document.getElementById('sp-invalid-input').value.trim();
if (val && RULES.invalidKeywords.indexOf(val) === -1) { RULES.invalidKeywords.push(val); renderInvalid(); updateCounts(); document.getElementById('sp-invalid-input').value = ''; }
} else if (type === 'pattern') {
var val = document.getElementById('sp-pattern-input').value.trim();
if (val && !RULES.strongAIPatterns.some(function(p){return p.source===val;})) { RULES.strongAIPatterns.push(new RegExp(val)); renderPattern(); updateCounts(); document.getElementById('sp-pattern-input').value = ''; }
} else if (type === 'asr') {
var wrong = document.getElementById('sp-asr-wrong').value.trim();
var correct = document.getElementById('sp-asr-correct').value.trim();
if (wrong && correct) { RULES.asrFix[wrong] = correct; renderAsr(); updateCounts(); document.getElementById('sp-asr-wrong').value = ''; document.getElementById('sp-asr-correct').value = ''; }
}
});
});
// 删除规则
panel.addEventListener('click', function(e){
var btn = e.target.closest('.del');
if (!btn) return;
var type = btn.dataset.type;
if (type === 'protect') RULES.protectWords = RULES.protectWords.filter(function(w){return w !== btn.dataset.word;});
else if (type === 'invalid') RULES.invalidKeywords = RULES.invalidKeywords.filter(function(w){return w !== btn.dataset.word;});
else if (type === 'pattern') {
var idx = parseInt(btn.dataset.idx, 10);
if (!isNaN(idx)) RULES.strongAIPatterns.splice(idx, 1);
}
else if (type === 'asr') delete RULES.asrFix[btn.dataset.wrong];
renderAll();
});
// 从服务器同步规则和速度设置
document.getElementById('syncServerBtn').addEventListener('click', async function(){
if (!backendOk) { alert('后端未连接'); return; }
try {
var resp = await new Promise(function(res){GM_xmlhttpRequest({method:'GET',url:API+'?action=get_full_runtime_config',onload:function(r){try{res(JSON.parse(r.responseText));}catch{res({success:false});}},onerror:function(){res({success:false});}});});
if (!resp.success || !resp.config) { alert('同步失败'); return; }
var cfg = resp.config;
RULES.protectWords = cfg.protect_words || [];
RULES.asrFix = cfg.asr_fix || {};
RULES.strongAIPatterns = (cfg.strong_ai_patterns || []).map(function(p){return new RegExp(p);});
RULES.invalidKeywords = cfg.invalid_keywords || [];
// 同步速度设置
if (cfg.delay_item) { Settings.localDelayItem = cfg.delay_item; document.getElementById('set-dItem').value = cfg.delay_item; }
if (cfg.delay_fill) { Settings.localDelayFill = cfg.delay_fill; document.getElementById('set-dFill').value = cfg.delay_fill; }
if (cfg.delay_click) { Settings.localDelayClick = cfg.delay_click; document.getElementById('set-dClick').value = cfg.delay_click; }
renderAll();
alert('已同步服务器规则与速度设置');
} catch(e) { alert('同步出错'); }
});
// 单独同步速度设置
document.getElementById('syncSpeedBtn').addEventListener('click', async function(){
if (!backendOk) { alert('后端未连接'); return; }
try {
var resp = await new Promise(function(res){GM_xmlhttpRequest({method:'GET',url:API+'?action=get_speed_config',onload:function(r){try{res(JSON.parse(r.responseText));}catch{res({success:false});}},onerror:function(){res({success:false});}});});
if (!resp.success || !resp.config) { alert('同步速度设置失败'); return; }
Settings.localDelayItem = resp.config.delay_item || Settings.localDelayItem;
Settings.localDelayFill = resp.config.delay_fill || Settings.localDelayFill;
Settings.localDelayClick = resp.config.delay_click || Settings.localDelayClick;
document.getElementById('set-dItem').value = Settings.localDelayItem;
document.getElementById('set-dFill').value = Settings.localDelayFill;
document.getElementById('set-dClick').value = Settings.localDelayClick;
alert('速度设置已同步');
} catch(e) { alert('同步出错'); }
});
// 保存
document.getElementById('cancelSettingsBtn').addEventListener('click', function(){overlay.remove();});
const dItemInput = document.getElementById('set-dItem');
const dFillInput = document.getElementById('set-dFill');
const dClickInput = document.getElementById('set-dClick');
document.getElementById('saveSettingsBtn').addEventListener('click', function(){
Settings.localDelayItem = +dItemInput.value;
Settings.localDelayFill = +dFillInput.value;
Settings.localDelayClick = +dClickInput.value;
Settings.autoSelectValid = document.getElementById('set-autoValid').checked;
Settings.autoClearOnInvalid = document.getElementById('set-autoClear').checked;
Settings.autoSubmit = document.getElementById('set-autoSubmit').checked;
saveSettings();
saveRulesToStorage();
overlay.remove();
alert('设置与规则已保存');
});
// 管理员保存到服务器按钮
if (isAdmin) {
document.getElementById('saveToServerBtn').style.display = 'block';
}
document.getElementById('saveToServerBtn').addEventListener('click', async function(){
await saveRulesToServer();
});
document.getElementById('logoutSettingsBtn').addEventListener('click', function(){localStorage.clear();location.reload();});
}
// ---------- 反馈面板 ----------
function showFeedbackPanel(itemIdx) {
const item = items[itemIdx];
const data = extractItemData(item, itemIdx);
if (!data) return;
const overlay=document.createElement('div'); overlay.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:100004;display:flex;align-items:center;justify-content:center;';
const panel=document.createElement('div'); panel.style.cssText='background:#fff;border-radius:16px;padding:24px;width:580px;max-height:80vh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,0.3);';
panel.innerHTML = `
<h2>💬 提交反馈</h2>
<p><strong>原始文本:</strong> ${escapeHtml(data.originalText)}</p>
<p><strong>当前转写:</strong> ${escapeHtml(data.textarea.value)}</p>
<label>正确文本(选填):</label>
<textarea id="fbCorrect" rows="2" style="width:100%; padding:8px; border-radius:8px; border:1px solid #ccc; margin-bottom:10px;"></textarea>
<label>错误类型:</label>
<select id="fbErrorType" style="width:100%; padding:8px; border-radius:8px; border:1px solid #ccc; margin-bottom:10px;">
<option value="数字转换错误">数字转换错误</option><option value="日期转换错误">日期转换错误</option><option value="AI误判">AI误判</option><option value="标点错误">标点错误</option><option value="其他">其他</option>
</select>
<label>补充说明(选填):</label>
<textarea id="fbNote" rows="2" style="width:100%; padding:8px; border-radius:8px; border:1px solid #ccc; margin-bottom:15px;"></textarea>
<button id="fbSubmit" style="padding:10px 20px; background:#3b82f6; color:#fff; border:none; border-radius:8px; cursor:pointer;">提交反馈</button>
<hr style="margin:20px 0;">
<h3>📜 近期反馈</h3>
<div id="fbHistory" style="max-height:200px; overflow-y:auto;">加载中...</div>
`;
overlay.appendChild(panel);
document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
GM_xmlhttpRequest({
method: 'GET', url: API + '?action=get_my_feedback&limit=5', headers: {'X-Auth-Token': token},
onload: r => {
try {
const j = JSON.parse(r.responseText);
const historyDiv = document.getElementById('fbHistory');
if (j.success && j.feedback.length) {
historyDiv.innerHTML = j.feedback.map(f=>`<div style="border-bottom:1px solid #eee;padding:8px 0;"><b>${f.error_type}</b> ${f.created_at}<br>${f.original_text}<br>${f.status==='已修正'?'✅ 已修正':(f.admin_reply||'⏳ 待处理')}</div>`).join('');
} else historyDiv.innerHTML = '<div style="color:#999;">暂无反馈记录</div>';
} catch(e) {}
}
});
document.getElementById('fbSubmit').onclick = () => {
const correct = document.getElementById('fbCorrect').value.trim();
const errorType = document.getElementById('fbErrorType').value;
const note = document.getElementById('fbNote').value.trim();
const submitBtn = document.getElementById('fbSubmit');
submitBtn.disabled = true;
submitBtn.textContent = '提交中...';
GM_xmlhttpRequest({
method: 'POST', url: API + '?action=submit_feedback',
data: JSON.stringify({
original_text: data.originalText,
transcribed_text: data.textarea.value,
correct_text: correct,
error_type: errorType,
user_is_valid: curValid ? 1 : 0,
system_is_valid: curValid ? 1 : 0,
note: note
}),
headers: {'Content-Type':'application/json','X-Auth-Token':token},
onload: r => {
try {
const resp = JSON.parse(r.responseText);
if (resp.success) {
alert('反馈已提交,感谢您的反馈!');
} else {
alert('提交失败: ' + (resp.message || '未知错误'));
submitBtn.disabled = false;
submitBtn.textContent = '提交反馈';
return;
}
} catch(e) {
alert('提交失败: 响应解析错误');
submitBtn.disabled = false;
submitBtn.textContent = '提交反馈';
return;
}
overlay.remove();
},
onerror: () => {
alert('提交失败: 网络错误');
submitBtn.disabled = false;
submitBtn.textContent = '提交反馈';
}
});
};
}
function initManualClear() {
document.body.addEventListener('change', e => {
if(!e.isTrusted||e.target.type!=='radio'||!Settings.autoClearOnInvalid) return;
const label=e.target.closest('label.ant-v5-radio-wrapper');
if(!label) return;
const span=label.querySelector('.ant-v5-radio-label');
if(!span||span.innerText.trim()!=='无效') return;
const item=e.target.closest('.labelRender-item');
if(!item) return;
let ta=null;
for(let w of item.querySelectorAll('.labelRender-item-answer-wrap')) {
const ti=w.querySelector('.labelRender-item-answer-title');
if(ti&&ti.innerText.includes('转写文本')) { ta=w.querySelector('textarea.ant-v5-input'); break; }
}
if(ta) setTextarea(ta,'');
});
}
// ---------- 初始化(修复登录持久化) ----------
async function init() {
loadSettings();
loadRules();
try {
const resp=await new Promise((res,rej)=>GM_xmlhttpRequest({ method:'GET', url:API+'?action=ping', onload:res, onerror:rej }));
backendOk=JSON.parse(resp.responseText).success===true;
} catch(e) { backendOk=false; }
if (backendOk && token) {
// 验证 token 有效性(调用一个轻量接口)
try {
const validResp = await new Promise(res=> GM_xmlhttpRequest({
method:'GET', url:API+'?action=check_quota', headers:{'X-Auth-Token':token},
onload: r=>{ try{ res(JSON.parse(r.responseText)); }catch{ res({success:false}); } },
onerror: ()=> res({success:false})
}));
if (validResp.success) {
loggedIn = true;
user = { username: savedUser };
// 检查是否为管理员
checkAdminStatus();
showMainPanel();
return;
}
} catch(e) {}
// token 无效,清除
localStorage.removeItem(STORAGE_TOKEN);
localStorage.removeItem(STORAGE_USER);
token = '';
}
showLoginPanel();
}
init();
})();
增加登陆系统,接入数据库,新增后台控制关键词减少前端代码
优化启动逻辑,优化前端面板
📋 规则总览与示例
| 规则类别 | 具体规则 | 示例输入 | 输出/判定 |
|---|---|---|---|
| AI无效过滤 | 长关键词(≥5字) | 您拨打的电话已关机 | ❌ 无效 |
| 短关键词(<5字) | 重录秦案2、空号 | ❌ 无效(直接命中) | |
| 纯问候语(≤5字) | 你好、在吗 | ❌ 无效 | |
| 强AI模板(正则) | 您拨叫的用户正忙、请在滴声后留言 | ❌ 无效 | |
| 引导指令评分 | 按1转接,按井号重录 | ❌ 无效(得分≥5) | |
| 数字转换 | 整数金额 → 纯数字 | 十四万八千 | 148000 |
| 金额带角分 → 补零保留单位 | 十四万八千块零六毛六 | 148000块06毛06分 | |
| 日期时间 | 二零二五年十月三号十一点二十五分 | 2025年10月3号11点25分 | |
| 小数(前后数字) | 三点一四 | 3.14 | |
| 范围 | 三到五个 | 3~5个 | |
| 电话/编码(逐位) | 幺五九杠八八八 | 159-888 | |
| 百分数 | 百分之二十 | 20% | |
| 分数 | 三分之二 | 2/3 | |
| 大数(≥1万) | 五万一千 | 51000(整数)或 5万1000(如带单位) | |
| 绝对保护 | 一/二/两 + 量词 | 一个、两条、两款 | 原样保留 |
| 常见成语/专名 | 一心一意、三国演义 | 原样保留 | |
| 标点/空格 | 合并字母间空格 | a p p | app |
| 英文标点转中文 | hello. | hello。 | |
| 疑问句自动加问号 | 是吗 | 是吗? |
阉割版0.1
只保留了转写部分
© 版权声明
THE END











暂无评论内容