// popup.js — 控制面板逻辑 const btnStart = document.getElementById('btnStart'); const btnPause = document.getElementById('btnPause'); const btnExport = document.getElementById('btnExport'); const btnClear = document.getElementById('btnClear'); const statusText = document.getElementById('statusText'); const progressFill = document.getElementById('progressFill'); const logBox = document.getElementById('logBox'); const modeSelect = document.getElementById('modeSelect'); const optDedup = document.getElementById('optDedup'); const optChat = document.getElementById('optChat'); const limitSelect = document.getElementById('limitSelect'); const chatTimeRangeSelect = document.getElementById('chatTimeRange'); const chatCustomDates = document.getElementById('chatCustomDates'); const chatSinceDate = document.getElementById('chatSinceDate'); const chatUntilDate = document.getElementById('chatUntilDate'); const chatLimitHint = document.getElementById('chatLimitHint'); function addLog(msg, type = '') { const p = document.createElement('p'); p.className = type; p.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`; logBox.appendChild(p); logBox.scrollTop = logBox.scrollHeight; } function setProgress(current, total) { const pct = total > 0 ? Math.round((current / total) * 100) : 0; progressFill.style.width = pct + '%'; statusText.textContent = `${current} / ${total} (${pct}%)`; } function updateButtons(phase) { const running = phase === 'list' || phase === 'detail'; const paused = phase === 'paused'; btnStart.disabled = running || paused; btnPause.disabled = !running && !paused; btnPause.textContent = paused ? '恢复' : '暂停'; btnExport.disabled = running || paused; } // 带重试的 send,content script 注入需要时间 function send(action, data = {}, retries = 3) { return new Promise(resolve => { function attempt(remaining) { chrome.tabs.query({ active: true, currentWindow: true }, tabs => { if (!tabs[0]) { addLog('未找到活动标签页', 'error'); resolve(); return; } // 检查是否在正确的域名 if (!tabs[0].url || !tabs[0].url.includes('message.alibaba.com')) { addLog('请在 message.alibaba.com 页面操作', 'error'); resolve(); return; } chrome.tabs.sendMessage(tabs[0].id, { action, ...data }, resp => { if (chrome.runtime.lastError) { // content script 未注入(扩展重载后旧页面失效/新标签注入慢)→ 程序化注入兜底。 // content.js 自带 __alibabaScraperInit 防重复注入,重复执行安全。 chrome.scripting.executeScript({ target: { tabId: tabs[0].id }, files: ['content.js'] }, () => { if (chrome.runtime.lastError) { addLog('自动注入失败: ' + chrome.runtime.lastError.message, 'error'); resolve(); return; } if (remaining > 0) { setTimeout(() => attempt(remaining - 1), 800); // 注入完成后再试 } else { addLog('连接失败,请刷新页面后重试', 'error'); resolve(); } }); } else { resolve(resp); } }); }); } attempt(retries); }); } // 检查当前状态 send('getState').then(resp => { if (resp) { if (resp.running) { addLog(`有进行中的任务 (列表${resp.listCount}条, 详情${resp.detailCount}条, 队列${resp.queueIndex}/${resp.queueTotal})`, 'info'); statusText.textContent = `运行中 (${resp.queueIndex}/${resp.queueTotal})`; updateButtons(resp.phase); } else if (resp.phase === 'paused') { addLog('有暂停的任务,点击"恢复"继续', 'info'); statusText.textContent = '已暂停'; updateButtons('paused'); } } }); btnStart.addEventListener('click', async () => { const range = computeChatRange(chatTimeRangeSelect.value, chatSinceDate.value, chatUntilDate.value); if (range.error) { addLog(range.error, 'error'); statusText.textContent = '就绪'; updateButtons('idle'); return; } // 与 range 同步快照:导航等待 5s 内用户仍可改下拉,不能让部分参数冻结、部分活读(参数漂移) const chatLimitSnap = chatTimeRangeSelect.value === 'all' ? (parseInt(document.getElementById('chatLimitInput').value, 10) || 0) : 0; updateButtons('list'); hideRiskBanner(); // 开始新任务时隐藏风控红条 addLog('开始抓取...', 'info'); statusText.textContent = '启动中...'; chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { const tab = tabs && tabs[0]; // 不在消息页 → 自动跳转,等 content.js 注入后再发 start if (tab && !(tab.url && tab.url.includes('message.alibaba.com'))) { addLog('当前不在消息页,自动跳转...', 'info'); statusText.textContent = '跳转消息页...'; await chrome.tabs.update(tab.id, { url: 'https://message.alibaba.com/message/default.htm#feedback/all' }); await new Promise(r => setTimeout(r, 5000)); // 等 document_idle + content 注入 addLog('消息页就绪,开始抓取', 'info'); } await send('start', { mode: modeSelect.value, dedup: optDedup.checked, chat: optChat.checked, limit: parseInt(limitSelect.value, 10), chatLimit: chatLimitSnap, chatSince: range.since, chatUntil: range.until, }); }); }); btnPause.addEventListener('click', async () => { const phase = btnPause.textContent === '恢复' ? 'resume' : 'pause'; await send(phase); if (phase === 'pause') { updateButtons('paused'); statusText.textContent = '已暂停'; btnPause.textContent = '恢复'; addLog('已暂停(支持跨页面恢复)', 'info'); } else { updateButtons('list'); hideRiskBanner(); // 恢复时隐藏风控红条(用户已处理验证码) statusText.textContent = '恢复中...'; btnPause.textContent = '暂停'; addLog('恢复抓取...', 'info'); } }); btnExport.addEventListener('click', async () => { addLog('生成 CSV...', 'info'); const resp = await send('export'); if (resp && resp.success) { addLog(`导出 ${resp.count} 条数据`, 'success'); const blob = new Blob(['\uFEFF' + resp.csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `alibaba_inquiry_${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } }); btnClear.addEventListener('click', async () => { await send('clear'); hideRiskBanner(); // 清空时隐藏风控红条 logBox.innerHTML = ''; statusText.textContent = '就绪'; progressFill.style.width = '0%'; btnPause.textContent = '暂停'; updateButtons('idle'); addLog('数据已清空', 'info'); }); // 监听 content script 推送 chrome.runtime.onMessage.addListener(msg => { if (msg.type === 'log') addLog(msg.text, msg.level || ''); if (msg.type === 'progress') setProgress(msg.current, msg.total); if (msg.type === 'done') { updateButtons('done'); btnExport.disabled = false; statusText.textContent = '完成'; } if (msg.type === 'error') { updateButtons('idle'); statusText.textContent = '出错'; } // 安全层 #6:风控报警 —— 顶部红条 + 日志,按钮切到 paused 态 if (msg.type === 'risk') { const banner = document.getElementById('riskBanner'); if (banner) banner.style.display = 'block'; addLog('⚠️ 疑似触发风控: ' + (msg.msg || ''), 'error'); updateButtons('paused'); statusText.textContent = '风控暂停'; } }); // 隐藏风控红条(开始新任务/恢复时调用) function hideRiskBanner() { const banner = document.getElementById('riskBanner'); if (banner) banner.style.display = 'none'; } // ==== 聊天时间范围(spec 2026-08-25 §4.1)==== // minusMonths/computeChatRange 与 scripts/verify-chat-range.mjs 基准实现逐字同步 // 自然月回退:直接 setMonth(-n) 月末溢出(3-31−1月→"2-31"→3-02),先退月初再钳制天数 function minusMonths(base, n) { const d = new Date(base.getFullYear(), base.getMonth(), 1, base.getHours(), base.getMinutes(), base.getSeconds(), base.getMilliseconds()); d.setMonth(d.getMonth() - n); const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate(); d.setDate(Math.min(base.getDate(), lastDay)); return d; } // popup 范围计算:mode=all|1m|2m|3m|custom → {since,until}(ISO|null) 或 {error} function computeChatRange(mode, sinceStr, untilStr) { if (mode === 'all') return { since: null, until: null }; if (mode === '1m' || mode === '2m' || mode === '3m') { // 整天边界:since=起始日 00:00:00(完整自然月天数),until=今日 23:59:59.999("至今"取今天整天,与 custom 止=今天一致) const dayStart = new Date(); dayStart.setHours(0, 0, 0, 0); const dayEnd = new Date(); dayEnd.setHours(23, 59, 59, 999); return { since: minusMonths(dayStart, Number(mode[0])).toISOString(), until: dayEnd.toISOString() }; } if (mode === 'custom') { if (!sinceStr || !untilStr) return { error: '请填写自定义起止日期' }; const s = new Date(sinceStr + 'T00:00:00.000'); // ⚠️ 必须拼时间:裸 date-only 按 UTC 解析差 8h const u = new Date(untilStr + 'T23:59:59.999'); if (isNaN(s.getTime()) || isNaN(u.getTime())) return { error: '日期格式错误' }; if (s.getTime() > u.getTime()) return { error: '起始日期不能晚于截止日期' }; const uDay = new Date(untilStr + 'T00:00:00.000'); const t0 = new Date(); t0.setHours(0, 0, 0, 0); if (uDay.getTime() > t0.getTime()) return { error: '截止日期不能晚于今天' }; return { since: s.toISOString(), until: u.toISOString() }; } return { error: '未知时间范围模式' }; } function syncChatModeUI() { const mode = chatTimeRangeSelect.value; const custom = mode === 'custom'; chatCustomDates.style.display = custom ? 'block' : 'none'; if (custom && !chatUntilDate.value) { const t = new Date(); chatUntilDate.value = `${t.getFullYear()}-${String(t.getMonth() + 1).padStart(2, '0')}-${String(t.getDate()).padStart(2, '0')}`; } const timeMode = mode !== 'all'; document.getElementById('chatLimitInput').disabled = timeMode; // D1: 时间替代条数 chatLimitHint.textContent = timeMode ? '(时间模式下忽略)' : ''; } chatTimeRangeSelect.addEventListener('change', syncChatModeUI); syncChatModeUI(); // === 后端推送配置 === const cfgServerUrl = document.getElementById('cfgServerUrl'); const cfgToken = document.getElementById('cfgToken'); const cfgEnabled = document.getElementById('cfgEnabled'); // 加载现有配置(background 返回 { ok, config }) chrome.runtime.sendMessage({ type: 'getPushConfig' }, resp => { if (chrome.runtime.lastError) { addLog('读取推送配置失败: ' + chrome.runtime.lastError.message, 'error'); return; } const cfg = resp && resp.ok ? resp.config : null; cfgServerUrl.value = (cfg && cfg.serverUrl) || 'http://219.157.255.213:23010'; cfgToken.value = (cfg && cfg.token) || '688cbb4b6e880b340b2360559c4ad29d01a39ccbb3c31c4d'; cfgEnabled.checked = cfg ? !!cfg.enabled : true; // 默认勾「启用推送」(首次未配置时) }); // 授权域名(chrome.permissions.request 必须在用户手势内) document.getElementById('btnAuthorize').addEventListener('click', () => { const url = cfgServerUrl.value.trim(); if (!url) { addLog('请先填写服务器 URL', 'error'); return; } let origin; try { const u = new URL(url); origin = `${u.origin}/*`; } catch { addLog('URL 格式错误', 'error'); return; } chrome.permissions.request({ origins: [origin] }, granted => { if (chrome.runtime.lastError) { addLog('授权请求失败: ' + chrome.runtime.lastError.message, 'error'); return; } if (granted) addLog('已授权 ' + origin, 'success'); else addLog('授权被拒绝', 'error'); }); }); // 保存设置 document.getElementById('btnSaveConfig').addEventListener('click', () => { const config = { serverUrl: cfgServerUrl.value.trim().replace(/\/+$/, ''), // 去尾斜杠 token: cfgToken.value.trim(), enabled: cfgEnabled.checked, }; chrome.runtime.sendMessage({ type: 'setPushConfig', config }, resp => { if (chrome.runtime.lastError) { addLog('保存失败: ' + chrome.runtime.lastError.message, 'error'); return; } if (resp && resp.ok) addLog('推送设置已保存' + (config.enabled ? '(已启用)' : ''), 'success'); else addLog('保存失败', 'error'); }); }); // === 推送状态显示 === const pushStatus = document.getElementById('pushStatus'); function refreshPushStatus() { chrome.runtime.sendMessage({ type: 'getPushStats' }, resp => { if (chrome.runtime.lastError || !resp || !resp.ok || !resp.stats) { pushStatus.style.color = '#999'; pushStatus.textContent = '推送:未启用'; return; } const stats = resp.stats; const pending = stats.pending || 0; const failed = stats.failed || 0; const sent = stats.sent || 0; pushStatus.style.color = failed > 0 ? '#f5222d' : '#666'; pushStatus.textContent = `推送:已推 ${sent} / 待推 ${pending} / 失败 ${failed}`; }); } refreshPushStatus(); const statusTimer = setInterval(refreshPushStatus, 2000); window.addEventListener('unload', () => clearInterval(statusTimer)); // 失败时点击查看提示 pushStatus.addEventListener('click', () => { if (pushStatus.textContent.includes('失败 0')) return; alert('有推送失败项。可在扩展后台日志查看错误,或检查后端是否在线。'); });