NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name 太平车险自动输入采集
// @namespace https://docs.scriptcat.org/
// @version 0.1.0
// @description try to take over the world!
// @author Mail: admin@zhirong.cloud
// @match https://autopp.tpi.cntaiping.com/web/home/
// @icon https://www.google.com/s2/favicons?sz=64&domain=autopp.tpi.cntaiping.com
// @grant none
// @license Apache-2.0
// @noframes
// ==/UserScript==
(function() {
'use strict';
// ====================== 请在这里粘贴你的车架号列表 ======================
const VIN_LIST =["车架号,车主"]
// ==============================================================
function parseVinRecords(list) {
const records = [];
list.forEach(item => {
const raw = String(item || '').trim();
if (!raw) return;
const parts = raw.split(',');
const vin = (parts.shift() || '').trim();
const owner = parts.join(',').trim();
if (!vin || vin === '车架号' || vin.toLowerCase() === 'vin') {
return;
}
records.push({ vin, owner });
});
return records;
}
const VIN_RECORDS = parseVinRecords(VIN_LIST);
// 元素定位【完整更新】
const SELECTOR = {
vinInput: '#simple-proposal > div:nth-child(2) > div.context > div.tp-form-cover > form > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div > div > div > div > div > input',
searchBtn: '#simple-proposal > div:nth-child(2) > div.context > div.tp-form-cover > form > div:nth-child(1) > div:nth-child(2) > div > div > div > div > div.tp-form-item-ext > button',
// 带入按钮(第一个按钮)
bringInBtn: 'body > div:nth-child(12) > div > div.el-dialog__footer > span > button:nth-child(1)',
// 车主名字输入框
ownerInput: 'input[inputcode="carOwnerNow"]',
// 弹窗判断元素(标题)
dialogCheck: 'body > div.el-dialog__wrapper.dialog-large-page > div > div.el-dialog__header > span',
// 需要点击的按钮
dialogBtn: 'body > div.el-dialog__wrapper.dialog-large-page > div > div.el-dialog__body > div > div.el-table__body-wrapper.is-scrolling-none > table > tbody > tr:nth-child(1) > td.el-table_12_column_81 > div > button > span > p',
// 手机号输入框
phoneInput: '#simple-proposal > div:nth-child(4) > div.context > div:nth-child(3) > div:nth-child(2) > div.tp-form-cover.tp-form-policyholder-info > form > div:nth-child(1) > div:nth-child(4) > div:nth-child(1) > div > div > div > div > div > input'
};
// 全局状态
const STATUS = {
total: VIN_RECORDS.length,
current: 0,
success: 0,
fail: 0,
isRunning: false,
isPaused: false,
stopFlag: false
};
const RESULTS = [];
const CONFIG = {
autoExportEvery: 10,
maxLogLines: 260,
logTrimTo: 180,
compactSuccessLogs: true,
cooldownEvery: 10,
cooldownMs: 3500,
maxPhoneRetries: 5,
phoneLookupWaitMs: 1200,
phoneRetryDelayMs: 1000
};
const LOG_RUNTIME = {
suppressedSuccess: 0
};
const NOISY_SUCCESS_PATTERNS = [
/车架号填写完成/,
/已点击智能检索/,
/已填写车主名字/,
/已关闭弹窗/,
/弹窗按钮点击成功/,
/检测到弹窗,执行点击按钮/
];
function injectStyles() {
if (document.getElementById('auto-tool-style')) return;
const style = document.createElement('style');
style.id = 'auto-tool-style';
style.textContent = `
#auto-tool-panel {
position: fixed;
top: 16px;
right: 16px;
width: 396px;
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
border: 1px solid #dbe7ff;
border-radius: 14px;
box-shadow: 0 14px 36px rgba(15, 53, 120, 0.18);
z-index: 999999;
padding: 12px;
font-size: 12px;
font-family: "Microsoft YaHei", sans-serif;
}
.auto-tool-title {
font-weight: 700;
font-size: 15px;
margin-bottom: 8px;
text-align: center;
color: #163f7a;
letter-spacing: 0.2px;
}
#status-info {
margin-bottom: 8px;
color: #30486d;
line-height: 1.6;
background: #edf4ff;
border-radius: 8px;
padding: 6px 8px;
border: 1px solid #dbe8ff;
}
.auto-tool-btn-group {
display: flex;
gap: 6px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.auto-tool-btn {
flex: 1;
padding: 7px;
color: #fff;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
.auto-tool-btn:hover {
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(19, 44, 95, 0.26);
filter: brightness(1.04);
}
.auto-tool-btn-start { background: linear-gradient(135deg, #1b8cff, #1c6dff); }
.auto-tool-btn-pause { background: linear-gradient(135deg, #ff9a2f, #ff6a3a); }
.auto-tool-btn-export { background: linear-gradient(135deg, #12b86f, #17a06a); }
.auto-tool-btn-view { background: linear-gradient(135deg, #6f6ef9, #4b63e6); }
#log-box {
height: 240px;
overflow-y: auto;
background: #0f172a;
color: #d3ddff;
padding: 8px 10px;
border: 1px solid #1f335f;
border-radius: 8px;
font-size: 11px;
line-height: 1.45;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}
#log-box .log-line {
padding: 2px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.09);
white-space: pre-wrap;
word-break: break-all;
}
#log-box .log-line:last-child { border-bottom: none; }
#log-box .log-clean { color: #93a9d6; }
#result-viewer-mask {
position: fixed;
inset: 0;
background: rgba(6, 15, 37, 0.5);
z-index: 1000000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.result-viewer-dialog {
width: 760px;
max-width: 92vw;
max-height: 88vh;
background: linear-gradient(180deg, #ffffff 0%, #f9fbff 100%);
border-radius: 16px;
box-shadow: 0 22px 54px rgba(6, 20, 53, 0.28);
border: 1px solid #d8e6ff;
display: flex;
flex-direction: column;
overflow: hidden;
}
.result-viewer-header {
padding: 14px 16px 10px;
color: #fff;
background: linear-gradient(120deg, #205ccf 0%, #2f8cff 90%);
}
.result-viewer-title {
font-size: 15px;
font-weight: 700;
margin-bottom: 8px;
}
.result-viewer-stats {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.result-viewer-stat {
padding: 4px 8px;
border-radius: 999px;
font-size: 11px;
background: rgba(255, 255, 255, 0.2);
}
.result-viewer-body {
padding: 12px;
background: #f7faff;
}
.result-viewer-textarea {
width: 100%;
min-height: 330px;
max-height: 56vh;
padding: 12px;
resize: vertical;
border: 1px solid #d4e2ff;
border-radius: 10px;
font-size: 12px;
line-height: 1.65;
color: #20304f;
box-sizing: border-box;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
background: #fff;
}
.result-viewer-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px;
border-top: 1px solid #e7efff;
background: #fff;
}
.result-viewer-btn {
padding: 7px 14px;
border-radius: 8px;
cursor: pointer;
transition: filter 0.2s ease;
}
.result-viewer-btn:hover { filter: brightness(1.05); }
.result-viewer-btn-primary {
color: #fff;
border: none;
background: linear-gradient(135deg, #1f7bff, #2464d6);
}
.result-viewer-btn-ghost {
border: 1px solid #ccd7ef;
color: #2e476f;
background: #fff;
}
`;
document.head.appendChild(style);
}
function createActionButton(text, className, onClick) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = `auto-tool-btn ${className}`;
btn.innerText = text;
btn.onclick = onClick;
return btn;
}
// —————————————— 悬浮控制面板 ——————————————
function createPanel() {
const oldPanel = document.getElementById('auto-tool-panel');
if (oldPanel) oldPanel.remove();
const panel = document.createElement('div');
panel.id = 'auto-tool-panel';
const title = document.createElement('div');
title.className = 'auto-tool-title';
title.innerText = '车险自动处理工具';
const statusLine = document.createElement('div');
statusLine.id = 'status-info';
const btnGroup = document.createElement('div');
btnGroup.className = 'auto-tool-btn-group';
const startBtn = createActionButton('启动任务', 'auto-tool-btn-start', startTask);
const pauseBtn = createActionButton('暂停', 'auto-tool-btn-pause', togglePause);
pauseBtn.id = 'pause-btn';
const exportBtn = createActionButton('导出结果', 'auto-tool-btn-export', () => exportResult({ auto: false }));
const viewBtn = createActionButton('查看结果', 'auto-tool-btn-view', showResultViewer);
const logBox = document.createElement('div');
logBox.id = 'log-box';
btnGroup.append(startBtn, pauseBtn, exportBtn, viewBtn);
panel.append(title, statusLine, btnGroup, logBox);
document.body.appendChild(panel);
}
function updateStatus() {
const el = document.getElementById('status-info');
const pauseText = STATUS.isPaused ? '【已暂停】' : STATUS.isRunning ? '【运行中】' : '【已停止】';
el.innerHTML = `状态:${pauseText} | 总数:${STATUS.total} | 当前:${STATUS.current} | 成功:${STATUS.success} | 失败:${STATUS.fail}`;
}
function appendLogLine(logBox, text, className = '') {
const line = document.createElement('div');
line.className = `log-line${className ? ` ${className}` : ''}`;
line.textContent = text;
logBox.appendChild(line);
}
function isNoisySuccessLog(text) {
if (!CONFIG.compactSuccessLogs) return false;
if (!String(text || '').startsWith('✅')) return false;
return NOISY_SUCCESS_PATTERNS.some((pattern) => pattern.test(text));
}
function addLog(text) {
const logBox = document.getElementById('log-box');
if (!logBox) return;
const time = new Date().toLocaleTimeString();
if (isNoisySuccessLog(text)) {
LOG_RUNTIME.suppressedSuccess++;
if (LOG_RUNTIME.suppressedSuccess >= 12) {
appendLogLine(logBox, `[${time}] ℹ️ 已省略 ${LOG_RUNTIME.suppressedSuccess} 条常规成功日志`, 'log-clean');
LOG_RUNTIME.suppressedSuccess = 0;
}
logBox.scrollTop = logBox.scrollHeight;
return;
}
if (LOG_RUNTIME.suppressedSuccess > 0) {
appendLogLine(logBox, `[${time}] ℹ️ 已省略 ${LOG_RUNTIME.suppressedSuccess} 条常规成功日志`, 'log-clean');
LOG_RUNTIME.suppressedSuccess = 0;
}
appendLogLine(logBox, `[${time}] ${text}`);
if (logBox.children.length > CONFIG.maxLogLines) {
while (logBox.children.length > CONFIG.logTrimTo) {
logBox.removeChild(logBox.firstChild);
}
appendLogLine(logBox, `[${time}] 🧹 日志过多,已自动清理(保留最近 ${CONFIG.logTrimTo} 条)`, 'log-clean');
}
logBox.scrollTop = logBox.scrollHeight;
}
function togglePause() {
if (!STATUS.isRunning) { addLog('⚠️ 任务未启动'); return; }
STATUS.isPaused = !STATUS.isPaused;
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) pauseBtn.textContent = STATUS.isPaused ? '继续' : '暂停';
addLog(STATUS.isPaused ? '⏸️ 任务已暂停' : '▶️ 任务已继续');
updateStatus();
}
function getResultsText() {
if (RESULTS.length === 0) return '';
return RESULTS
.map((item, index) => `${index + 1}. 车架号:${item.vin} 车主:${item.owner || '-'} 手机号:${item.phone}`)
.join('\n');
}
function formatFileTimestamp(date = new Date()) {
const pad = (num) => String(num).padStart(2, '0');
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}_${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
}
function getExportText() {
const summary = [
`导出时间:${new Date().toLocaleString()}`,
`任务总数:${STATUS.total}`,
`已处理:${STATUS.current}`,
`成功:${STATUS.success}`,
`失败:${STATUS.fail}`,
''
].join('\n');
const details = getResultsText() || '暂无抓取成功记录';
return `${summary}\n${details}`;
}
function copyTextToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
return new Promise((resolve, reject) => {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textArea);
if (success) resolve();
else reject(new Error('复制失败'));
} catch (error) {
document.body.removeChild(textArea);
reject(error);
}
});
}
function exportResult(options = {}) {
const { auto = false } = options;
if (auto && STATUS.current === 0) return false;
const content = getExportText();
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const prefix = auto ? '车险结果_自动备份' : '车险结果';
a.download = `${prefix}_${formatFileTimestamp()}_已处理${STATUS.current}.txt`;
a.click();
URL.revokeObjectURL(url);
addLog(auto ? `💾 自动导出完成(已处理 ${STATUS.current} 条)` : '📁 结果已导出为TXT文件');
return true;
}
function showResultViewer() {
const oldMask = document.getElementById('result-viewer-mask');
if (oldMask) oldMask.remove();
const mask = document.createElement('div');
mask.id = 'result-viewer-mask';
const dialog = document.createElement('div');
dialog.className = 'result-viewer-dialog';
const header = document.createElement('div');
header.className = 'result-viewer-header';
const title = document.createElement('div');
title.className = 'result-viewer-title';
title.textContent = `抓取结果列表(共 ${RESULTS.length} 条)`;
const stats = document.createElement('div');
stats.className = 'result-viewer-stats';
const statItems = [
`总数 ${STATUS.total}`,
`已处理 ${STATUS.current}`,
`成功 ${STATUS.success}`,
`失败 ${STATUS.fail}`
];
statItems.forEach((item) => {
const chip = document.createElement('span');
chip.className = 'result-viewer-stat';
chip.textContent = item;
stats.appendChild(chip);
});
header.append(title, stats);
const body = document.createElement('div');
body.className = 'result-viewer-body';
const textarea = document.createElement('textarea');
textarea.readOnly = true;
textarea.value = getResultsText() || '暂无抓取结果';
textarea.className = 'result-viewer-textarea';
body.appendChild(textarea);
const footer = document.createElement('div');
footer.className = 'result-viewer-footer';
const copyBtn = document.createElement('button');
copyBtn.textContent = '复制结果';
copyBtn.className = 'result-viewer-btn result-viewer-btn-primary';
const closeBtn = document.createElement('button');
closeBtn.textContent = '关闭';
closeBtn.className = 'result-viewer-btn result-viewer-btn-ghost';
copyBtn.onclick = async () => {
const content = textarea.value;
if (!content || content === '暂无抓取结果') {
addLog('⚠️ 暂无可复制结果');
return;
}
try {
await copyTextToClipboard(content);
addLog('✅ 结果列表已复制到剪贴板');
copyBtn.textContent = '已复制';
setTimeout(() => {
copyBtn.textContent = '复制结果';
}, 1200);
} catch (error) {
addLog(`❌ 复制失败:${error.message || error}`);
}
};
closeBtn.onclick = () => mask.remove();
mask.onclick = (event) => {
if (event.target === mask) mask.remove();
};
footer.append(copyBtn, closeBtn);
dialog.append(header, body, footer);
mask.appendChild(dialog);
document.body.appendChild(mask);
}
// —————————————— 工具函数 ——————————————
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitForCondition(checkFn, options = {}) {
const { timeout = 5000, interval = 200 } = options;
const endAt = Date.now() + timeout;
while (Date.now() <= endAt) {
const result = checkFn();
if (result) return result;
await sleep(interval);
}
return null;
}
async function waitForElementSmart(selector, options = {}) {
const { visible = false } = options;
return waitForCondition(() => {
const el = document.querySelector(selector);
if (!el) return null;
if (!visible) return el;
return el.offsetParent !== null ? el : null;
}, options);
}
async function waitForElementGone(selector, options = {}) {
const done = await waitForCondition(() => {
const el = document.querySelector(selector);
return !el || el.offsetParent === null ? true : null;
}, options);
return Boolean(done);
}
function waitForElement(selector, retry = 1, interval = 2000) {
const timeout = (retry + 1) * interval;
return waitForElementSmart(selector, { timeout, interval });
}
async function closeKnowDialogIfPresent() {
const knowBtn = await waitForElementSmart('.el-message-box__btns button.el-button--primary', {
timeout: 1200,
interval: 180,
visible: true
});
if (!knowBtn || !String(knowBtn.textContent || '').includes('知道了')) return false;
knowBtn.click();
await waitForElementGone('.el-message-box__wrapper', { timeout: 2200, interval: 160 });
addLog('ℹ️ 已处理提示弹窗');
return true;
}
function setInputValue(el, value) {
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function findButtonByText(text) {
const buttons = document.querySelectorAll('button.el-button');
const targetText = String(text || '').replace(/\s+/g, '');
for (const button of buttons) {
const buttonText = String(button.textContent || '').replace(/\s+/g, '');
const isTextMatched = targetText === '带入'
? buttonText === '带入'
: buttonText.includes(targetText);
if (isTextMatched && button.offsetParent !== null) {
return button;
}
}
return null;
}
async function waitForResume() {
while (STATUS.isPaused) {
await sleep(320);
}
}
// —————————————— 核心流程【最新完整版】 ——————————————
async function processVin(record) {
const vin = record?.vin || '';
const owner = record?.owner || '';
const recordTag = `${vin}${owner ? ` / ${owner}` : ''}`;
const fail = (message) => {
addLog(`❌ [${recordTag}] ${message}`);
return false;
};
await waitForResume();
STATUS.current++;
updateStatus();
addLog(`开始处理:${recordTag}`);
// 1. 填写车架号
const vinEl = await waitForElementSmart(SELECTOR.vinInput, {
timeout: 4200,
interval: 220
});
if (!vinEl) return fail('未找到车架号输入框');
setInputValue(vinEl, vin);
addLog('✅ 车架号填写完成');
// 2. 点击智能检索
const searchBtn = await waitForElementSmart(SELECTOR.searchBtn, {
timeout: 4200,
interval: 220,
visible: true
});
if (!searchBtn) return fail('未找到检索按钮');
searchBtn.click();
addLog('✅ 已点击智能检索');
await closeKnowDialogIfPresent();
// 3. 填写车主后点击【带入】按钮
let ownerInput = await waitForElementSmart(SELECTOR.ownerInput, {
timeout: 5200,
interval: 220,
visible: true
});
if (!ownerInput) {
ownerInput = document.querySelector('div.el-form-item input[inputcode="carOwnerNow"]');
}
if (!ownerInput) return fail('未找到车主名字输入框,跳过');
if (!owner) return fail('当前记录缺少车主名字,跳过带入');
setInputValue(ownerInput, owner);
ownerInput.dispatchEvent(new Event('blur', { bubbles: true }));
addLog(`✅ 已填写车主名字:${owner}`);
let confirmBtn = await waitForElementSmart(SELECTOR.bringInBtn, {
timeout: 4200,
interval: 220,
visible: true
});
if (!confirmBtn || String(confirmBtn.textContent || '').replace(/\s+/g, '') !== '带入') {
confirmBtn = findButtonByText('带入');
}
if (confirmBtn) {
confirmBtn.click();
addLog('✅ 已点击【带入】按钮');
await Promise.race([
waitForElementGone(SELECTOR.bringInBtn, { timeout: 4200, interval: 180 }),
waitForElementSmart(SELECTOR.dialogCheck, { timeout: 4200, interval: 200, visible: true }),
waitForElementSmart(SELECTOR.phoneInput, { timeout: 4200, interval: 220 })
]);
let confirmBtnAfterClick = document.querySelector(SELECTOR.bringInBtn);
if (
(!confirmBtnAfterClick || String(confirmBtnAfterClick.textContent || '').replace(/\s+/g, '') !== '带入')
&& findButtonByText('带入')
) {
confirmBtnAfterClick = findButtonByText('带入');
}
if (confirmBtnAfterClick && confirmBtnAfterClick.offsetParent !== null) {
addLog('⚠️ 带入按钮仍存在,执行兜底点击');
confirmBtnAfterClick.click();
await waitForElementGone(SELECTOR.bringInBtn, { timeout: 3000, interval: 180 });
}
} else {
return fail('未找到带入按钮,跳过');
}
await closeKnowDialogIfPresent();
// ====================== 【新增步骤:弹窗判断+点击按钮】 ======================
const dialogExist = document.querySelector(SELECTOR.dialogCheck)
|| await waitForElementSmart(SELECTOR.dialogCheck, { timeout: 1600, interval: 180, visible: true });
if (dialogExist) {
addLog('ℹ️ 检测到业务弹窗,执行点击按钮');
const targetBtn = await waitForElementSmart(SELECTOR.dialogBtn, {
timeout: 2600,
interval: 180,
visible: true
});
if (targetBtn) {
targetBtn.click();
await Promise.race([
waitForElementGone(SELECTOR.dialogBtn, { timeout: 3000, interval: 180 }),
waitForElementSmart(SELECTOR.phoneInput, { timeout: 3000, interval: 220 })
]);
// 兜底方案:检查按钮是否还存在,如果存在则再次点击
const targetBtnAfterClick = document.querySelector(SELECTOR.dialogBtn);
if (targetBtnAfterClick && targetBtnAfterClick.offsetParent !== null) {
addLog('⚠️ 弹窗按钮仍存在,执行兜底点击');
targetBtnAfterClick.click();
await waitForElementGone(SELECTOR.dialogBtn, { timeout: 2600, interval: 180 });
}
addLog('✅ 弹窗按钮点击成功');
} else {
addLog(`⚠️ [${recordTag}] 检测到弹窗但未找到操作按钮`);
}
} else {
addLog('ℹ️ 未检测到弹窗,直接执行下一步');
}
// ============================================================================
await closeKnowDialogIfPresent();
// 5. 查找手机号(重试)
let phone = '';
for (let attempt = 1; attempt <= CONFIG.maxPhoneRetries; attempt++) {
await waitForResume();
await closeKnowDialogIfPresent();
const phoneEl = await waitForElementSmart(SELECTOR.phoneInput, {
timeout: CONFIG.phoneLookupWaitMs,
interval: 200
});
const phoneValue = phoneEl && phoneEl.value ? phoneEl.value.trim() : '';
if (phoneValue) {
phone = phoneValue;
break;
}
addLog(`❌ [${recordTag}] 第${attempt}次未获取到手机号,将在${CONFIG.phoneRetryDelayMs}ms后重试`);
await sleep(CONFIG.phoneRetryDelayMs);
}
if (phone) {
RESULTS.push({ vin, owner, phone });
STATUS.success++;
addLog(`✅ [${vin}] 获取手机号成功:${phone}`);
return true;
}
return fail(`${CONFIG.maxPhoneRetries}次尝试均未获取到手机号,跳过`);
}
function autoExportCheckpoint() {
if (STATUS.current > 0 && STATUS.current % CONFIG.autoExportEvery === 0) {
exportResult({ auto: true });
}
}
async function cooldownCheckpoint() {
if (STATUS.current <= 0) return;
if (STATUS.current % CONFIG.cooldownEvery !== 0) return;
if (!STATUS.isRunning) return;
const totalSeconds = (CONFIG.cooldownMs / 1000).toFixed(1);
addLog(`🛡️ 已处理 ${STATUS.current} 条,执行 ${totalSeconds} 秒冷却,降低页面压力`);
const endAt = Date.now() + CONFIG.cooldownMs;
while (Date.now() < endAt) {
await waitForResume();
const remain = endAt - Date.now();
if (remain <= 0) break;
await sleep(Math.min(300, remain));
}
addLog('▶️ 冷却结束,继续处理');
}
// —————————————— 启动任务 ——————————————
async function startTask() {
if (STATUS.isRunning) { addLog('⚠️ 任务正在运行中'); return; }
if (VIN_RECORDS.length === 0) { addLog('❌ 请先粘贴车架号列表'); return; }
STATUS.total = VIN_RECORDS.length;
STATUS.current = 0;
STATUS.success = 0;
STATUS.fail = 0;
STATUS.isRunning = true;
STATUS.isPaused = false;
STATUS.stopFlag = false;
RESULTS.length = 0;
const pauseBtn = document.getElementById('pause-btn');
if (pauseBtn) pauseBtn.textContent = '暂停';
updateStatus();
addLog('🚀 任务启动,开始批量处理');
addLog(`💡 已启用自动备份:每处理 ${CONFIG.autoExportEvery} 条自动导出 TXT`);
addLog(`💡 已启用分段冷却:每处理 ${CONFIG.cooldownEvery} 条暂停 ${CONFIG.cooldownMs}ms`);
for (const record of VIN_RECORDS) {
if (STATUS.stopFlag) break;
await waitForResume();
let success = false;
try {
success = await processVin(record);
} catch (error) {
addLog(`❌ 执行异常:${error && error.message ? error.message : error}`);
}
if (!success) STATUS.fail++;
updateStatus();
autoExportCheckpoint();
await cooldownCheckpoint();
await sleep(260);
}
STATUS.isRunning = false;
STATUS.isPaused = false;
if (pauseBtn) pauseBtn.textContent = '暂停';
updateStatus();
if (STATUS.current > 0 && STATUS.current % CONFIG.autoExportEvery !== 0) {
exportResult({ auto: true });
}
addLog('🎉 全部任务执行完成');
addLog(`📊 最终统计:成功 ${STATUS.success} 条,失败 ${STATUS.fail} 条`);
}
// 初始化
injectStyles();
createPanel();
updateStatus();
addLog('✅ 工具加载完成,可点击按钮操作');
})();