PeisenXu / 太平车险自动输入采集

// ==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('✅ 工具加载完成,可点击按钮操作');
})();