// js/history.js

// 全局函数定义（在 DOMContentLoaded 外部，确保脚本加载后立即可以访问）
window.deleteHistoryRecord = function(id, name) {
    // 在 DOMContentLoaded 内部实际执行
    const execute = () => {
        openHistoryClearModal('record', { recordId: id, recordName: name || '' });
    };
    // 如果 DOM 已加载，直接执行；否则等待
    if (document.readyState !== 'loading') {
        execute();
    } else {
        document.addEventListener('DOMContentLoaded', execute);
    }
};

window.showHistoryDetail = function(id) {
    try {
        localStorage.setItem('pbx_current_doc_id', id);
    } catch (e) {
        console.warn('[showHistoryDetail] Failed to save doc id to localStorage', e);
    }
    window.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
};

// =====================
// 历史记录面板相关逻辑
// =====================

/**
 * 当 HTML 文档完全加载并解析完成后，执行此函数。
 * 主要负责初始化历史记录面板的用户交互：
 *  - 为"显示历史"按钮绑定点击事件，用于打开历史面板并渲染历史列表。
 *  - 为"关闭历史面板"按钮绑定点击事件。
 *  - 为"清空历史记录"按钮绑定点击事件，并在用户确认后清空所有历史数据并刷新列表。
 */
document.addEventListener('DOMContentLoaded', function() {
    const REQUIRED_CLEAR_PHRASE = '确定删除';

    // --------------------------------------------------
    // 侧边栏快捷历史记录逻辑
    // --------------------------------------------------
    async function renderSidebarQuickAccess() {
        const quickListEl = document.getElementById('sidebarHistoryQuickList');
        if (!quickListEl) return;

        try {
            // 假设 getAllResultsFromDB 是全局可用的 (在 storage.js 中定义)
            const results = await window.getAllResultsFromDB();
            if (!results || !Array.isArray(results) || results.length === 0) {
                quickListEl.innerHTML = '<div class="px-3 py-2 text-xs text-slate-400 text-center">暂无记录</div>';
                return;
            }

            // 按时间倒序取前 5 条
            const recent = results.slice().sort((a, b) => new Date(b.time) - new Date(a.time)).slice(0, 5);

            quickListEl.innerHTML = recent.map(record => {
                const safeId = escapeAttr(record.id);
                const name = escapeHtml(record.name || '未命名文档');
                // 简短时间格式: MM/DD HH:mm
                const timeObj = new Date(record.time);
                const timeStr = `${timeObj.getMonth() + 1}/${timeObj.getDate()} ${String(timeObj.getHours()).padStart(2, '0')}:${String(timeObj.getMinutes()).padStart(2, '0')}`;

                return `
                    <div class="group flex items-center gap-2 px-2 py-1.5 text-[13px] text-slate-600 hover:bg-slate-100 hover:text-slate-900 transition-colors cursor-pointer rounded-md mx-2 mb-0.5" onclick="showHistoryDetail('${safeId}')" title="${name}\n${timeObj.toLocaleString()}">
                        <iconify-icon icon="carbon:document" width="14" class="flex-shrink-0 text-slate-400 group-hover:text-slate-500 transition-colors"></iconify-icon>
                        <span class="truncate flex-1">${name}</span>
                        <span class="text-[10px] text-slate-400 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">${timeStr}</span>
                    </div>
                `;
            }).join('');

        } catch (e) {
            console.error('Failed to render sidebar history:', e);
            quickListEl.innerHTML = '<div class="px-3 py-2 text-xs text-red-400 text-center">加载失败</div>';
        }
    }

    function initSidebarHistory() {
        const mainBtn = document.getElementById('sidebarHistoryMainBtn');
        const toggleBtn = document.getElementById('sidebarHistoryToggleBtn');
        const quickList = document.getElementById('sidebarHistoryQuickList');
        const chevron = document.getElementById('sidebarHistoryChevron');

        // 左侧主按钮：直接打开完整历史面板
        if (mainBtn) {
            mainBtn.addEventListener('click', openHistoryPanel);
        }

        // 右侧切换按钮：展开/收起快捷列表
        if (toggleBtn && quickList) {
            toggleBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                // 当前是否隐藏
                const isHidden = quickList.classList.contains('hidden');
                // 切换显示状态
                quickList.classList.toggle('hidden', !isHidden);

                // 更新图标方向：展开时旋转90度向下
                if (chevron) {
                    chevron.classList.toggle('rotate-90', isHidden);
                }

                // 保存展开状态到 localStorage
                localStorage.setItem('pbx_history_expanded', isHidden ? 'true' : 'false');

                // 如果是展开操作，刷新数据
                if (isHidden) {
                    renderSidebarQuickAccess();
                }
            });
        }

        // 从 localStorage 恢复历史记录展开状态
        const isExpanded = localStorage.getItem('pbx_history_expanded') === 'true';
        if (isExpanded && quickList && chevron) {
            quickList.classList.remove('hidden');
            chevron.classList.add('rotate-90');
            renderSidebarQuickAccess();
        } else {
            // 初始加载数据（保持折叠状态）
            renderSidebarQuickAccess();
        }

        // 暴露给全局以便其他模块调用刷新
        window.refreshSidebarHistory = renderSidebarQuickAccess;
    }

    // 显示历史面板并渲染历史列表
    async function openHistoryPanel() {
        const panel = document.getElementById('historyPanel');
        if (panel) panel.classList.remove('hidden');
        await renderHistoryList();
    }

    // 初始化侧边栏历史
    initSidebarHistory();

    const sidebarHistoryBtn = document.getElementById('sidebarHistoryBtn');
    if (sidebarHistoryBtn) {
        sidebarHistoryBtn.addEventListener('click', openHistoryPanel);
    }
    const mobileHistoryBtn = document.getElementById('mobileHistoryBtn');
    if (mobileHistoryBtn) {
        mobileHistoryBtn.addEventListener('click', openHistoryPanel);
    }
    // 悬浮历史记录按钮
    const floatingHistoryBtn = document.getElementById('floatingHistoryBtn');
    if (floatingHistoryBtn) {
        floatingHistoryBtn.addEventListener('click', openHistoryPanel);
    }
    // 关闭历史面板
    document.getElementById('closeHistoryPanel').onclick = function() {
        document.getElementById('historyPanel').classList.add('hidden');
    };
    const clearHistoryBtn = document.getElementById('clearHistoryBtn');

    const historyClearModal = document.getElementById('historyClearConfirmModal');
    const historyClearStep1 = document.getElementById('historyClearStep1');
    const historyClearStep1Message = document.getElementById('historyClearStep1Message');
    const historyClearStep2 = document.getElementById('historyClearStep2');
    const historyClearStep3 = document.getElementById('historyClearStep3');
    const historyClearFinalMessage = document.getElementById('historyClearFinalMessage');
    const historyClearPhraseInput = document.getElementById('historyClearPhraseInput');
    const historyClearCancelBtn = document.getElementById('historyClearCancelBtn');
    const historyClearCloseBtn = document.getElementById('historyClearCloseBtn');
    const historyClearStep1Next = document.getElementById('historyClearStep1Next');
    const historyClearStep2Next = document.getElementById('historyClearStep2Next');
    const historyClearStep2Back = document.getElementById('historyClearStep2Back');
    const historyClearStep3Back = document.getElementById('historyClearStep3Back');
    const historyClearExecute = document.getElementById('historyClearExecute');

    let currentClearStep = 1;
    let historyClearMode = 'all';
    let pendingDeleteRecordId = null;
    let pendingDeleteRecordName = '';

    function updateHistoryClearContent() {
        if (historyClearStep1Message) {
            if (historyClearMode === 'record') {
                const displayName = pendingDeleteRecordName || '选中的记录';
                historyClearStep1Message.textContent = `即将删除历史记录“${displayName}”。请确认是否继续。`;
            } else {
                historyClearStep1Message.textContent = '即将永久删除所有历史记录（包括所有批次、译文、文件夹分配）。请确认是否继续。';
            }
        }
        if (historyClearFinalMessage) {
            historyClearFinalMessage.textContent = historyClearMode === 'record'
                ? '历史记录删除后将无法恢复。请确保已备份需要的数据。'
                : '历史记录一旦清空，将无法恢复。请确保已备份需要的数据。';
        }
        if (historyClearExecute) {
            historyClearExecute.textContent = historyClearMode === 'record' ? '删除记录' : '永久删除';
        }
    }

    function setHistoryClearStep(step) {
        currentClearStep = step;
        const stepMap = { 1: historyClearStep1, 2: historyClearStep2, 3: historyClearStep3 };
        Object.entries(stepMap).forEach(([key, el]) => {
            if (!el) return;
            const isActive = Number(key) === step;
            if (isActive) {
                el.classList.remove('hidden');
                el.removeAttribute('hidden');
            } else {
                el.classList.add('hidden');
                el.setAttribute('hidden', '');
            }
            el.setAttribute('aria-hidden', String(!isActive));
        });

        if (step === 1) {
            resetStep2State();
        }
        if (step === 2 && historyClearPhraseInput) {
            setTimeout(() => historyClearPhraseInput.focus(), 0);
        }
    }

    function resetStep2State() {
        if (historyClearPhraseInput) {
            historyClearPhraseInput.value = '';
        }
        if (historyClearStep2Next) {
            historyClearStep2Next.disabled = true;
            historyClearStep2Next.classList.add('opacity-60', 'cursor-not-allowed');
        }
    }

    function openHistoryClearModal(mode = 'all', { recordId = null, recordName = '' } = {}) {
        historyClearMode = mode === 'record' ? 'record' : 'all';
        pendingDeleteRecordId = historyClearMode === 'record' ? recordId : null;
        pendingDeleteRecordName = historyClearMode === 'record' ? (recordName || '') : '';
        updateHistoryClearContent();
        if (!historyClearModal) {
            const targetLabel = pendingDeleteRecordName || '选中的记录';
            const fallbackConfirm = historyClearMode === 'record'
                ? `确定要删除历史记录“${targetLabel}”吗？此操作无法恢复。`
                : '确定要清空所有历史记录吗？此操作无法恢复。';
            const confirmed = confirm(fallbackConfirm);
            if (!confirmed) return;
            performClearHistory();
            return;
        }
        resetStep2State();
        setHistoryClearStep(1);
        historyClearModal.classList.remove('hidden');
        historyClearModal.classList.add('flex');
    }

    function closeHistoryClearModal() {
        if (!historyClearModal) return;
        historyClearModal.classList.add('hidden');
        historyClearModal.classList.remove('flex');
        resetStep2State();
        setHistoryClearStep(1);
        historyClearMode = 'all';
        pendingDeleteRecordId = null;
        pendingDeleteRecordName = '';
    }

    async function performClearHistory() {
        if (historyClearMode === 'record') {
            if (pendingDeleteRecordId) {
                removeFolderAssignmentForRecord(pendingDeleteRecordId);
                await deleteResultFromDB(pendingDeleteRecordId);
                await renderHistoryList();
                if (typeof showNotification === 'function') {
                    showNotification('历史记录已删除。', 'success');
                }
            }
        } else {
            await clearAllResultsFromDB();
            clearFolderAssignments();
            historyUIState.activeFolder = 'all';
            historyUIState.searchQuery = '';
            historyUIState.batchSearch = {};
            historyUIState.batchSearchDraft = {};
            await renderHistoryList();
            if (typeof showNotification === 'function') {
                showNotification('历史记录已全部清空。', 'success');
            }
        }
        historyClearMode = 'all';
        pendingDeleteRecordId = null;
        pendingDeleteRecordName = '';
    }

    if (clearHistoryBtn) {
        clearHistoryBtn.onclick = function() {
            openHistoryClearModal('all');
        };
    }

    if (historyClearModal) {
        historyClearModal.addEventListener('click', function(event) {
            if (event.target === historyClearModal) {
                closeHistoryClearModal();
            }
        });
    }

    if (historyClearCancelBtn) {
        historyClearCancelBtn.addEventListener('click', function() {
            closeHistoryClearModal();
        });
    }
    if (historyClearCloseBtn) {
        historyClearCloseBtn.addEventListener('click', function() {
            closeHistoryClearModal();
        });
    }
    if (historyClearStep1Next) {
        historyClearStep1Next.addEventListener('click', function() {
            setHistoryClearStep(2);
        });
    }
    if (historyClearStep2Back) {
        historyClearStep2Back.addEventListener('click', function() {
            setHistoryClearStep(1);
        });
    }
    if (historyClearStep3Back) {
        historyClearStep3Back.addEventListener('click', function() {
            setHistoryClearStep(2);
        });
    }
    if (historyClearStep2Next) {
        historyClearStep2Next.addEventListener('click', function() {
            if (historyClearStep2Next.disabled) return;
            setHistoryClearStep(3);
        });
    }
    if (historyClearPhraseInput) {
        historyClearPhraseInput.addEventListener('input', function(event) {
            if (!historyClearStep2Next) return;
            const matches = (event.target.value || '').trim() === REQUIRED_CLEAR_PHRASE;
            historyClearStep2Next.disabled = !matches;
            historyClearStep2Next.classList.toggle('opacity-60', !matches);
            historyClearStep2Next.classList.toggle('cursor-not-allowed', !matches);
        });
    }
    if (historyClearExecute) {
        historyClearExecute.addEventListener('click', async function() {
            historyClearExecute.disabled = true;
            historyClearExecute.classList.add('opacity-70');
            try {
                await performClearHistory();
                closeHistoryClearModal();
            } finally {
                historyClearExecute.disabled = false;
                historyClearExecute.classList.remove('opacity-70');
            }
        });
    }

    const HISTORY_FOLDER_STORAGE_KEY = 'pbxHistoryFolders';
    const HISTORY_FOLDER_ASSIGNMENT_KEY = 'pbxHistoryFolderAssignments';
    const MAX_HISTORY_FOLDER_NAME = 40;
    const historyUIState = {
        activeFolder: 'all',
        searchQuery: '',
        batchSearch: {},
        batchSearchDraft: {}
    };

    let currentFolderAssignments = {};
    let currentFolderOptions = [];
    let currentUserFolderMap = new Map();

    const historySearchInput = document.getElementById('historySearchInput');
    const historyFolderSelectMobile = document.getElementById('historyFolderSelectMobile');

    // 性能优化：防抖函数（减少频繁的渲染调用）
    function debounce(fn, delay) {
        let timer = null;
        return function debounced(...args) {
            const context = this;
            if (timer) clearTimeout(timer);
            timer = setTimeout(() => {
                timer = null;
                fn.apply(context, args);
            }, delay);
        };
    }

    // 创建防抖版本的渲染函数（300ms 延迟）
    const debouncedRenderHistoryList = debounce(function() {
        renderHistoryList();
    }, 300);
    if (historySearchInput) {
        historySearchInput.addEventListener('input', function(event) {
            historyUIState.searchQuery = event.target.value || '';
            debouncedRenderHistoryList();  // 使用防抖版本，减少渲染次数
        });
    }
    if (historyFolderSelectMobile) {
        historyFolderSelectMobile.addEventListener('change', function(event) {
            const nextFolder = (event.target.value || 'all');
            historyUIState.activeFolder = nextFolder;
            renderHistoryList();
        });
    }

    const historyFolderListElement = document.getElementById('historyFolderList');
    if (historyFolderListElement) {
        historyFolderListElement.addEventListener('click', handleHistoryFolderAction);
    }

    const historyAddFolderBtn = document.getElementById('historyAddFolderBtn');
    const historyAddFolderBtnMobile = document.getElementById('historyAddFolderBtnMobile');

    function handleCreateFolder() {
        const name = prompt('请输入新的文件夹名称');
        if (name == null) return;
        const trimmed = name.trim();
        if (!trimmed) {
            showNotification && showNotification('文件夹名称不能为空。', 'warning');
            return;
        }
        if (trimmed.length > MAX_HISTORY_FOLDER_NAME) {
            showNotification && showNotification(`文件夹名称请控制在 ${MAX_HISTORY_FOLDER_NAME} 个字符以内。`, 'warning');
            return;
        }
        const userFolders = loadUserFolders();
        if (userFolders.some(f => (f.name || '').toLowerCase() === trimmed.toLowerCase())) {
            showNotification && showNotification('已存在同名文件夹。', 'warning');
            return;
        }
        const folderId = generateFolderId();
        userFolders.push({ id: folderId, name: trimmed, createdAt: Date.now() });
        saveUserFolders(userFolders);
        historyUIState.activeFolder = folderId;
        renderHistoryList();
        showNotification && showNotification(`已创建文件夹“${trimmed}”`, 'success');
    }

    if (historyAddFolderBtn) {
        historyAddFolderBtn.addEventListener('click', handleCreateFolder);
    }
    if (historyAddFolderBtnMobile) {
        historyAddFolderBtnMobile.addEventListener('click', handleCreateFolder);
    }

    const historyRenameFolderBtnMobile = document.getElementById('historyRenameFolderBtnMobile');
    const historyDeleteFolderBtnMobile = document.getElementById('historyDeleteFolderBtnMobile');
    if (historyRenameFolderBtnMobile) {
        historyRenameFolderBtnMobile.addEventListener('click', function() {
            const select = document.getElementById('historyFolderSelectMobile');
            const current = select ? (select.value || 'all') : historyUIState.activeFolder;
            if (current === 'all' || current === 'uncategorized') {
                showNotification && showNotification('系统文件夹无法执行该操作。', 'info');
                return;
            }
            renameUserFolder(current);
        });
    }
    if (historyDeleteFolderBtnMobile) {
        historyDeleteFolderBtnMobile.addEventListener('click', function() {
            const select = document.getElementById('historyFolderSelectMobile');
            const current = select ? (select.value || 'all') : historyUIState.activeFolder;
            if (current === 'all' || current === 'uncategorized') {
                showNotification && showNotification('系统文件夹无法执行该操作。', 'info');
                return;
            }
            deleteUserFolder(current);
        });
    }

    // ---------------------
    // 历史记录列表渲染
    // ---------------------
    /**
     * 从 IndexedDB 加载所有历史记录，并将其渲染到历史记录面板的列表中。
     *
     * 主要步骤:
     * 1. 获取用于显示历史列表的 DOM 元素 (`#historyList`)。
     * 2. 调用 `getAllResultsFromDB()` 从 IndexedDB 异步获取所有存储的处理结果。
     * 3. 检查结果：如果无历史记录，则在列表区域显示"暂无历史记录"的提示。
     * 4. 排序：将获取到的历史记录按处理时间 (`time` 字段) 降序排列 (最新的在前)。
     * 5. 生成 HTML：遍历排序后的结果数组，为每条记录生成一个 HTML 片段，
     *    包含文件名、删除按钮、时间戳、OCR 和翻译内容的摘要、以及"查看详情"和"下载"按钮。
     *    每个按钮都绑定了相应的 `window` 上的全局函数 (如 `deleteHistoryRecord`, `showHistoryDetail`, `downloadHistoryRecord`)。
     * 6. 更新 DOM：将生成的 HTML 字符串集合设置为 `#historyList` 的 `innerHTML`。
     *
     * @async
     * @private
     * @returns {Promise<void>} 当列表渲染完成时解决。
     */
    async function renderHistoryList() {
        const listDiv = document.getElementById('historyList');
        if (!listDiv) return;

        const previousOpenBatchIds = new Set();
        if (listDiv) {
            listDiv.querySelectorAll('details[data-batch-id]').forEach(detailEl => {
                if (detailEl && detailEl.open) {
                    const batchId = detailEl.getAttribute('data-batch-id');
                    if (batchId) {
                        previousOpenBatchIds.add(batchId);
                    }
                }
            });
        }

        if (historySearchInput && historySearchInput.value !== historyUIState.searchQuery) {
            historySearchInput.value = historyUIState.searchQuery;
        }

        const results = await getAllResultsFromDB();
        const assignments = loadFolderAssignments();
        const userFolders = loadUserFolders();

        currentFolderAssignments = assignments;
        currentUserFolderMap = new Map(userFolders.map(folder => [folder.id, folder]));
        currentFolderOptions = buildAssignableFolderOptions(currentUserFolderMap);

        renderHistoryFolders(Array.isArray(results) ? results : [], assignments, userFolders);

        if (!results || results.length === 0) {
            listDiv.innerHTML = '<div class="text-gray-400 text-center py-8">暂无历史记录</div>';
            window.__historyRecordCache = {};
            window.__historyBatchCache = {};
            historyUIState.batchSearch = {};
            historyUIState.batchSearchDraft = {};
            return;
        }

        results.sort((a, b) => new Date(b.time) - new Date(a.time));

        if (!folderExists(historyUIState.activeFolder)) {
            historyUIState.activeFolder = 'all';
        }

        const searchTerm = (historyUIState.searchQuery || '').trim().toLowerCase();
        const recordCache = {};
        const batchMap = new Map();
        const fullBatchMap = new Map();
        const singleRecords = [];

        results.forEach(record => {
            if (!record || !record.id) return;
            if (record.batchId) {
                if (!fullBatchMap.has(record.batchId)) {
                    fullBatchMap.set(record.batchId, []);
                }
                fullBatchMap.get(record.batchId).push(record);
            }

            const folderId = resolveRecordFolder(record.id, assignments);

            if (historyUIState.activeFolder === 'uncategorized' && folderId !== 'uncategorized') return;
            if (historyUIState.activeFolder !== 'all' && historyUIState.activeFolder !== 'uncategorized' && folderId !== historyUIState.activeFolder) return;

            if (searchTerm && !recordMatchesQuery(record, searchTerm)) return;

            let batchQueryLower = '';
            if (record.batchId && historyUIState.batchSearch[record.batchId]) {
                batchQueryLower = historyUIState.batchSearch[record.batchId].trim().toLowerCase();
                if (batchQueryLower && !recordMatchesQuery(record, batchQueryLower)) return;
            }

            recordCache[record.id] = record;

            if (record.batchId) {
                if (!batchMap.has(record.batchId)) {
                    batchMap.set(record.batchId, []);
                }
                batchMap.get(record.batchId).push(record);
            } else {
                singleRecords.push(record);
            }
        });

        const fragments = [];
        const visibleBatchIds = new Set();

        batchMap.forEach((group, batchId) => {
            if (!group || group.length === 0) return;
            group.sort((a, b) => {
                const orderA = typeof a.batchOrder === 'number' ? a.batchOrder : (typeof a.batchOriginalIndex === 'number' ? a.batchOriginalIndex + 1 : 0);
                const orderB = typeof b.batchOrder === 'number' ? b.batchOrder : (typeof b.batchOriginalIndex === 'number' ? b.batchOriginalIndex + 1 : 0);
                if (orderA !== orderB) return orderA - orderB;
                return new Date(a.time) - new Date(b.time);
            });
            visibleBatchIds.add(batchId);
            fragments.push(renderBatchGroupItem(batchId, group, {
                searchValue: historyUIState.batchSearch[batchId] || '',
                folderIds: group.map(item => resolveRecordFolder(item.id, assignments)),
                isOpen: previousOpenBatchIds.has(batchId)
            }));
        });

        Object.keys(historyUIState.batchSearch).forEach(batchId => {
            if (!visibleBatchIds.has(batchId)) {
                delete historyUIState.batchSearch[batchId];
            }
        });
        Object.keys(historyUIState.batchSearchDraft).forEach(batchId => {
            if (!visibleBatchIds.has(batchId)) {
                delete historyUIState.batchSearchDraft[batchId];
            }
        });

        singleRecords.forEach(record => {
            fragments.push(renderHistoryRecordItem(record));
        });

        listDiv.innerHTML = fragments.length > 0
            ? fragments.join('')
            : '<div class="text-gray-400 text-center py-8">未匹配到符合条件的历史记录</div>';

        window.__historyRecordCache = recordCache;
        const batchCache = {};
        fullBatchMap.forEach((group, batchId) => {
            if (Array.isArray(group)) {
                group.sort((a, b) => {
                    const orderA = typeof a.batchOrder === 'number' ? a.batchOrder : (typeof a.batchOriginalIndex === 'number' ? a.batchOriginalIndex + 1 : 0);
                    const orderB = typeof b.batchOrder === 'number' ? b.batchOrder : (typeof b.batchOriginalIndex === 'number' ? b.batchOriginalIndex + 1 : 0);
                    if (orderA !== orderB) return orderA - orderB;
                    return new Date(a.time) - new Date(b.time);
                });
            }
            batchCache[batchId] = group;
        });
        window.__historyBatchCache = batchCache;

        // 同步刷新侧边栏快捷入口
        if (typeof window.refreshSidebarHistory === 'function') {
            window.refreshSidebarHistory();
        }
    }

    function loadUserFolders() {
        try {
            const raw = localStorage.getItem(HISTORY_FOLDER_STORAGE_KEY);
            if (!raw) return [];
            const parsed = JSON.parse(raw);
            if (!Array.isArray(parsed)) return [];
            return parsed.filter(folder => folder && typeof folder.id === 'string' && typeof folder.name === 'string');
        } catch (error) {
            console.warn('加载历史文件夹失败:', error);
            return [];
        }
    }

    function saveUserFolders(folders) {
        try {
            const compact = Array.isArray(folders) ? folders.slice() : [];
            localStorage.setItem(HISTORY_FOLDER_STORAGE_KEY, JSON.stringify(compact));
        } catch (error) {
            console.warn('保存历史文件夹失败:', error);
        }
    }

    function loadFolderAssignments() {
        try {
            const raw = localStorage.getItem(HISTORY_FOLDER_ASSIGNMENT_KEY);
            if (!raw) return {};
            const parsed = JSON.parse(raw);
            if (parsed && typeof parsed === 'object') {
                return parsed;
            }
        } catch (error) {
            console.warn('加载历史文件夹分配失败:', error);
        }
        return {};
    }

    function saveFolderAssignments(assignments) {
        try {
            localStorage.setItem(HISTORY_FOLDER_ASSIGNMENT_KEY, JSON.stringify(assignments || {}));
        } catch (error) {
            console.warn('保存历史文件夹分配失败:', error);
        }
    }

    function clearFolderAssignments() {
        try {
            localStorage.removeItem(HISTORY_FOLDER_ASSIGNMENT_KEY);
        } catch (error) {
            console.warn('清除历史文件夹分配失败:', error);
        }
    }

    function generateFolderId() {
        return 'folder-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
    }

    function buildAssignableFolderOptions(folderMap) {
        const options = [{ id: 'uncategorized', name: '未分组' }];
        if (folderMap && typeof folderMap.forEach === 'function') {
            folderMap.forEach(folder => {
                if (folder && typeof folder.id === 'string' && typeof folder.name === 'string') {
                    options.push({ id: folder.id, name: folder.name });
                }
            });
        }
        return options;
    }

    function resolveRecordFolder(recordId, assignmentsOverride) {
        if (!recordId) return 'uncategorized';
        const assignments = assignmentsOverride || currentFolderAssignments || {};
        const assigned = assignments[recordId];
        if (!assigned || assigned === 'uncategorized') {
            return 'uncategorized';
        }
        return folderExists(assigned) ? assigned : 'uncategorized';
    }

    function folderExists(folderId) {
        if (!folderId) return false;
        if (folderId === 'all' || folderId === 'uncategorized') return true;
        if (currentUserFolderMap && currentUserFolderMap.has(folderId)) return true;
        const userFolders = loadUserFolders();
        return userFolders.some(folder => folder && folder.id === folderId);
    }

    function recordMatchesQuery(record, queryLower) {
        if (!queryLower) return true;
        const safeQuery = String(queryLower).toLowerCase();
        if (!safeQuery) return true;
        const pool = [];
        if (record.name) pool.push(record.name);
        if (record.relativePath) pool.push(record.relativePath);
        if (record.batchId) pool.push(record.batchId);
        if (record.batchTemplate) pool.push(record.batchTemplate);
        if (record.batchOutputLanguage || record.targetLanguage) pool.push(record.batchOutputLanguage || record.targetLanguage);
        if (record.ocr) pool.push(record.ocr);
        if (record.translation) pool.push(record.translation);
        if (record.file && record.file.pbxRelativePath) pool.push(record.file.pbxRelativePath);
        for (let i = 0; i < pool.length; i++) {
            const value = pool[i];
            if (typeof value === 'string' && value.toLowerCase().includes(safeQuery)) {
                return true;
            }
        }
        return false;
    }

    function renderFolderSelect({ scope, ownerId, selectedId, isMixed }) {
        const options = currentFolderOptions || [];
        const normalizedScope = scope === 'batch' ? 'batch' : 'record';
        const normalizedOwner = ownerId || '';
        const ariaLabel = normalizedScope === 'batch' ? '选择批量任务文件夹' : '选择历史记录文件夹';
        const effectiveSelected = selectedId && folderExists(selectedId) ? selectedId : 'uncategorized';
        const selectClasses = 'min-w-[120px] rounded-md border border-gray-200 bg-white px-2 py-1 text-xs text-gray-700 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-300';
        const optionHtml = options.map(folder => {
            const value = escapeAttr(folder.id);
            const isSelected = !isMixed && effectiveSelected === folder.id;
            return `<option value="${value}" ${isSelected ? 'selected' : ''}>${escapeHtml(folder.name)}</option>`;
        }).join('');
        const mixedOption = isMixed ? '<option value="" selected>（多个文件夹）</option>' : '';
        return `
            <select class="${selectClasses}" data-history-folder-select="${escapeAttr(normalizedScope)}" data-owner-id="${escapeAttr(normalizedOwner)}" aria-label="${escapeAttr(ariaLabel)}">
                ${mixedOption}
                ${optionHtml}
            </select>
        `;
    }

    function renderHistoryFolders(allRecords, assignments, userFolders) {
        const listEl = document.getElementById('historyFolderList');
        const mobileSelect = document.getElementById('historyFolderSelectMobile');
        if (!listEl && !mobileSelect) return;
        const records = Array.isArray(allRecords) ? allRecords : [];
        const counts = new Map();
        counts.set('all', records.length);

        const validFolderIds = new Set((userFolders || []).map(folder => folder.id));
        let uncategorizedCount = 0;

        records.forEach(record => {
            if (!record || !record.id) return;
            const folderId = resolveRecordFolder(record.id, assignments);
            if (!folderId || folderId === 'uncategorized' || !validFolderIds.has(folderId)) {
                uncategorizedCount++;
                return;
            }
            counts.set(folderId, (counts.get(folderId) || 0) + 1);
        });

        counts.set('uncategorized', uncategorizedCount);

        const fragments = [];
        fragments.push(renderFolderListItem({ id: 'all', name: '全部记录', count: counts.get('all') || 0, system: true }));
        fragments.push(renderFolderListItem({ id: 'uncategorized', name: '未分组', count: counts.get('uncategorized') || 0, system: true }));

        const sortedUserFolders = (userFolders || []).slice().sort((a, b) => {
            return (a.name || '').localeCompare(b.name || '', 'zh-Hans-CN');
        });

        sortedUserFolders.forEach(folder => {
            fragments.push(renderFolderListItem({
                id: folder.id,
                name: folder.name,
                count: counts.get(folder.id) || 0,
                system: false
            }));
        });

        if (listEl) {
            listEl.innerHTML = fragments.join('') || '<div class="text-xs text-gray-400 py-4 text-center">暂无文件夹</div>';
        }

        if (mobileSelect) {
            const opts = [];
            // 系统选项
            opts.push({ id: 'all', name: '全部记录', count: counts.get('all') || 0 });
            opts.push({ id: 'uncategorized', name: '未分组', count: counts.get('uncategorized') || 0 });
            // 用户文件夹（按名称排序）
            const sortedUserFolders = (userFolders || []).slice().sort((a, b) => {
                return (a.name || '').localeCompare(b.name || '', 'zh-Hans-CN');
            });
            sortedUserFolders.forEach(folder => {
                opts.push({ id: folder.id, name: folder.name, count: counts.get(folder.id) || 0 });
            });
            mobileSelect.innerHTML = opts.map(opt => {
                const selected = historyUIState.activeFolder === opt.id ? 'selected' : '';
                return `<option value="${escapeAttr(opt.id)}" ${selected}>${escapeHtml(opt.name)} (${opt.count})</option>`;
            }).join('');
        }
    }

    function renderFolderListItem({ id, name, count, system }) {
        const isActive = historyUIState.activeFolder === id;
        const baseClasses = 'group flex items-center justify-between px-2 py-1.5 rounded-lg border transition-colors';
        const activeClasses = isActive ? 'border-blue-200 bg-blue-50 text-blue-600' : 'border-transparent hover:bg-gray-100 text-gray-700';
        const title = escapeAttr(name || '未命名');
        const countBadge = `<span class="ml-2 inline-flex min-w-[1.5rem] justify-center rounded-full bg-gray-100 px-1.5 py-0.5 text-[11px] font-medium text-gray-500">${typeof count === 'number' ? count : 0}</span>`;
        const selectButton = `
            <button type="button" class="flex-1 text-left text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300 focus-visible:ring-offset-1" data-folder-action="select" data-folder-id="${escapeAttr(id)}" title="查看${title}">
                <span>${escapeHtml(name || '未命名')}</span>
                ${countBadge}
            </button>`;
        const actionButtons = system ? '' : `
            <div class="ml-2 flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
                <button type="button" class="rounded p-1 text-gray-400 hover:text-blue-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-300" data-folder-action="rename" data-folder-id="${escapeAttr(id)}" title="重命名">
                    <iconify-icon icon="carbon:edit" width="14"></iconify-icon>
                </button>
                <button type="button" class="rounded p-1 text-gray-400 hover:text-red-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-300" data-folder-action="delete" data-folder-id="${escapeAttr(id)}" title="删除">
                    <iconify-icon icon="carbon:trash-can" width="14"></iconify-icon>
                </button>
            </div>`;
        return `<div class="${baseClasses} ${activeClasses}">${selectButton}${actionButtons}</div>`;
    }

    function handleHistoryFolderAction(event) {
        const actionEl = event.target.closest('[data-folder-action]');
        if (!actionEl) return;
        const action = actionEl.getAttribute('data-folder-action');
        const folderId = actionEl.getAttribute('data-folder-id');
        if (!action || !folderId) return;
        event.preventDefault();

        if (action === 'select') {
            if (historyUIState.activeFolder !== folderId) {
                historyUIState.activeFolder = folderId;
                renderHistoryList();
            }
            return;
        }

        if (folderId === 'all' || folderId === 'uncategorized') {
            showNotification && showNotification('系统文件夹无法执行该操作。', 'info');
            return;
        }
        if (action === 'rename') {
            renameUserFolder(folderId);
            return;
        }
        if (action === 'delete') {
            deleteUserFolder(folderId);
            return;
        }
    }

    function renameUserFolder(folderId) {
        if (!folderId || folderId === 'all' || folderId === 'uncategorized') {
            showNotification && showNotification('系统文件夹无法执行该操作。', 'info');
            return;
        }
        const userFolders = loadUserFolders();
        const target = userFolders.find(folder => folder.id === folderId);
        if (!target) {
            showNotification && showNotification('未找到目标文件夹。', 'warning');
            return;
        }
        const newName = prompt('修改文件夹名称', target.name || '');
        if (newName == null) return;
        const trimmed = newName.trim();
        if (!trimmed) {
            showNotification && showNotification('文件夹名称不能为空。', 'warning');
            return;
        }
        if (trimmed.length > MAX_HISTORY_FOLDER_NAME) {
            showNotification && showNotification(`文件夹名称请控制在 ${MAX_HISTORY_FOLDER_NAME} 个字符以内。`, 'warning');
            return;
        }
        const duplicate = userFolders.some(folder => folder.id !== folderId && (folder.name || '').toLowerCase() === trimmed.toLowerCase());
        if (duplicate) {
            showNotification && showNotification('已存在同名文件夹。', 'warning');
            return;
        }
        target.name = trimmed;
        saveUserFolders(userFolders);
        showNotification && showNotification('文件夹名称已更新。', 'success');
        renderHistoryList();
    }

    function deleteUserFolder(folderId) {
        if (!folderId || folderId === 'all' || folderId === 'uncategorized') {
            showNotification && showNotification('系统文件夹无法执行该操作。', 'info');
            return;
        }
        const userFolders = loadUserFolders();
        const target = userFolders.find(folder => folder.id === folderId);
        if (!target) {
            showNotification && showNotification('未找到目标文件夹。', 'warning');
            return;
        }
        if (!confirm(`确定要删除文件夹“${target.name}”吗？文件夹内的记录将回到“未分组”。`)) {
            return;
        }
        const updatedFolders = userFolders.filter(folder => folder.id !== folderId);
        saveUserFolders(updatedFolders);
        const assignments = loadFolderAssignments();
        let modified = false;
        Object.keys(assignments).forEach(recordId => {
            if (assignments[recordId] === folderId) {
                delete assignments[recordId];
                modified = true;
            }
        });
        if (modified) {
            saveFolderAssignments(assignments);
        }
        if (historyUIState.activeFolder === folderId) {
            historyUIState.activeFolder = 'all';
        }
        showNotification && showNotification('文件夹已删除。', 'info');
        renderHistoryList();
    }

    function assignRecordToFolder(recordId, folderId) {
        if (!recordId) return;
        const assignments = loadFolderAssignments();
        const normalized = folderExists(folderId) && folderId !== 'all' ? folderId : 'uncategorized';
        if (normalized === 'uncategorized') {
            if (assignments[recordId]) {
                delete assignments[recordId];
                saveFolderAssignments(assignments);
            }
        } else {
            assignments[recordId] = normalized;
            saveFolderAssignments(assignments);
        }
    }

    function assignBatchToFolder(batchId, folderId) {
        if (!batchId) return;
        const cache = window.__historyBatchCache || {};
        const records = cache[batchId] || [];
        if (!records.length) return;
        const assignments = loadFolderAssignments();
        const normalized = folderExists(folderId) && folderId !== 'all' ? folderId : 'uncategorized';
        let modified = false;
        records.forEach(record => {
            if (!record || !record.id) return;
            if (normalized === 'uncategorized') {
                if (assignments[record.id]) {
                    delete assignments[record.id];
                    modified = true;
                }
            } else if (assignments[record.id] !== normalized) {
                assignments[record.id] = normalized;
                modified = true;
            }
        });
        if (modified) {
            saveFolderAssignments(assignments);
        }
    }

    function removeFolderAssignmentForRecord(recordId) {
        if (!recordId) return;
        const assignments = loadFolderAssignments();
        if (assignments && assignments[recordId]) {
            delete assignments[recordId];
            saveFolderAssignments(assignments);
        }
    }

    function handleHistoryListInput(event) {
        const target = event.target;
        if (!target) return;
        if (target.hasAttribute('data-history-batch-search-input')) {
            const batchId = target.getAttribute('data-batch-id');
            if (!batchId) return;
            historyUIState.batchSearchDraft[batchId] = target.value || '';
        }
    }

    function handleHistoryListKeydown(event) {
        if (event.key !== 'Enter') return;
        const target = event.target;
        if (!target || !target.hasAttribute('data-history-batch-search-input')) return;
        event.preventDefault();
        const batchId = target.getAttribute('data-batch-id');
        if (!batchId) return;
        const value = target.value || '';
        historyUIState.batchSearchDraft[batchId] = value;
        const normalized = value.trim();
        if (normalized) {
            historyUIState.batchSearch[batchId] = normalized;
            historyUIState.batchSearchDraft[batchId] = normalized;
        } else {
            delete historyUIState.batchSearch[batchId];
            delete historyUIState.batchSearchDraft[batchId];
        }
        renderHistoryList();
    }

    const DEFAULT_EXPORT_TEMPLATE = '{original_name}_{output_language}_{processing_time:YYYYMMDD-HHmmss}.{original_type}';
    const DEFAULT_EXPORT_FORMATS = ['original', 'markdown'];
    const SUPPORTED_EXPORT_FORMATS = ['original', 'markdown', 'html', 'docx'];
    const TEXTUAL_ORIGINAL_EXTENSIONS = new Set(['txt', 'md', 'markdown', 'yaml', 'yml', 'json', 'csv', 'ini', 'cfg', 'log', 'tex', 'html', 'htm']);
    const PACKAGING_OPTIONS = {
        preserve: 'preserve',
        flat: 'flat'
    };
    const ICON_BUTTON_CLASS = 'inline-flex items-center justify-center w-9 h-9 rounded-full border border-slate-200 bg-white text-gray-500 hover:text-blue-600 hover:border-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-1';
    const ICON_BUTTON_DANGER_EXTRA = 'hover:text-red-500 hover:border-red-200 focus:ring-red-300';
    const ICON_BUTTON_SUCCESS_EXTRA = 'hover:text-emerald-500 hover:border-emerald-200 focus:ring-emerald-300';

    const historyListElement = document.getElementById('historyList');
    if (historyListElement) {
        historyListElement.addEventListener('click', handleHistoryListAction);
        historyListElement.addEventListener('change', handleHistoryListChange);
        historyListElement.addEventListener('input', handleHistoryListInput);
        historyListElement.addEventListener('keydown', handleHistoryListKeydown);
    }

    function renderBatchGroupItem(batchId, records, options = {}) {
        const safeBatchId = sanitizeId(batchId || 'batch');
        const representative = records[0] || {};
        const summaryName = representative.name || batchId || '批量任务';
        const timeLabel = formatDisplayTime(representative.time);
        const targetLang = representative.batchOutputLanguage || representative.targetLanguage || '';
        const template = representative.batchTemplate || DEFAULT_EXPORT_TEMPLATE;
        const rawBatchFormats = Array.isArray(representative.batchFormats) && representative.batchFormats.length > 0
            ? Array.from(new Set(['original', ...representative.batchFormats]))
            : DEFAULT_EXPORT_FORMATS;
        let formats = rawBatchFormats.filter(fmt => SUPPORTED_EXPORT_FORMATS.includes(fmt));
        if (formats.length === 0) {
            formats = [...DEFAULT_EXPORT_FORMATS];
        }
        const zipEnabled = typeof representative.batchZip === 'boolean' ? representative.batchZip : false;
        const structure = representative.batchZipStructure || PACKAGING_OPTIONS.preserve;

        const childrenHtml = records.map(record => renderHistoryRecordItem(record, { withinBatch: true, batchId })).join('');
        const configId = `batch-export-config-${safeBatchId}`;
        const activeSearchValue = typeof options.searchValue === 'string' ? options.searchValue : '';
        const hasDraftValue = Object.prototype.hasOwnProperty.call(historyUIState.batchSearchDraft, batchId);
        const draftValueRaw = hasDraftValue ? historyUIState.batchSearchDraft[batchId] : activeSearchValue;
        const draftValue = typeof draftValueRaw === 'string' ? draftValueRaw : '';
        const folderIds = Array.isArray(options.folderIds) ? options.folderIds.filter(Boolean) : [];
        const firstFolderId = folderIds.length > 0 ? folderIds[0] : resolveRecordFolder(representative.id, currentFolderAssignments);
        const isMixedFolder = folderIds.length > 0 ? !folderIds.every(id => id === firstFolderId) : false;
        const folderSelectHtml = renderFolderSelect({
            scope: 'batch',
            ownerId: batchId,
            selectedId: isMixedFolder ? null : firstFolderId,
            isMixed: isMixedFolder
        });
        const isOpen = !!options.isOpen;
        const openAttr = isOpen ? ' open' : '';
        const batchSearchInput = `
            <div class="relative w-full max-w-xs">
                <iconify-icon icon="carbon:search" class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" width="16"></iconify-icon>
                <input type="search" value="${escapeAttr(draftValue)}" placeholder="搜索此批量任务" data-history-batch-search-input data-batch-id="${escapeAttr(batchId)}" class="w-full rounded-md border border-gray-200 bg-white pl-8 pr-3 py-1.5 text-xs text-gray-700 focus:border-blue-400 focus:outline-none focus:ring-1 focus:ring-blue-300" autocomplete="off">
            </div>`;
        const batchExportBtn = `
            <button type="button" class="${ICON_BUTTON_CLASS}" data-history-action="open-batch-export" data-batch-id="${escapeAttr(batchId)}" data-target="${configId}" aria-label="配置导出" title="配置导出">
                <iconify-icon icon="carbon:share" width="18"></iconify-icon>
            </button>`;
        const batchDeleteBtn = `
            <button type="button" class="${ICON_BUTTON_CLASS} ${ICON_BUTTON_DANGER_EXTRA}" data-history-action="delete-batch" data-batch-id="${escapeAttr(batchId)}" aria-label="删除批量任务" title="删除批量任务">
                <iconify-icon icon="carbon:trash-can" width="18"></iconify-icon>
            </button>`;
        const batchSearchApplyBtn = `
            <button type="button" class="${ICON_BUTTON_CLASS}" data-history-action="apply-batch-search" data-batch-id="${escapeAttr(batchId)}" aria-label="应用搜索" title="应用搜索">
                <iconify-icon icon="carbon:search" width="18"></iconify-icon>
            </button>`;
        const batchSearchControls = `
            <div class="flex items-center gap-2">
                ${batchSearchInput}
                ${batchSearchApplyBtn}
            </div>`;
        const batchHeaderTools = `
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between pt-3">
                <div class="flex items-center gap-2 text-xs text-gray-600">
                    <span class="font-medium text-gray-600">文件夹</span>
                    ${folderSelectHtml}
                </div>
                ${batchSearchControls}
            </div>`;
        const batchChildrenHtml = childrenHtml || (activeSearchValue.trim()
            ? '<div class="text-xs text-gray-500">未找到符合搜索条件的记录。</div>'
            : '<div class="text-xs text-gray-500">暂无记录</div>');

        return `
        <details class="history-batch-group border border-slate-200 bg-white rounded-xl shadow-sm mb-4 overflow-hidden hover:border-blue-200 hover:shadow-md transition" data-batch-id="${escapeAttr(batchId)}"${openAttr}>
            <summary class="cursor-pointer select-none px-4 py-3 bg-slate-50">
                <div class="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
                    <span class="text-sm font-semibold text-gray-800 flex flex-wrap items-center gap-2">
                        <span>批量任务</span>
                        <span class="text-blue-600">${escapeHtml(summaryName)}</span>
                        <span class="text-[11px] bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full">${records.length} 个文件</span>
                    </span>
                    <span class="text-xs text-gray-500">${timeLabel}${targetLang ? ` · 语言：${escapeHtml(targetLang)}` : ''}</span>
                </div>
                <div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-600 md:text-sm">
                    ${batchExportBtn}
                    ${batchDeleteBtn}
                </div>
            </summary>
            <div class="px-4 pb-4 space-y-3 bg-white">
                ${batchHeaderTools}
                ${renderExportConfigPanel({
                    id: configId,
                    scope: 'batch',
                    ownerId: batchId,
                    template,
                    formats,
                    zipEnabled,
                    structure,
                    withinBatch: true
                })}
                ${batchChildrenHtml}
            </div>
        </details>
        `;
    }

    function renderHistoryRecordItem(record, options = {}) {
        const safeId = sanitizeId(record.id || 'record');
        const withinBatch = !!options.withinBatch;
        const status = analyzeRecordStatus(record);
        const statusBadge = buildStatusBadge(status);
        const ocrSnippet = buildSnippetText(record.ocr);
        const translationSnippet = buildSnippetText(record.translation);
        const timeLabel = formatDisplayTime(record.time);
        // 新增：模型信息（兼容旧记录无该字段情况）
        const ocrEngine = (record.ocrEngine || '').toLowerCase();
        const ocrLabel = ocrEngine === 'mistral' ? 'Mistral OCR' : ocrEngine === 'mineru' ? 'MinerU OCR' : ocrEngine === 'doc2x' ? 'Doc2X OCR' : '';
        const transName = record.translationModelName || 'none';
        let transLabel = '';
        if (transName === 'none') {
            transLabel = '未翻译';
        } else if (transName === 'custom') {
            // 显示自定义源站配置的“模型ID”（优先），若缺失则退回显示名称/自定义
            transLabel = record.translationModelId
                ? escapeHtml(record.translationModelId)
                : (record.translationModelCustomName ? escapeHtml(record.translationModelCustomName) : '自定义');
        } else {
            // 预设模型
            const mapping = { deepseek: 'DeepSeek', gemini: 'Gemini', tongyi: '通义百炼', volcano: '火山引擎', deeplx: 'DeepLX' };
            transLabel = mapping[transName] || transName;
        }
        const targetLang = record.batchOutputLanguage || record.targetLanguage || '';
        const relativePathLabel = buildRelativePathLabel(record);
        const template = record.batchTemplate || DEFAULT_EXPORT_TEMPLATE;
        const rawFormats = Array.isArray(record.batchFormats) && record.batchFormats.length > 0
            ? Array.from(new Set(['original', ...record.batchFormats]))
            : DEFAULT_EXPORT_FORMATS;
        let formats = rawFormats.filter(fmt => SUPPORTED_EXPORT_FORMATS.includes(fmt));
        if (formats.length === 0) {
            formats = [...DEFAULT_EXPORT_FORMATS];
        }
        const zipEnabled = typeof record.batchZip === 'boolean' ? record.batchZip : false;
        const structure = record.batchZipStructure || PACKAGING_OPTIONS.preserve;
        const configId = `record-export-config-${safeId}${options.batchId ? `-${sanitizeId(options.batchId)}` : ''}`;
        const retryDisabled = status.failed === 0 ? 'disabled opacity-50 cursor-not-allowed' : '';
        const folderId = resolveRecordFolder(record.id, currentFolderAssignments);
        const folderSelectHtml = renderFolderSelect({
            scope: 'record',
            ownerId: record.id,
            selectedId: folderId,
            isMixed: false
        });
        const escapedRecordId = escapeAttr(record.id || '');
        const exportBtnHtml = `
            <button type="button" class="${ICON_BUTTON_CLASS}" data-history-action="open-record-export" data-record-id="${escapeAttr(record.id)}" data-target="${configId}" aria-label="配置导出" title="配置导出">
                <iconify-icon icon="carbon:share" width="18"></iconify-icon>
            </button>`;
        const downloadBtnHtml = `
            <button type="button" class="${ICON_BUTTON_CLASS} ${ICON_BUTTON_SUCCESS_EXTRA}" onclick="downloadHistoryRecord('${escapedRecordId}')" aria-label="下载记录" title="下载记录">
                <iconify-icon icon="carbon:download" width="18"></iconify-icon>
            </button>`;
        const recordDisplayName = record.name || relativePathLabel || record.id || '历史记录';
        const escapedRecordNameAttr = escapeAttr(recordDisplayName);
        const deleteBtnHtml = withinBatch ? '' : `
            <button type="button" class="${ICON_BUTTON_CLASS} ${ICON_BUTTON_DANGER_EXTRA}" data-history-action="delete-record" data-record-id="${escapedRecordId}" data-record-name="${escapedRecordNameAttr}" aria-label="删除记录" title="删除记录">
                <iconify-icon icon="carbon:trash-can" width="18"></iconify-icon>
            </button>`;
        const startReadingBtnHtml = `
            <button type="button" class="inline-flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1" onclick="showHistoryDetail('${escapedRecordId}')">
                <iconify-icon icon="carbon:document-view" width="18"></iconify-icon>
                <span>开始阅读</span>
            </button>`;

        const containerClasses = withinBatch
            ? 'border border-slate-200 rounded-xl p-3 bg-white shadow-sm hover:border-blue-200 transition'
            : 'border border-slate-200 rounded-xl p-4 bg-white shadow-sm hover:border-blue-200 hover:shadow-md transition';

        return `
        <div class="${containerClasses}" id="history-item-${safeId}" data-record-id="${escapeAttr(record.id)}">
            <div class="flex flex-col gap-1">
                <div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between md:gap-4">
                    <div class="min-w-0">
                        <div class="text-sm font-semibold text-gray-800 flex flex-wrap items-center gap-2 break-all">
                            <span>${escapeHtml(record.name || '未命名')}</span> ${statusBadge}
                        </div>
                        <div class="text-xs text-gray-500 mt-1">
                            ${timeLabel}${targetLang ? ` · 语言：${escapeHtml(targetLang)}` : ''}
                        </div>
                        ${(ocrLabel || (transLabel && transLabel !== '未翻译')) ? `
                        <div class="text-xs text-gray-500">
                            ${ocrLabel ? `OCR：${ocrLabel}` : ''}
                            ${(ocrLabel && (transLabel && transLabel !== '未翻译')) ? ' · ' : ''}
                            ${(transLabel && transLabel !== '未翻译') ? `翻译：${escapeHtml(transLabel)}` : ''}
                        </div>
                        ` : ''}
                        <div class="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-600">
                            <span class="font-medium text-gray-600">文件夹</span>
                            ${folderSelectHtml}
                        </div>
                    </div>
                    <div class="hidden md:flex flex-wrap gap-2 text-xs text-gray-600 justify-end md:text-sm items-center">
                        ${exportBtnHtml}
                        ${startReadingBtnHtml}
                        ${downloadBtnHtml}
                        ${deleteBtnHtml}
                    </div>
                </div>
                <div class="mt-2 md:hidden">
                    <details class="group">
                        <summary class="inline-flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-md bg-white text-gray-700 cursor-pointer select-none">
                            <iconify-icon icon="carbon:overflow-menu-horizontal" width="18"></iconify-icon>
                            <span class="text-sm">操作</span>
                        </summary>
                        <div class="mt-2 flex flex-wrap gap-2 text-xs text-gray-600">
                            ${exportBtnHtml}
                            ${startReadingBtnHtml}
                            ${downloadBtnHtml}
                            ${deleteBtnHtml}
                        </div>
                    </details>
                </div>
                <div class="text-xs text-gray-600 break-words">OCR：${ocrSnippet}</div>
                <div class="text-xs text-gray-600 break-words">翻译：${translationSnippet}</div>
                <div class="flex flex-wrap items-center gap-2 text-xs text-gray-600 mt-2">
                    <button id="retry-failed-btn-${safeId}" onclick="retryTranslateRecord('${record.id}','failed')" class="px-2 py-1 border border-gray-200 rounded hover:bg-gray-100 ${retryDisabled}">重试失败段</button>
                    <button id="retry-all-btn-${safeId}" onclick="retryTranslateRecord('${record.id}','all')" class="px-2 py-1 border border-gray-200 rounded hover:bg-gray-100">重新翻译全部</button>
                    <span id="retry-status-${safeId}" class="text-xs text-gray-500"></span>
                </div>
                ${renderExportConfigPanel({
                    id: configId,
                    scope: 'record',
                    ownerId: record.id,
                    template,
                    formats,
                    zipEnabled,
                    structure,
                    withinBatch
                })}
            </div>
        </div>
        `;
    }

    function renderExportConfigPanel({ id, scope, ownerId, template, formats, zipEnabled, structure, withinBatch }) {
        const formatOptions = SUPPORTED_EXPORT_FORMATS.map(fmt => {
            const checked = formats.includes(fmt) ? 'checked' : '';
            const label = fmt === 'original' ? '原格式' : fmt.toUpperCase();
            return `<label class="inline-flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-full bg-white shadow-sm"><input type="checkbox" value="${fmt}" ${checked} data-config-format="${fmt}" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"><span>${label}</span></label>`;
        }).join('');

        const sectionClasses = withinBatch ? 'mt-2 hidden border border-dashed border-blue-200 bg-white rounded-lg p-3' : 'mt-3 hidden border border-dashed border-gray-200 bg-gray-50 rounded-lg p-3';
        const structureValue = structure && PACKAGING_OPTIONS[structure] ? structure : PACKAGING_OPTIONS.preserve;
        const preserveChecked = structureValue === PACKAGING_OPTIONS.preserve ? 'checked' : '';
        const flatChecked = structureValue === PACKAGING_OPTIONS.flat ? 'checked' : '';

        const zipCheckedAttr = (zipEnabled || structureValue === PACKAGING_OPTIONS.flat) ? 'checked' : '';
        const zipDisabledAttr = structureValue === PACKAGING_OPTIONS.flat ? 'disabled' : '';

        return `
        <div id="${id}" class="${sectionClasses}" data-config-scope="${scope}" data-owner-id="${escapeAttr(ownerId)}">
            <label class="block text-xs text-gray-600 mb-2">
                命名模板
                <input type="text" class="mt-1 w-full border border-gray-200 rounded px-2 py-1 text-sm focus:ring-1 focus:ring-blue-400 focus:border-blue-400" value="${escapeAttr(template || DEFAULT_EXPORT_TEMPLATE)}" data-config-template>
            </label>
            <div class="flex flex-wrap items-center gap-3 text-xs text-gray-700" data-config-formats>
                ${formatOptions}
            </div>
            <div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-gray-600" data-config-structure-group>
                <span class="font-medium text-gray-600">ZIP 结构</span>
                <label class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-gray-200 bg-white">
                    <input type="radio" name="pack-structure-${id}" value="preserve" ${preserveChecked} data-config-structure>
                    <span>保留原始目录结构</span>
                </label>
                <label class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-gray-200 bg-white">
                    <input type="radio" name="pack-structure-${id}" value="flat" ${flatChecked} data-config-structure>
                    <span>原文/译文分组，不保留目录</span>
                </label>
                <span class="hidden h-4 w-px bg-gray-200 sm:block"></span>
                <label class="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-gray-200 bg-white ${zipDisabledAttr ? 'opacity-60 cursor-not-allowed' : ''}">
                    <input type="checkbox" ${zipCheckedAttr} ${zipDisabledAttr} data-config-zip class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
                    <span>导出为 ZIP（某些结构将自动启用）</span>
                </label>
            </div>
            <div class="mt-3 flex items-center gap-2">
                <button type="button" class="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700" data-history-action="confirm-${scope}-export" data-owner-id="${escapeAttr(ownerId)}" data-target="${id}">确认导出</button>
                <button type="button" class="px-3 py-1 border border-gray-200 rounded text-gray-600 hover:bg-gray-100" data-history-action="cancel-config" data-target="${id}">取消</button>
            </div>
        </div>
        `;
    }

    function analyzeRecordStatus(record) {
        // 标准分块统计
        const ocrChunks = Array.isArray(record.ocrChunks) ? record.ocrChunks : [];
        const translatedChunks = Array.isArray(record.translatedChunks) ? record.translatedChunks : [];
        if (ocrChunks.length > 0) {
            const total = ocrChunks.length;
            let failed = 0;
            for (let i = 0; i < total; i++) {
                const text = translatedChunks[i] || '';
                if (_isChunkFailed(text)) failed++;
            }
            const success = total - failed;
            return { total, success, failed, isStructured: false };
        }

        // 结构化翻译统计（无常规分块时）
        const meta = record && record.metadata ? record.metadata : {};
        const transList = Array.isArray(meta.translatedContentList) ? meta.translatedContentList : [];
        const supportsStructured = !!meta.supportsStructuredTranslation;
        if (supportsStructured && transList.length > 0) {
            const total = transList.length;
            // 统一的字段标准化函数（处理字符串、数组等）
            const _norm = (v) => {
                if (v == null) return '';
                try {
                    if (Array.isArray(v)) return v.join(' ').trim();
                    if (typeof v === 'string') return v.trim();
                    return String(v).trim();
                } catch(_) { return ''; }
            };

            let failed = 0;
            for (let i = 0; i < total; i++) {
                const it = transList[i];
                // 只统计真正失败的项（空译文），忽略"译文与原文相同"的项
                if (it && it.failed === true) {
                    // 如果有 failureReason 字段，只统计 'empty' 类型的失败
                    if (it.failureReason) {
                        if (it.failureReason === 'empty') {
                            failed++;
                        }
                        // failureReason === 'unchanged' 不统计为失败
                    } else {
                        // 旧数据没有 failureReason 字段，需要检查是否真的失败（空译文）
                        // 只有原文不为空且译文为空时才计入失败，译文与原文相同不算失败
                        const orig = Array.isArray(meta.contentListJson) ? meta.contentListJson[i] : null;
                        if (orig) {
                            let shouldCountAsFailed = false;
                            if (orig.type === 'text') {
                                const a = _norm(orig.text);
                                const b = _norm(it.text);
                                shouldCountAsFailed = a && !b;  // 原文不为空且译文为空才算失败
                            } else if (orig.type === 'image') {
                                const a = _norm(orig.image_caption);
                                const b = _norm(it.image_caption);
                                shouldCountAsFailed = a && !b;
                            } else if (orig.type === 'table') {
                                const a = _norm(orig.table_caption);
                                const b = _norm(it.table_caption);
                                shouldCountAsFailed = a && !b;
                            }
                            if (shouldCountAsFailed) {
                                failed++;
                            }
                        }
                        // 如果没有原文数据，不统计（无法判断）
                    }
                }
            }
            // 若元数据提供了失败项，需要过滤掉"unchanged"类型的
            if (Array.isArray(meta.failedStructuredItems) && meta.failedStructuredItems.length > 0) {
                // 检查 failedStructuredItems 中每个项，过滤掉"译文与原文相同"的项
                let actualFailed = 0;
                for (const failedItem of meta.failedStructuredItems) {
                    const idx = failedItem.index;
                    if (idx >= 0 && idx < transList.length) {
                        const item = transList[idx];
                        // 如果有 failureReason，只统计 'empty' 类型
                        if (item && item.failureReason) {
                            if (item.failureReason === 'empty') {
                                actualFailed++;
                            }
                        } else {
                            // 没有 failureReason，检查是否真的失败（空译文）
                            const orig = Array.isArray(meta.contentListJson) ? meta.contentListJson[idx] : null;
                            if (orig && item) {
                                let shouldCountAsFailed = false;
                                if (orig.type === 'text') {
                                    const a = _norm(orig.text);
                                    const b = _norm(item.text);
                                    shouldCountAsFailed = a && !b;
                                } else if (orig.type === 'image') {
                                    const a = _norm(orig.image_caption);
                                    const b = _norm(item.image_caption);
                                    shouldCountAsFailed = a && !b;
                                } else if (orig.type === 'table') {
                                    const a = _norm(orig.table_caption);
                                    const b = _norm(item.table_caption);
                                    shouldCountAsFailed = a && !b;
                                }
                                if (shouldCountAsFailed) {
                                    actualFailed++;
                                }
                            }
                            // 如果没有原文或译文数据，不统计（无法判断）
                        }
                    }
                    // 如果索引超出范围，不统计（无法判断）
                }
                failed = actualFailed;
            }
            // 回退：若未统计到失败但可对比原始内容，则尝试检测空译文（仅 text/image/table）
            // 注意：只统计译文为空的情况，译文与原文相同是正常行为
            if (failed === 0 && Array.isArray(meta.contentListJson)) {
                const origList = meta.contentListJson;
                const minLen = Math.min(origList.length, transList.length);
                for (let i = 0; i < minLen; i++) {
                    const o = origList[i] || {};
                    const t = transList[i] || {};
                    if (o.type === 'text') {
                        const a = _norm(o.text);
                        const b = _norm(t.text);
                        if (a && !b) failed++;  // 移除 a === b 判断
                    } else if (o.type === 'image') {
                        const a = _norm(o.image_caption);
                        const b = _norm(t.image_caption);
                        if (a && !b) failed++;  // 移除 a === b 判断
                    } else if (o.type === 'table') {
                        const a = _norm(o.table_caption);
                        const b = _norm(t.table_caption);
                        if (a && !b) failed++;  // 移除 a === b 判断
                    }
                }
            }
            const success = Math.max(0, total - failed);
            return { total, success, failed, isStructured: true };
        }

        return { total: 0, success: 0, failed: 0, isStructured: !!supportsStructured };
    }

    function buildStatusBadge(status) {
        if (!status || status.total === 0) {
            return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-gray-100 text-gray-500">未分块</span>';
        }
        // 若没有任何成功块（0/total），改为“预览中，无翻译块”
        if (status.success === 0) {
            // 结构化翻译下，将提示文案替换为“PDF对照”
            if (status.isStructured) {
                return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-blue-100 text-blue-700">PDF对照</span>';
            }
            return '<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-gray-100 text-gray-600">预览中，无翻译块</span>';
        }
        // 有成功也有失败 → 部分失败
        if (status.failed > 0) {
            return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-amber-100 text-amber-700">部分失败 ${status.success}/${status.total}</span>`;
        }
        // 全部成功
        return `<span class="ml-2 inline-block text-[11px] px-2 py-0.5 rounded bg-green-100 text-green-700">完成 ${status.success}/${status.total}</span>`;
    }

    function buildSnippetText(text) {
        if (!text) return '无';
        const sanitized = text.replace(/\s+/g, ' ').trim();
        return sanitized.length > 80 ? `${escapeHtml(sanitized.slice(0, 80))}…` : escapeHtml(sanitized);
    }

    function formatDisplayTime(timeValue) {
        if (!timeValue) return '未知时间';
        try {
            const date = new Date(timeValue);
            if (Number.isNaN(date.getTime())) return '未知时间';
            return date.toLocaleString();
        } catch (e) {
            return '未知时间';
        }
    }

    function buildRelativePathLabel(record) {
        const rel = record.relativePath || (record.file && record.file.pbxRelativePath) || '';
        if (!rel) return '';
        return rel;
    }

    function sanitizeId(id) {
        return String(id || '').replace(/[^a-zA-Z0-9_-]/g, '_');
    }

    function escapeHtml(str) {
        return String(str || '').replace(/[&<>"']/g, function(ch) {
            switch (ch) {
                case '&': return '&amp;';
                case '<': return '&lt;';
                case '>': return '&gt;';
                case '"': return '&quot;';
                case "'": return '&#39;';
                default: return ch;
            }
        });
    }

    function escapeAttr(str) {
        return escapeHtml(str).replace(/"/g, '&quot;');
    }

    async function handleHistoryListAction(event) {
        const actionButton = event.target.closest('[data-history-action]');
        if (!actionButton) return;

        const action = actionButton.getAttribute('data-history-action');
        const targetId = actionButton.getAttribute('data-target');

        try {
            switch (action) {
                case 'open-record-export':
                case 'open-batch-export': {
                    const panel = targetId ? document.getElementById(targetId) : null;
                    if (!panel) break;
                    if (action === 'open-batch-export') {
                        const parentDetails = actionButton.closest('details');
                        if (parentDetails) parentDetails.open = true;
                    }
                    togglePanelVisibility(panel);
                    break;
                }
                case 'delete-record': {
                    const recordId = actionButton.getAttribute('data-record-id');
                    if (!recordId) break;
                    const recordName = actionButton.getAttribute('data-record-name') || '';
                    openHistoryClearModal('record', { recordId, recordName });
                    break;
                }
                case 'apply-batch-search': {
                    const batchId = actionButton.getAttribute('data-batch-id');
                    if (!batchId) break;
                    const draftValue = Object.prototype.hasOwnProperty.call(historyUIState.batchSearchDraft, batchId)
                        ? historyUIState.batchSearchDraft[batchId]
                        : (historyUIState.batchSearch[batchId] || '');
                    const normalized = (draftValue || '').trim();
                    if (normalized) {
                        historyUIState.batchSearch[batchId] = normalized;
                        historyUIState.batchSearchDraft[batchId] = normalized;
                    } else {
                        delete historyUIState.batchSearch[batchId];
                        delete historyUIState.batchSearchDraft[batchId];
                    }
                    renderHistoryList();
                    break;
                }
                case 'cancel-config': {
                    const panel = targetId ? document.getElementById(targetId) : null;
                    if (panel) {
                        panel.classList.add('hidden');
                    }
                    break;
                }
                case 'confirm-record-export': {
                    const recordId = actionButton.getAttribute('data-owner-id');
                    if (!recordId) break;
                    const panel = targetId ? document.getElementById(targetId) : null;
                    if (!panel) break;
                    const config = collectExportConfig(panel);
                    if (config.formats.length === 0) {
                        showNotification && showNotification('请至少选择一种导出格式', 'warning');
                        break;
                    }
                    const recordCache = window.__historyRecordCache || {};
                    const record = recordCache[recordId];
                    if (!record) {
                        showNotification && showNotification('未找到历史记录数据', 'error');
                        break;
                    }
                    panel.classList.add('hidden');
                    await performHistoryExport([record], config);
                    break;
                }
                case 'confirm-batch-export': {
                    const batchId = actionButton.getAttribute('data-owner-id');
                    if (!batchId) break;
                    const panel = targetId ? document.getElementById(targetId) : null;
                    if (!panel) break;
                    const config = collectExportConfig(panel);
                    if (config.formats.length === 0) {
                        showNotification && showNotification('请至少选择一种导出格式', 'warning');
                        break;
                    }
                    const batchCache = window.__historyBatchCache || {};
                    const records = batchCache[batchId];
                    if (!records || records.length === 0) {
                        showNotification && showNotification('未找到批量任务记录', 'error');
                        break;
                    }
                    panel.classList.add('hidden');
                    await performHistoryExport(records, config, { batchId });
                    break;
                }
                case 'delete-batch': {
                    const batchId = actionButton.getAttribute('data-batch-id');
                    if (!batchId) break;
                    if (!confirm('确定要删除整个批量任务吗？此操作不可恢复。')) break;
                    await deleteBatchRecords(batchId);
                    await renderHistoryList();
                    showNotification && showNotification('批量任务已删除', 'success');
                    break;
                }
                default:
                    break;
            }
        } catch (error) {
            console.error('历史记录操作失败:', error);
            showNotification && showNotification(`导出失败：${error && error.message ? error.message : error}`, 'error');
        }
    }

    function handleHistoryListChange(event) {
        const target = event.target;
        if (!target) return;

        if (target.hasAttribute('data-history-folder-select')) {
            const scope = target.getAttribute('data-history-folder-select');
            const selectedFolder = target.value || 'uncategorized';
            if (scope === 'record') {
                const recordId = target.getAttribute('data-owner-id');
                assignRecordToFolder(recordId, selectedFolder);
                renderHistoryList();
            } else if (scope === 'batch') {
                const batchId = target.getAttribute('data-owner-id');
                assignBatchToFolder(batchId, selectedFolder);
                renderHistoryList();
            }
            return;
        }

        if (target.hasAttribute('data-config-structure')) {
            const panel = target.closest('[data-config-scope]');
            if (!panel) return;
            const zipInput = panel.querySelector('[data-config-zip]');
            if (!zipInput) return;
            if (target.value === PACKAGING_OPTIONS.flat) {
                zipInput.checked = true;
                zipInput.disabled = true;
            } else {
                zipInput.disabled = false;
            }
        }
    }

    function togglePanelVisibility(panel) {
        if (!panel) return;
        if (panel.classList.contains('hidden')) {
            // 隐藏同级已展开的配置面板
            const siblings = panel.parentElement ? panel.parentElement.querySelectorAll('[data-config-scope]') : [];
            siblings.forEach(el => { if (el !== panel) el.classList.add('hidden'); });
            panel.classList.remove('hidden');
        } else {
            panel.classList.add('hidden');
        }
    }

    function collectExportConfig(panel) {
        const templateInput = panel.querySelector('[data-config-template]');
        const formatInputs = panel.querySelectorAll('[data-config-formats] input[type="checkbox"]');
        const zipInput = panel.querySelector('[data-config-zip]');
        const structureInput = panel.querySelector('[data-config-structure]:checked');

        const template = templateInput && templateInput.value && templateInput.value.trim()
            ? templateInput.value.trim()
            : DEFAULT_EXPORT_TEMPLATE;
        const formats = Array.from(formatInputs || [])
            .filter(input => input.checked)
            .map(input => input.value)
            .filter(fmt => SUPPORTED_EXPORT_FORMATS.includes(fmt));
        if (!formats.includes('original')) {
            formats.unshift('original');
            const originalCheckbox = panel.querySelector('[data-config-formats] input[value="original"]');
            if (originalCheckbox) originalCheckbox.checked = true;
            if (typeof showNotification === 'function') {
                showNotification('已自动保留“原格式”导出。', 'info');
            }
        }
        const uniqueFormats = Array.from(new Set(formats));
        const structure = structureInput ? structureInput.value : PACKAGING_OPTIONS.preserve;
        const enforceZip = structure === PACKAGING_OPTIONS.flat;
        const zip = enforceZip ? true : (zipInput ? zipInput.checked : false);
        if (enforceZip && zipInput) {
            zipInput.checked = true;
            zipInput.disabled = true;
        } else if (zipInput) {
            zipInput.disabled = false;
        }

        return { template, formats: uniqueFormats, zip, structure };
    }

    async function performHistoryExport(records, config, context = {}) {
        if (!Array.isArray(records) || records.length === 0) return;
        if (!config || config.formats.length === 0) return;

        const exporter = window.PBXHistoryExporter;
        if (!exporter) {
            showNotification && showNotification('导出模块尚未加载完成', 'error');
            return;
        }

        const structure = config.structure || PACKAGING_OPTIONS.preserve;
        const enforceZip = structure === PACKAGING_OPTIONS.flat;
        const shouldZip = enforceZip || config.zip || records.length > 1 || config.formats.length > 1;
        if (shouldZip && typeof JSZip === 'undefined') {
            showNotification && showNotification('JSZip 未加载，无法打包成 ZIP', 'error');
            return;
        }

        const ensureExporter = (format, shouldZip) => {
            if (format === 'pdf' && shouldZip) {
                showNotification && showNotification('PDF 导出目前不支持打包为 ZIP，请单独导出或选择其他格式。', 'warning');
                return null;
            }
            const handler = resolveFormatHandler(format, exporter);
            if (!handler || !handler.exporterFn) {
                showNotification && showNotification(`暂不支持导出 ${format.toUpperCase()} 格式`, 'warning');
                return null;
            }
            return handler;
        };

        const assets = [];
        const generatedPaths = new Set();
        for (const record of records) {
            const variants = [];
            const hasTranslation = record.translation && record.translation.trim();
            if (hasTranslation) {
                variants.push('translation');
            }
            const includeOriginal = !hasTranslation || structure === PACKAGING_OPTIONS.flat || shouldZip;
            if (includeOriginal) {
                variants.push('original');
            }
            if (variants.length === 0) {
                variants.push('original');
            }
            for (const variant of variants) {
                const payload = buildExportPayloadFromRecord(record, variant);
                if (!payload && format !== 'original') continue;
                for (const format of config.formats) {
                    if (!SUPPORTED_EXPORT_FORMATS.includes(format)) continue;

                    if (format === 'original') {
                        if (variant === 'translation' && !TEXTUAL_ORIGINAL_EXTENSIONS.has((record.originalExtension || record.fileType || '').toLowerCase())) {
                            continue;
                        }
                        const originalAsset = buildOriginalAsset(record);
                        if (!originalAsset) {
                            showNotification && showNotification('无法导出原始格式：缺少原始内容。', 'warning');
                            continue;
                        }
                        if (variant === 'original') {
                            const relativeDirOriginal = computeRelativeDirectory(record, 'original', format, structure);
                            const sanitizedDirOriginal = relativeDirOriginal ? sanitizePath(relativeDirOriginal) : '';
                            const originalName = determineOriginalFileName(record, originalAsset.extension);
                            const uniqueOriginalName = ensureUniqueFileName(originalName, sanitizedDirOriginal, generatedPaths, 'original');
                            if (!shouldZip) {
                                saveAs(originalAsset.blob, uniqueOriginalName);
                            } else {
                                assets.push({ blob: originalAsset.blob, fileName: uniqueOriginalName, relativeDir: sanitizedDirOriginal });
                            }
                        } else if (variant === 'translation') {
                            const translatedAsset = buildOriginalTranslationAsset(record, originalAsset.extension);
                            if (!translatedAsset) continue;
                            const contextForTemplate = buildTemplateContext(record, 'original', 'translation');
                            const desiredName = applyNamingTemplate(config.template, contextForTemplate);
                            const fileName = ensureFileName(desiredName, originalAsset.extension || 'txt');
                            const relativeDir = computeRelativeDirectory(record, 'translation', format, structure);
                            const sanitizedDir = relativeDir ? sanitizePath(relativeDir) : '';
                            const uniqueName = ensureUniqueFileName(fileName, sanitizedDir, generatedPaths, 'original-translation');
                            if (!shouldZip) {
                                saveAs(translatedAsset.blob, uniqueName);
                            } else {
                                assets.push({ blob: translatedAsset.blob, fileName: uniqueName, relativeDir: sanitizedDir });
                            }
                        }
                        continue;
                    }

                    const handler = ensureExporter(format, shouldZip);
                    if (!handler) continue;

                    if (format === 'pdf' && shouldZip) {
                        continue; // 已提示
                    }

                    if (!payload) continue;
                    const contextForTemplate = buildTemplateContext(record, format, variant);
                    const desiredName = applyNamingTemplate(config.template, contextForTemplate);
                    const fileName = ensureFileName(desiredName, handler.extension);
                    const options = shouldZip ? { returnBlob: true, fileName } : { returnBlob: true, fileName };

                    try {
                        const result = await handler.exporterFn(payload, options);
                        if (!result) continue;
                        const blob = result.blob || (result.content ? new Blob([result.content], { type: result.mime || 'application/octet-stream' }) : null);
                        if (!blob) continue;

                        const relativeDir = computeRelativeDirectory(record, variant, format, structure);
                        const sanitizedDir = relativeDir ? sanitizePath(relativeDir) : '';
                        const uniqueName = ensureUniqueFileName(fileName, sanitizedDir, generatedPaths, variant);
                        if (!shouldZip) {
                            saveAs(blob, uniqueName);
                            continue;
                        }

                        assets.push({ blob, fileName: uniqueName, relativeDir: sanitizedDir });
                    } catch (error) {
                        console.error('导出失败:', error);
                        showNotification && showNotification(`导出 ${format.toUpperCase()} (${variant === 'original' ? '原文' : '译文'}) 失败：${error.message || error}`, 'error');
                    }
                }
            }
        }

        if (!shouldZip) {
            return;
        }

        const filteredAssets = assets.filter(asset => asset && asset.blob);
        if (!filteredAssets.length) {
            showNotification && showNotification('没有可打包的导出文件', 'warning');
            return;
        }

        const zip = new JSZip();
        filteredAssets.forEach(asset => {
            const dir = asset.relativeDir ? asset.relativeDir : '';
            const path = dir ? `${dir}/${asset.fileName}` : asset.fileName;
            zip.file(path, asset.blob);
        });

        const archiveName = buildArchiveName(records, config, context);
        const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } });
        saveAs(zipBlob, archiveName);
        showNotification && showNotification(`已生成 ZIP (${filteredAssets.length} 个文件)`, 'success');
    }

    function resolveFormatHandler(format, exporter) {
        switch (format) {
            case 'markdown':
                return { extension: 'md', exporterFn: exporter.exportAsMarkdown };
            case 'html':
                return { extension: 'html', exporterFn: exporter.exportAsHtml };
            case 'docx':
                return { extension: 'docx', exporterFn: exporter.exportAsDocx };
            case 'pdf':
                return { extension: 'pdf', exporterFn: exporter.exportAsPdf };
            default:
                return { extension: format, exporterFn: null };
        }
    }

    function buildExportPayloadFromRecord(record, variant = 'auto') {
        if (!record) return null;
        const data = {
            id: record.id,
            name: record.name,
            time: record.time,
            ocr: record.ocr || '',
            translation: record.translation || '',
            images: Array.isArray(record.images) ? record.images : [],
            ocrChunks: Array.isArray(record.ocrChunks) ? record.ocrChunks : [],
            translatedChunks: Array.isArray(record.translatedChunks) ? record.translatedChunks : []
        };
        let mode = 'ocr';
        if (variant === 'translation') {
            if (!data.translation || !data.translation.trim()) return null;
            mode = 'translation';
        } else if (variant === 'original') {
            if (!data.ocr || !data.ocr.trim()) return null;
            mode = 'ocr';
        } else {
            mode = data.translation ? 'translation' : 'ocr';
            if (mode === 'translation' && (!data.translation || !data.translation.trim())) {
                mode = 'ocr';
            }
        }
        const exporter = window.PBXHistoryExporter;
        if (!exporter || typeof exporter.preparePayload !== 'function') {
            return null;
        }
        const payload = exporter.preparePayload(mode, data);
        if (payload) {
            payload.customFileName = record.name;
        }
        return payload;
    }

    function buildTemplateContext(record, format, variant = 'translation') {
        const relativePath = normalizeRelativePath(record);
        const originalName = relativePath ? relativePath.split('/').pop() : (record.name || 'document');
        let originalType;
        if (format === 'markdown') {
            originalType = 'md';
        } else if (format === 'html') {
            originalType = 'html';
        } else if (format === 'docx') {
            originalType = 'docx';
        } else if (format === 'pdf') {
            originalType = 'pdf';
        } else if (format === 'original') {
            originalType = (record.originalExtension || record.fileType || 'txt').replace(/[^a-zA-Z0-9_-]/g, '') || 'txt';
        } else {
            originalType = format.replace(/[^a-zA-Z0-9_-]/g, '') || 'txt';
        }
        return {
            originalName,
            originalType,
            outputLanguage: record.batchOutputLanguage || record.targetLanguage || '',
            processingTime: record.processedAt || record.time,
            batchId: record.batchId || null,
            variant: variant === 'original' ? 'original' : 'translation'
        };
    }

    function applyNamingTemplate(template, context) {
        const safeTemplate = template || DEFAULT_EXPORT_TEMPLATE;
        return safeTemplate.replace(/\{([^{}]+)\}/g, (match, token) => {
            const [key, modifier] = token.split(':');
            switch (key) {
                case 'original_name':
                    return sanitizeFileName(context.originalName || 'document');
                case 'original_type':
                    return (context.originalType || '').replace(/[^a-zA-Z0-9_-]/g, '');
                case 'output_language':
                    return sanitizeFileName(context.outputLanguage || '');
                case 'processing_time':
                    return formatProcessingTime(context.processingTime, modifier);
                case 'batch_id':
                    return sanitizeFileName(context.batchId || '');
                case 'variant':
                    return sanitizeFileName(context.variant || '');
                default:
                    return '';
            }
        });
    }

    function formatProcessingTime(timeValue, pattern = 'YYYYMMDD-HHmmss') {
        if (!timeValue) return '';
        const date = new Date(timeValue);
        if (Number.isNaN(date.getTime())) return '';
        const pad = num => String(num).padStart(2, '0');
        return pattern
            .replace(/YYYY/g, date.getFullYear())
            .replace(/MM/g, pad(date.getMonth() + 1))
            .replace(/DD/g, pad(date.getDate()))
            .replace(/HH/g, pad(date.getHours()))
            .replace(/mm/g, pad(date.getMinutes()))
            .replace(/ss/g, pad(date.getSeconds()));
    }

    function ensureFileName(baseName, extension) {
        const sanitized = sanitizeFileName(baseName || 'document');
        const ext = (extension || '').replace(/[^a-zA-Z0-9]/g, '');
        if (!ext) return sanitized;
        if (sanitized.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {
            return sanitized;
        }
        return `${sanitized}.${ext}`;
    }

    function sanitizeFileName(name) {
        return (name || 'document').replace(/[\\/:*?"<>|]/g, '_');
    }

    function sanitizePath(path) {
        return (path || '').split('/').map(segment => sanitizeFileName(segment)).filter(Boolean).join('/');
    }

    function ensureFileExtension(baseName, extension) {
        const sanitized = sanitizeFileName(baseName || 'document');
        const ext = (extension || '').replace(/[^a-zA-Z0-9]/g, '');
        if (!ext) return sanitized;
        if (sanitized.toLowerCase().endsWith(`.${ext.toLowerCase()}`)) {
            return sanitized;
        }
        return `${sanitized}.${ext}`;
    }

    function normalizeRelativePath(record) {
        const rel = record.relativePath || '';
        if (!rel) {
            return (record.name || '').replace(/\\/g, '/');
        }
        return rel.replace(/\\/g, '/');
    }

    function normalizeRelativeDir(record) {
        const rel = normalizeRelativePath(record);
        if (!rel) return '';
        const lastSlash = rel.lastIndexOf('/');
        if (lastSlash === -1) return '';
        return sanitizePath(rel.slice(0, lastSlash));
    }

    function computeRelativeDirectory(record, variant, format, structure) {
        if (structure === PACKAGING_OPTIONS.flat) {
            const variantDir = variant === 'original' ? 'original' : 'translation';
            let formatDir;
            if (format === 'original') {
                formatDir = (record.originalExtension || record.fileType || 'raw').toLowerCase();
            } else {
                formatDir = format.toLowerCase();
            }
            return `${variantDir}/${sanitizeFileName(formatDir || 'raw')}`;
        }
        return normalizeRelativeDir(record);
    }

    function ensureUniqueFileName(fileName, dir, set, variant) {
        let uniqueName = fileName;
        let counter = 1;
        let key = `${dir}||${uniqueName}`;
        while (set.has(key)) {
            uniqueName = appendSuffix(fileName, counter++, variant);
            key = `${dir}||${uniqueName}`;
        }
        set.add(key);
        return uniqueName;
    }

    function appendSuffix(fileName, counter, variant) {
        let baseSuffix;
        if (variant === 'original') {
            baseSuffix = '_original';
        } else if (variant === 'original-translation') {
            baseSuffix = '_translated';
        } else {
            baseSuffix = '_translation';
        }
        const suffix = counter === 1 ? baseSuffix : `${baseSuffix}${counter}`;
        const idx = fileName.lastIndexOf('.');
        if (idx === -1) {
            return `${fileName}${suffix}`;
        }
        const name = fileName.slice(0, idx);
        const ext = fileName.slice(idx);
        return `${name}${suffix}${ext}`;
    }

    function buildOriginalAsset(record) {
        if (!record) return null;
        const extension = (record.originalExtension || record.fileType || 'txt').toLowerCase();
        if (record.originalEncoding === 'text' && typeof record.originalContent === 'string') {
            const mime = guessMimeType(extension, true);
            return {
                blob: new Blob([record.originalContent], { type: `${mime};charset=utf-8` }),
                extension
            };
        }
        if (record.originalEncoding && record.originalEncoding !== 'text' && record.originalBinary) {
            const buffer = base64ToArrayBuffer(record.originalBinary);
            if (!buffer) return null;
            const mime = guessMimeType(extension, false);
            return {
                blob: new Blob([buffer], { type: mime }),
                extension
            };
        }
        return null;
    }

    function buildOriginalTranslationAsset(record, extension) {
        if (!record || !record.translation || !record.translation.trim()) {
            return null;
        }
        const lowered = (extension || '').toLowerCase();
        if (!TEXTUAL_ORIGINAL_EXTENSIONS.has(lowered)) {
            // 暂不支持复杂二进制格式的译文导出
            return null;
        }
        const mime = guessMimeType(lowered, true);
        // 直接输出译文内容（目前为 Markdown 或纯文本）
        const content = record.translation;
        return {
            blob: new Blob([content], { type: `${mime};charset=utf-8` })
        };
    }

    function determineOriginalFileName(record, extension) {
        const relativePath = normalizeRelativePath(record);
        if (relativePath) {
            const parts = relativePath.split('/');
            const baseName = parts.pop() || relativePath;
            return ensureFileExtension(baseName, extension || 'txt');
        }
        const base = sanitizeFileName(record.name || 'document');
        return ensureFileExtension(base, extension || 'txt');
    }

    function guessMimeType(ext, isText) {
        const lowercase = (ext || '').toLowerCase();
        if (isText) {
            if (lowercase === 'html' || lowercase === 'htm') return 'text/html';
            if (lowercase === 'md' || lowercase === 'markdown') return 'text/markdown';
            if (lowercase === 'yaml' || lowercase === 'yml') return 'text/yaml';
            if (lowercase === 'json') return 'application/json';
            if (lowercase === 'txt') return 'text/plain';
            return 'text/plain';
        }
        if (lowercase === 'docx') return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
        if (lowercase === 'pptx') return 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
        if (lowercase === 'epub') return 'application/epub+zip';
        if (lowercase === 'pdf') return 'application/pdf';
        return 'application/octet-stream';
    }

    function base64ToArrayBuffer(base64) {
        try {
            const binaryString = atob(base64);
            const len = binaryString.length;
            const bytes = new Uint8Array(len);
            for (let i = 0; i < len; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            return bytes.buffer;
        } catch (error) {
            console.warn('base64ToArrayBuffer failed:', error);
            return null;
        }
    }

    function buildArchiveName(records, config, context) {
        const firstRecord = records[0];
        const ext = 'zip';
        const suffix = config.structure === PACKAGING_OPTIONS.flat ? '_flat' : '';
        if (records.length === 1) {
            const baseName = applyNamingTemplate(config.template, buildTemplateContext(firstRecord, 'zip'));
            return ensureFileName(`${baseName}${suffix}`, ext);
        }
        const base = context.batchId || 'batch';
        const time = formatProcessingTime(firstRecord.processedAt || firstRecord.time, 'YYYYMMDD-HHmmss');
        return ensureFileName(`${base}_${time || Date.now()}${suffix}`, ext);
    }

    async function deleteBatchRecords(batchId) {
        const batchCache = window.__historyBatchCache || {};
        const records = batchCache[batchId];
        if (!records || !records.length) return;
        for (const record of records) {
            await deleteResultFromDB(record.id);
            removeFolderAssignmentForRecord(record.id);
        }
    }

    /**
     * (全局可调用) 删除指定 ID 的单条历史记录。
     * 操作完成后会重新渲染历史记录列表以反映更改。
     *
     * @async
     * @param {string} id - 要删除的历史记录的唯一 ID (通常是 `result.id`)。
     * @returns {Promise<void>} 当删除和列表刷新完成后解决。
     */
    window.deleteHistoryRecord = function(id, name) {
        openHistoryClearModal('record', { recordId: id, recordName: name || '' });
    };

    /**
     * (全局可调用) 在新的浏览器标签页或窗口中显示指定历史记录的详细信息。
     * 它通过构建一个指向 `views/history/history_detail.html` 的 URL (包含记录 ID 作为查询参数) 并使用 `window.open` 实现。
     *
     * @param {string} id - 要查看详情的历史记录的唯一 ID。
     */
    window.showHistoryDetail = function(id) {
        try {
            localStorage.setItem('pbx_current_doc_id', id);
        } catch (e) {
            console.warn('[showHistoryDetail] Failed to save doc id to localStorage', e);
        }
        window.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
    };

    /**
     * (全局可调用) 将指定 ID 的单条历史记录打包成一个 ZIP 文件并触发浏览器下载。
     * ZIP 包中将包含：
     *  - `document.md`: 包含原始 OCR 文本（如果存在）。
     *  - `translation.md`: 包含翻译后的文本（如果存在）。
     *  - `images/` 文件夹: 包含处理过程中提取的所有图片 (PNG格式，从 Base64 数据转换)。
     *
     * 主要步骤:
     * 1. 从 IndexedDB 获取指定 ID 的历史记录数据。
     * 2. 检查 JSZip库是否已加载，未加载则提示错误。
     * 3. 创建一个新的 JSZip 实例。
     * 4. 根据记录名创建一个顶层文件夹（文件名中的非法字符会被替换）。
     * 5. 将 OCR 文本和翻译文本（如果存在）分别存为 Markdown 文件。
     * 6. 如果存在图片数据 (`r.images`)，则创建一个 `images` 子文件夹，并将每张图片（Base64编码）
     *    解码后以 PNG 格式存入该子文件夹。
     * 7. 使用 JSZip 生成 ZIP 文件的 Blob 数据 (DEFLATE 压缩)。
     * 8. 构建文件名 (包含原始文件名和时间戳) 并使用 `saveAs` (FileSaver.js) 触发下载。
     *
     * @async
     * @param {string} id - 要下载的历史记录的唯一 ID。
     * @returns {Promise<void>} 当 ZIP 文件准备好并开始下载时解决，或在发生错误时提前返回。
     */
    window.downloadHistoryRecord = async function(id) {
        const r = await getResultFromDB(id);
        if (!r) return;
        if (typeof JSZip === 'undefined') {
            alert('JSZip 加载失败，无法打包下载');
            return;
        }
        const zip = new JSZip();
        const normalizedPath = (r.relativePath || r.name || '').replace(/\\/g, '/');
        const dirPath = normalizedPath.includes('/') ? normalizedPath.slice(0, normalizedPath.lastIndexOf('/')) : '';
        const baseName = normalizedPath.includes('/') ? normalizedPath.slice(normalizedPath.lastIndexOf('/') + 1) : (r.name || 'document');
        const baseWithoutExt = baseName.replace(/\.[^.]+$/, '');
        const sanitizedDir = dirPath ? sanitizePath(dirPath) : '';
        const sanitizedBase = sanitizeFileName(baseWithoutExt).substring(0, 120) || 'document';
        const folderPath = sanitizedDir ? `${sanitizedDir}/${sanitizedBase}` : sanitizedBase;
        const folder = zip.folder(folderPath);
        folder.file('document.md', r.ocr || '');
        if (r.translation) folder.file('translation.md', r.translation);
        if (r.originalEncoding === 'text' && typeof r.originalContent === 'string') {
            const ext = (r.originalExtension || r.fileType || 'txt').toLowerCase();
            const mime = guessMimeType(ext, true);
            folder.file(`original.${ext || 'txt'}`, new Blob([r.originalContent], { type: `${mime};charset=utf-8` }));
        } else if (r.originalEncoding && r.originalEncoding !== 'text' && r.originalBinary) {
            const buffer = base64ToArrayBuffer(r.originalBinary);
            if (buffer) {
                const ext = (r.originalExtension || r.fileType || 'bin').toLowerCase();
                const mime = guessMimeType(ext, false);
                folder.file(`original.${ext || 'bin'}`, new Blob([buffer], { type: mime }));
            }
        }
        if (r.images && r.images.length > 0) {
            const imagesFolder = folder.folder('images');
            for (const img of r.images) {
                // 处理base64图片数据
                const base64Data = img.data.includes(',') ? img.data.split(',')[1] : img.data;
                if (base64Data) {
                    imagesFolder.file(`${img.id}.png`, base64Data, { base64: true });
                }
            }
        }
        // 生成zip并下载
        const zipBlob = await zip.generateAsync({ type: 'blob', compression: "DEFLATE", compressionOptions: { level: 6 } });
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const archiveName = ensureFileName(`${sanitizedBase}_${timestamp}`, 'zip');
        saveAs(zipBlob, archiveName);
    };

    // ---------------------
    // 重新翻译（全部/失败段）
    // ---------------------

    function _isChunkFailed(text) {
        if (text == null) return true;
        let t = String(text).trim();
        if (!t) return true;
        // 取首行，规避后续原文内容干扰
        const firstLine = t.split('\n', 1)[0];
        // 去除常见 Markdown 前缀（引用符、粗体符号等）
        let norm = firstLine.replace(/^>+\s*/, '').trim();
        norm = norm.replace(/^\*\*(.*)\*\*$/,'$1').trim();
        // 识别多种失败提示格式
        if (/^\[(?:翻译失败|处理错误|翻译错误|翻译意外失败)/i.test(norm)) return true;
        if (/保留原文\s*Part/i.test(norm)) return true;
        return false;
    }

    function _setBusy(id, busy, msg = '') {
        const failedBtn = document.getElementById(`retry-failed-btn-${id}`);
        const allBtn = document.getElementById(`retry-all-btn-${id}`);
        const statusEl = document.getElementById(`retry-status-${id}`);
        if (failedBtn) failedBtn.disabled = !!busy;
        if (allBtn) allBtn.disabled = !!busy;
        if (statusEl) statusEl.textContent = msg || '';
    }

    function _getEffectiveTargetLanguage(settings) {
        if (!settings) return 'chinese';
        if (settings.targetLanguage === 'custom') {
            const name = (settings.customTargetLanguageName || '').trim();
            return name || 'English';
        }
        return settings.targetLanguage || 'chinese';
    }

    function _getTranslationContext() {
        const settings = typeof loadSettings === 'function' ? loadSettings() : {};
        const modelName = settings.selectedTranslationModel || 'none';
        if (modelName === 'none') {
            showNotification && showNotification('当前未选择翻译模型，无法执行重译。', 'warning');
            return null;
        }

        let providerKey = modelName;
        let modelConfig = null;
        if (modelName === 'custom') {
            const siteId = settings.selectedCustomSourceSiteId;
            if (!siteId) {
                showNotification && showNotification('未选择自定义源站点，请先在主界面选择。', 'error');
                return null;
            }
            const allSites = typeof loadAllCustomSourceSites === 'function' ? loadAllCustomSourceSites() : {};
            const siteCfg = allSites[siteId];
            if (!siteCfg) {
                showNotification && showNotification('未能加载选定的自定义源站配置。', 'error');
                return null;
            }
            providerKey = `custom_source_${siteId}`;
            modelConfig = siteCfg;
        }

        // KeyProvider 来自 app.js，作为全局可用
        let kp = null;
        try { kp = new KeyProvider(providerKey); } catch(e) { console.error(e); }
        if (!kp || !kp.hasAvailableKeys()) {
            showNotification && showNotification('所选模型没有可用的 API Key，请先配置。', 'error');
            return null;
        }

        const ctx = {
            settings,
            modelName,
            modelConfig,
            keyProvider: kp,
            targetLangName: _getEffectiveTargetLanguage(settings),
            tokenLimit: parseInt(settings.maxTokensPerChunk) || 2000,
            defaultSystemPrompt: settings.defaultSystemPrompt || '',
            defaultUserPromptTemplate: settings.defaultUserPromptTemplate || '',
            useCustomPrompts: !!settings.useCustomPrompts
        };
        return ctx;
    }

    async function _translateOneChunk(chunkText, ctx, logPrefix) {
        // 轮询 Key，失败时标记无效并切换
        let lastErr = null;
        for (let attempt = 0; attempt < 5; attempt++) {
            const keyObj = ctx.keyProvider.getNextKey();
            if (!keyObj) { lastErr = new Error('无可用Key'); break; }
            const keyVal = keyObj.value;
            try {
                if (ctx.modelName === 'custom') {
                    const res = await translateMarkdown(
                        chunkText,
                        ctx.targetLangName,
                        'custom',
                        keyVal,
                        ctx.modelConfig,
                        logPrefix || '',
                        ctx.defaultSystemPrompt,
                        ctx.defaultUserPromptTemplate,
                        ctx.useCustomPrompts
                    );
                    return res;
                } else {
                    const res = await translateMarkdown(
                        chunkText,
                        ctx.targetLangName,
                        ctx.modelName,
                        keyVal,
                        logPrefix || '',
                        ctx.defaultSystemPrompt,
                        ctx.defaultUserPromptTemplate,
                        ctx.useCustomPrompts
                    );
                    return res;
                }
            } catch (e) {
                const msg = (e && e.message ? e.message : String(e)).toLowerCase();
                lastErr = e;
                // 常见失活/未授权关键词，标记Key失效
                if (msg.includes('unauthorized') || msg.includes('invalid') || msg.includes('forbidden') || msg.includes('401')) {
                    try { await ctx.keyProvider.markKeyAsInvalid(keyObj.id); } catch {}
                    continue;
                } else {
                    // 其他错误不标记失活，但尝试下一个Key
                    continue;
                }
            }
        }
        throw lastErr || new Error('翻译失败');
    }

    async function _retryRecordInternal(id, mode) {
        const ctx = _getTranslationContext();
        if (!ctx) return;
        _setBusy(id, true, '处理中...');
        try {
            const record = await getResultFromDB(id);
            if (!record) {
                showNotification && showNotification('未找到历史记录。', 'error');
                return;
            }
            const logPrefix = `[重译:${record.name}]`;

            if (mode === 'all') {
                // 按需求：不要直接执行重译，将整篇加入“上传文件列表”（虚拟目录），供用户统一点击处理
                const baseName = (record.name || 'document').replace(/\.pdf$/i, '');
                const header = `<!-- PBX-HISTORY-REF:${record.id} -->\n`;
                const mdBody = (record.ocr && record.ocr.trim()) ? record.ocr : Array.isArray(record.ocrChunks) ? record.ocrChunks.join('\n\n') : '';
                const mdText = header + mdBody;
                if (!mdText) {
                    showNotification && showNotification('该记录没有可用的 OCR 文本，无法加入待处理列表。', 'warning');
                    return;
                }
                const uniqueSuffix = Math.random().toString(36).slice(2,6);
                const fileName = `${baseName}-retranslate-${uniqueSuffix}.md`;
                try {
                    const virtualFile = new File([mdText], fileName, { type: 'text/markdown' });
                    try { virtualFile.virtualType = 'retranslate'; } catch(_) {}
                    if (typeof addFilesToList === 'function') {
                        addFilesToList([virtualFile]);
                        showNotification && showNotification(`已将“${fileName}”加入待处理列表，请在主界面点击“开始处理”。`, 'success');
                        // 成功后自动关闭历史面板，避免遮挡主界面交互
                        try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                    } else {
                        // 后备：直接操作全局数组并刷新UI
                        if (typeof window !== 'undefined' && Array.isArray(window.pdfFiles)) {
                            window.pdfFiles.push(virtualFile);
                            if (typeof updateFileListUI === 'function' && typeof updateProcessButtonState === 'function' && typeof handleRemoveFile === 'function') {
                                updateFileListUI(window.pdfFiles, window.isProcessing || false, handleRemoveFile);
                                updateProcessButtonState(window.pdfFiles, window.isProcessing || false);
                            }
                            showNotification && showNotification(`已将“${fileName}”加入待处理列表，请在主界面点击“开始处理”。`, 'success');
                            try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                        } else {
                            showNotification && showNotification('无法加入待处理列表：缺少文件列表接口。', 'error');
                        }
                    }
                } catch (e) {
                    console.error('创建虚拟文件失败:', e);
                    showNotification && showNotification('创建虚拟文件失败，无法加入待处理列表。', 'error');
                }
            } else {
                // 仅重试失败段
                const total = Array.isArray(record.ocrChunks) ? record.ocrChunks.length : 0;

                // 标准分块路径
                if (total > 0 && Array.isArray(record.translatedChunks)) {
                    const pieces = [];
                    for (let i = 0; i < total; i++) {
                        if (_isChunkFailed(record.translatedChunks[i])) {
                            const ocrText = record.ocrChunks[i] || '';
                            if (ocrText.trim()) {
                                pieces.push(`<!-- PBX-CHUNK-INDEX:${i} -->\n\n${ocrText}`);
                            }
                        }
                    }
                    if (pieces.length === 0) {
                        showNotification && showNotification('没有需要重试的片段。', 'info');
                        return;
                    }
                    const header = `<!-- PBX-HISTORY-REF:${record.id} -->\n<!-- PBX-MODE:retry-failed -->\n`;
                    const mdText = header + pieces.join('\n\n\n');
                    const baseName = (record.name || 'document').replace(/\.pdf$/i, '');
                    const uniqueSuffix = Math.random().toString(36).slice(2,6);
                    const fileName = `${baseName}-retry-failed-${uniqueSuffix}.md`;
                    try {
                        const virtualFile = new File([mdText], fileName, { type: 'text/markdown' });
                        try { virtualFile.virtualType = 'retry-failed'; } catch(_) {}
                        if (typeof addFilesToList === 'function') {
                            addFilesToList([virtualFile]);
                            showNotification && showNotification(`已将“${fileName}”加入待处理列表（失败片段），请点击“开始处理”。`, 'success');
                            try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                        } else if (typeof window !== 'undefined' && Array.isArray(window.pdfFiles)) {
                            window.pdfFiles.push(virtualFile);
                            if (typeof updateFileListUI === 'function' && typeof updateProcessButtonState === 'function' && typeof handleRemoveFile === 'function') {
                                updateFileListUI(window.pdfFiles, window.isProcessing || false, handleRemoveFile);
                                updateProcessButtonState(window.pdfFiles, window.isProcessing || false);
                            }
                            showNotification && showNotification(`已将“${fileName}”加入待处理列表（失败片段），请点击“开始处理”。`, 'success');
                            try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                        } else {
                            showNotification && showNotification('无法加入待处理列表：缺少文件列表接口。', 'error');
                        }
                    } catch (e) {
                        console.error('创建虚拟文件失败:', e);
                        showNotification && showNotification('创建虚拟文件失败，无法加入待处理列表。', 'error');
                    }
                    return;
                }

                // 结构化路径：创建虚拟文件加入上传列表（与标准分块路径保持一致，有UI进度反馈）
                const meta = record && record.metadata ? record.metadata : {};
                let failedItems = Array.isArray(meta.failedStructuredItems) ? meta.failedStructuredItems.slice() : [];

                // 回退：若缺少 failedStructuredItems，则从 translatedContentList 中推断
                if (failedItems.length === 0 && Array.isArray(meta.translatedContentList)) {
                    const _norm = (v) => {
                        if (v == null) return '';
                        try {
                            if (Array.isArray(v)) return v.join(' ').trim();
                            if (typeof v === 'string') return v.trim();
                            return String(v).trim();
                        } catch(_) { return ''; }
                    };
                    const tlist = meta.translatedContentList;
                    const olist = Array.isArray(meta.contentListJson) ? meta.contentListJson : [];
                    const minLen = Math.min(tlist.length, olist.length);
                    for (let i = 0; i < minLen; i++) {
                        const t = tlist[i] || {};
                        const o = olist[i] || {};
                        let isFailed = false;

                        // 如果有 failureReason 字段，根据它判断
                        if (t.failed && t.failureReason) {
                            isFailed = (t.failureReason === 'empty');
                            // failureReason === 'unchanged' 不重试
                        } else if (t.failed || !t.text) {
                            // 旧数据没有 failureReason，需要重新判定
                            // 只有原文不为空且译文为空时才标记为失败
                            if (o.type === 'text') {
                                const a = _norm(o.text);
                                const b = _norm(t.text);
                                isFailed = a && !b;
                            } else if (o.type === 'image') {
                                const a = _norm(o.image_caption);
                                const b = _norm(t.image_caption);
                                isFailed = a && !b;
                            } else if (o.type === 'table') {
                                const a = _norm(o.table_caption);
                                const b = _norm(t.table_caption);
                                isFailed = a && !b;
                            }
                        }
                        if (isFailed) {
                            const rawText = (o.type === 'text') ? (o.text || '')
                                            : (o.type === 'image') ? (Array.isArray(o.image_caption) ? o.image_caption.join(' ') : o.image_caption)
                                            : (o.type === 'table') ? (o.table_caption || '')
                                            : '';
                            const normText = _norm(rawText);
                            if (normText) {
                                failedItems.push({ index: i, type: o.type, page_idx: o.page_idx || 0, text: normText });
                            }
                        }
                    }
                }

                if (failedItems.length === 0) {
                    showNotification && showNotification('没有需要重试的片段。', 'info');
                    return;
                }

                // 构建失败片段的虚拟文件（加入上传列表，在主界面显示进度）
                // 使用特殊标记告诉 main.js 这是结构化翻译的失败重试
                const header = `<!-- PBX-HISTORY-REF:${record.id} -->\n<!-- PBX-MODE:retry-structured-failed -->\n<!-- PBX-FAILED-COUNT:${failedItems.length} -->\n`;
                const failedIndices = failedItems.map(fi => fi.index).join(',');
                const mdText = header + `<!-- PBX-FAILED-INDICES:${failedIndices} -->\n\n结构化翻译失败片段重试（${failedItems.length} 个）`;
                const baseName = (record.name || 'document').replace(/\.pdf$/i, '');
                const uniqueSuffix = Math.random().toString(36).slice(2,6);
                const fileName = `${baseName}-retry-structured-${uniqueSuffix}.md`;

                try {
                    const virtualFile = new File([mdText], fileName, { type: 'text/markdown' });
                    try { virtualFile.virtualType = 'retry-structured-failed'; } catch(_) {}

                    if (typeof addFilesToList === 'function') {
                        addFilesToList([virtualFile]);
                        showNotification && showNotification(`已将"${fileName}"加入待处理列表（${failedItems.length} 个失败片段），请点击"开始处理"。`, 'success');
                        try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                    } else if (typeof window !== 'undefined' && Array.isArray(window.pdfFiles)) {
                        window.pdfFiles.push(virtualFile);
                        if (typeof updateFileListUI === 'function' && typeof updateProcessButtonState === 'function' && typeof handleRemoveFile === 'function') {
                            updateFileListUI(window.pdfFiles, window.isProcessing || false, handleRemoveFile);
                            updateProcessButtonState(window.pdfFiles, window.isProcessing || false);
                        }
                        showNotification && showNotification(`已将"${fileName}"加入待处理列表（${failedItems.length} 个失败片段），请点击"开始处理"。`, 'success');
                        try { document.getElementById('historyPanel')?.classList.add('hidden'); } catch(_) {}
                    } else {
                        showNotification && showNotification('无法加入待处理列表：缺少文件列表接口。', 'error');
                    }
                } catch (e) {
                    console.error('创建虚拟文件失败:', e);
                    showNotification && showNotification('创建虚拟文件失败，无法加入待处理列表。', 'error');
                }
                return;
            }

            // 刷新列表显示状态
            await renderHistoryList();
        } catch (e) {
            console.error('重译发生错误:', e);
            showNotification && showNotification(`重译失败：${e && e.message ? e.message : String(e)}`, 'error');
        } finally {
            _setBusy(id, false, '');
        }
    }

    /**
     * (全局可调用) 对历史记录执行重新翻译
     * @param {string} id 历史记录ID
     * @param {('all'|'failed')} mode 模式：全部或仅失败
     */
    window.retryTranslateRecord = function(id, mode) {
        _retryRecordInternal(id, mode === 'all' ? 'all' : 'failed');
    };
});
