// ==UserScript== // @name 闲鱼 AI 智能客服 // @namespace https://goofish.com/ // @version 1.0.0 // @description 基于 OpenAI API 的智能自动回复,检测聊天列表未读角标触发,支持自定义 prompt、查看聊天记录、暂停功能 // @author You // @match https://www.goofish.com/im* // @run-at document-idle // @grant GM_xmlhttpRequest // @connect * // ==/UserScript== (function () { "use strict"; /* ═══════════════════════════════════════════ 配置 Key(LocalStorage) ═══════════════════════════════════════════ */ const KEY_ENABLED = "xianyu_ai_enabled"; const KEY_API_KEY = "xianyu_ai_api_key"; const KEY_API_BASE = "xianyu_ai_api_base"; const KEY_MODEL = "xianyu_ai_model"; const KEY_PROMPT = "xianyu_ai_sys_prompt"; const KEY_REPLY_SUFFIX = "xianyu_ai_reply_suffix"; const KEY_REPLIED = "xianyu_ai_replied"; /* ═══════════════════════════════════════════ 默认值 ═══════════════════════════════════════════ */ const DEFAULT_BASE = "https://api.openai.com/v1"; const DEFAULT_MODEL = "gpt-4o-mini"; const DEFAULT_PROMPT = `你是一位专业的闲鱼卖家客服助手。请根据买家的问题给出简洁、友好、专业的回复。 注意: - 回复要简短自然,不超过 100 字 - 用中文回复 - 不要每次都重复相同的问候语 - 如果买家询问发货/退款/价格,请给出合理解答`; const DEFAULT_REPLY_SUFFIX = "(由机器人回复)"; const SCAN_INTERVAL = 15_000; // 两次扫描间隔(毫秒) /* ═══════════════════════════════════════════ 工具函数 ═══════════════════════════════════════════ */ const store = { get: (key) => localStorage.getItem(key), set: (key, val) => localStorage.setItem(key, String(val)), }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function getConversationNameFromItem(item) { if (!item) return ""; const nameEl = [...item.querySelectorAll("div, span")].find( (node) => node.style.overflow === "hidden" && node.style.textOverflow === "ellipsis" && node.style.whiteSpace === "nowrap", ); return nameEl?.textContent?.trim() || ""; } function findVisibleConversationItem(username) { const items = document.querySelectorAll('[class*="conversation-item--"]'); for (const item of items) { if (getConversationNameFromItem(item) === username) { return item; } } return null; } function getConversationScrollContainer() { const preferred = [ "#conv-list-scrollable .rc-virtual-list-holder", "#conv-list-scrollable", ".rc-virtual-list-holder", ".rc-virtual-list", ]; for (const selector of preferred) { const node = document.querySelector(selector); if (node && node.scrollHeight > node.clientHeight + 20) { return node; } } const seed = document.querySelector('[class*="conversation-item-active--"]') || document.querySelector('[class*="conversation-item--"]'); let current = seed?.parentElement || null; while (current && current !== document.body) { const style = window.getComputedStyle(current); const overflowY = style.overflowY; const isVirtualHolder = current.classList?.contains("rc-virtual-list-holder") || current.closest("#conv-list-scrollable") !== null; if ( current.scrollHeight > current.clientHeight + 20 && ( overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay" || isVirtualHolder ) ) { return current; } current = current.parentElement; } return null; } /** 等待指定选择器出现,最多等 timeout ms */ function waitFor(selector, root = document, timeout = 8000) { return new Promise((resolve, reject) => { const el = root.querySelector(selector); if (el) return resolve(el); const obs = new MutationObserver(() => { const found = root.querySelector(selector); if (found) { obs.disconnect(); resolve(found); } }); obs.observe(root.nodeType === Node.DOCUMENT_NODE ? root.body : root, { childList: true, subtree: true, }); setTimeout(() => { obs.disconnect(); reject(new Error(`waitFor 超时: ${selector}`)); }, timeout); }); } /** 简单 HTML 转义,防止 XSS */ function esc(str) { return String(str ?? "") .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } /* ═══════════════════════════════════════════ 已回复记录(持久化) ═══════════════════════════════════════════ */ function loadReplied() { try { return JSON.parse(localStorage.getItem(KEY_REPLIED) || "[]"); } catch { return []; } } function saveReplied(list) { localStorage.setItem(KEY_REPLIED, JSON.stringify(list)); } /** 新增或更新一条回复记录(同一用户去重,最新时间置顶,累计未读条数) */ function addReplied(username) { const list = loadReplied(); const idx = list.findIndex((r) => r.username === username); const time = new Date(Date.now() + 8 * 3600_000) .toISOString() .replace("T", " ") .slice(0, 19); if (idx !== -1) { const existing = list.splice(idx, 1)[0]; existing.time = time; existing.unread = (existing.unread || 0) + 1; list.unshift(existing); } else { list.unshift({ username, time, unread: 1 }); } if (list.length > 200) list.length = 200; saveReplied(list); updateRepliedUI(); } function markRepliedRead(username) { const list = loadReplied().map((r) => r.username === username ? { ...r, unread: 0 } : r, ); saveReplied(list); updateRepliedUI(); } /** 在聊天列表中找到对应用户名的会话并点击(兼容虚拟列表) */ async function navigateToConversation(username) { if (!username) return; const clickItem = (item) => { item.scrollIntoView({ block: "center" }); item.click(); }; const visibleItem = findVisibleConversationItem(username); if (visibleItem) { clickItem(visibleItem); log(`🔎 已定位会话:${username}`); return; } const scroller = getConversationScrollContainer(); if (!scroller) { log(`⚠️ 未找到聊天列表滚动容器:${username}`); return; } log(`🔎 开始查找会话:${username}`); setStatus(`正在查找会话:${username}`); const initialTop = scroller.scrollTop; const step = Math.max(280, Math.floor(scroller.clientHeight * 0.85)); let attempts = 0; let stableRounds = 0; let lastTop = -1; scroller.scrollTop = 0; await sleep(450); while (attempts < 80) { const item = findVisibleConversationItem(username); if (item) { clickItem(item); log(`✅ 已跳转到会话:${username}`); setStatus(`已定位会话:${username}`); return; } const maxTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); const currentTop = scroller.scrollTop; if (currentTop >= maxTop - 4) { break; } const nextTop = Math.min(maxTop, currentTop + step); if (Math.abs(nextTop - lastTop) < 4) { stableRounds += 1; } else { stableRounds = 0; } if (stableRounds >= 3) { break; } lastTop = nextTop; scroller.scrollTo({ top: nextTop, behavior: "auto" }); attempts += 1; await sleep(320); } scroller.scrollTo({ top: initialTop, behavior: "auto" }); log(`⚠️ 未找到会话:${username}`); setStatus(`未找到会话:${username}`); } function updateRepliedUI() { const el = document.getElementById("xy-replied-list"); const countEl = document.getElementById("xy-replied-count"); const unreadEl = document.getElementById("xy-replied-unread"); if (!el) return; const list = loadReplied(); // 兼容旧 read:boolean 格式,统一到 unread 数字 const getUnread = (r) => r.unread !== undefined ? r.unread : r.read === false ? 1 : 0; const totalMsgs = list.reduce((s, r) => s + getUnread(r), 0); const unreadPeople = list.filter((r) => getUnread(r) > 0).length; if (countEl) countEl.textContent = list.length; if (unreadEl) { unreadEl.textContent = unreadPeople > 0 ? `人工未读 ${unreadPeople} 人 · 共 ${totalMsgs} 条` : "人工未读 0 条"; unreadEl.style.color = unreadPeople > 0 ? "#ff4d4f" : "#bbb"; } if (!list.length) { el.innerHTML = '
暂无回复记录
有自动回复后会显示在这里
'; return; } el.innerHTML = list .map((r) => { const n = getUnread(r); const isUnread = n > 0; const timeText = r.time ? r.time.slice(5, 16) : "--"; return `
${isUnread ? `人工未读 ${n}` : "已读"}
最近回复 ${esc(timeText)}
${ isUnread ? `` : "" }
`; }) .join(""); // 跳转事件 el.querySelectorAll(".xy-goto-user").forEach((button) => { button.addEventListener("click", async () => { await navigateToConversation(button.dataset.username); }); }); // 标记已读事件 el.querySelectorAll(".xy-mark-read").forEach((btn) => { btn.addEventListener("click", () => markRepliedRead(btn.dataset.username), ); }); } /* ═══════════════════════════════════════════ 1. 扫描未读会话 依据:列表项内存在 sup.ant-badge-count[data-show="true"] ═══════════════════════════════════════════ */ function findUnreadConversations() { const results = []; const items = document.querySelectorAll('[class*="conversation-item--"]'); for (const item of items) { // 角标存在且 data-show="true" 表示有未读消息 const badge = item.querySelector('sup.ant-badge-count[data-show="true"]'); if (!badge) continue; // 取用户名(依赖 overflow:hidden + text-overflow:ellipsis + white-space:nowrap 样式) const username = getConversationNameFromItem(item) || "未知用户"; results.push({ item, username }); } return results; } /* ═══════════════════════════════════════════ 2. 提取当前打开对话的聊天记录 direction: ltr → 买家(user) direction: rtl → 我(assistant) ═══════════════════════════════════════════ */ function extractChatHistory() { const messages = []; const listItems = document.querySelectorAll("li.ant-list-item"); for (const li of listItems) { const style = li.getAttribute("style") || ""; const isMe = style.includes("direction: rtl"); const isBuyer = style.includes("direction: ltr"); if (!isMe && !isBuyer) continue; // 系统消息/卡片,跳过 const textEl = li.querySelector('[class*="message-text--"]'); if (!textEl) continue; const text = textEl.textContent?.trim(); if (!text) continue; messages.push({ role: isMe ? "assistant" : "user", content: text }); } // 最多保留最近 20 条,避免超出 token 限制 return messages.slice(-20); } /* ═══════════════════════════════════════════ 3. 调用 OpenAI Chat Completions API ═══════════════════════════════════════════ */ function callOpenAI(history) { const apiKey = store.get(KEY_API_KEY) || ""; if (!apiKey) return Promise.reject(new Error("API Key 未设置,请在面板中填写")); const baseUrl = (store.get(KEY_API_BASE) || DEFAULT_BASE).replace( /\/$/, "", ); const model = store.get(KEY_MODEL) || DEFAULT_MODEL; const now = new Date(Date.now() + 8 * 3600_000); const nowStr = now.toISOString().replace("T", " ").slice(0, 19) + " (UTC+8)"; const sysPrompt = (store.get(KEY_PROMPT) || DEFAULT_PROMPT) + `\n\n当前时间:${nowStr}`; const payload = { model, messages: [{ role: "system", content: sysPrompt }, ...history], max_tokens: 400, temperature: 0.7, }; const url = `${baseUrl}/chat/completions`; const headers = { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }; // 优先用 GM_xmlhttpRequest(可绕过 CORS 和内容安全策略) if (typeof GM_xmlhttpRequest === "function") { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url, headers, data: JSON.stringify(payload), timeout: 30_000, onload(res) { try { const data = JSON.parse(res.responseText); if (data.error) return reject(new Error(data.error.message)); const content = data.choices?.[0]?.message?.content?.trim(); if (!content) return reject(new Error("API 返回空内容")); resolve(content); } catch (e) { reject( new Error("解析响应失败: " + res.responseText.slice(0, 200)), ); } }, onerror: () => reject(new Error("网络请求失败")), ontimeout: () => reject(new Error("请求超时(30s)")), }); }); } // 降级到 fetch(可能受 CORS 限制) return fetch(url, { method: "POST", headers, body: JSON.stringify(payload), }) .then((r) => r.json()) .then((data) => { if (data.error) throw new Error(data.error.message); const content = data.choices?.[0]?.message?.content?.trim(); if (!content) throw new Error("API 返回空内容"); return content; }); } /* ═══════════════════════════════════════════ 4. 向当前对话发送消息 targetUsername: 发送前校验当前激活会话是否仍是目标用户, 防止会话切换导致消息发错房间。 ═══════════════════════════════════════════ */ async function sendMessage(text, targetUsername) { text = text.trim(); if (!text) return; const replySuffix = (store.get(KEY_REPLY_SUFFIX) ?? DEFAULT_REPLY_SUFFIX).trim(); if (replySuffix) { text += `\n\n${replySuffix}`; } // 校验当前激活会话仍是目标用户(允许为空则跳过校验) if (targetUsername) { const activeNow = getActiveUsername(); if (activeNow && activeNow !== targetUsername) { throw new Error( `会话已切换(当前: ${activeNow},目标: ${targetUsername}),放弃发送`, ); } } const textarea = await waitFor("textarea.ant-input", document, 6000); textarea.focus(); // 通过原生 setter 触发 React 受控组件 const nativeSetter = Object.getOwnPropertyDescriptor( HTMLTextAreaElement.prototype, "value", )?.set; if (nativeSetter) nativeSetter.call(textarea, text); else textarea.value = text; textarea.dispatchEvent(new Event("input", { bubbles: true })); await sleep(500); // 发送前再次校验(AI 调用期间可能已切换会话) if (targetUsername) { const activeNow = getActiveUsername(); if (activeNow && activeNow !== targetUsername) { // 清空输入框,防止残留内容 const setter2 = Object.getOwnPropertyDescriptor( HTMLTextAreaElement.prototype, "value", )?.set; if (setter2) setter2.call(textarea, ""); textarea.dispatchEvent(new Event("input", { bubbles: true })); throw new Error( `发送前会话已切换(当前: ${activeNow},目标: ${targetUsername}),已取消`, ); } } // 等待发送按钮可用(最多 4.5 秒) let sendBtn = null; for (let i = 0; i < 15; i++) { sendBtn = document.querySelector( '[class*="sendbox-bottom"] button:not([disabled])', ); if (sendBtn) break; await sleep(300); } if (sendBtn) { sendBtn.click(); } else { // 备用:回车发送 textarea.dispatchEvent( new KeyboardEvent("keydown", { key: "Enter", keyCode: 13, bubbles: true, }), ); } await sleep(1200); } /* ═══════════════════════════════════════════ 5a. 当前激活会话监听(无角标时兜底) 当某个会话已被选中时,新消息不会出现角标, 改用对话内容指纹(最后一条买家消息)跟踪变化。 ═══════════════════════════════════════════ */ // 记录上次已处理的"最后一条买家消息"指纹(内容 + 消息总条数) let lastActiveKey = ""; // 防止对同一条消息重复触发 AI 调用 let activeReplying = false; /** * 全局串行发送锁:确保同一时刻只有一个"AI 调用 + 发送"流程在运行, * 彻底杜绝两条路径并发写输入框导致消息串联的问题。 */ let replyQueue = Promise.resolve(); function enqueueReply(fn) { replyQueue = replyQueue.then(() => fn().catch(() => {})); return replyQueue; } /** Observer 触发的 debounce 定时器 */ let activeCheckTimer = null; function scheduleActiveCheck() { clearTimeout(activeCheckTimer); activeCheckTimer = setTimeout(() => { enqueueReply(() => checkActiveConversation()); }, 800); } /** 获取当前激活会话的用户名(带 conversation-item-active 类的列表项) */ function getActiveUsername() { const activeItem = document.querySelector( '[class*="conversation-item-active--"]', ); if (!activeItem) return null; return getConversationNameFromItem(activeItem) || "当前会话"; } /** * 检查当前打开的会话是否有买家新消息未回复。 * 若检测到新消息指纹与上次不同,且最后一条是买家消息,则触发 AI 回复。 */ async function checkActiveConversation() { if (activeReplying) return; if (store.get(KEY_ENABLED) === "false") return; if (!store.get(KEY_API_KEY)) return; const history = extractChatHistory(); if (!history.length) return; const last = history[history.length - 1]; if (last.role !== "user") return; // 最后是我发的,无需处理 // 指纹:最后一条买家消息内容 + 消息总数,任一变化即视为新消息 const key = `${history.length}::${last.content}`; if (key === lastActiveKey) return; // 已处理过该消息 lastActiveKey = key; activeReplying = true; const username = getActiveUsername() || "当前会话"; log(`📩 当前会话收到新消息(${username}),调用 AI…`); setStatus("AI 生成中…"); try { const reply = await callOpenAI(history); log(`💬 ${reply.length > 35 ? reply.slice(0, 35) + "…" : reply}`); await sendMessage(reply, username); log(`✅ 已回复当前会话:${username}`); addReplied(username); } catch (e) { log(`❌ 当前会话回复失败:${e.message}`); // 失败时重置指纹,下次扫描可重试 lastActiveKey = ""; } finally { activeReplying = false; setStatus(`上次扫描:${new Date().toLocaleTimeString()}`); } } /** 启动对当前聊天消息列表的 MutationObserver,实时感知新消息 */ function watchActiveChatMessages() { let currentListEl = null; let listObserver = null; function attachToList(listEl) { if (listEl === currentListEl) return; listObserver?.disconnect(); currentListEl = listEl; listObserver = new MutationObserver(() => { // 新 li 节点加入时触发检查,debounce 800ms 等 React 渲染稳定 scheduleActiveCheck(); }); listObserver.observe(listEl, { childList: true, subtree: true }); } // 外层观察器:等待 .ant-list-items 容器出现(会话切换时容器会重建) new MutationObserver(() => { const listEl = document.querySelector("ul.ant-list-items"); if (listEl) attachToList(listEl); }).observe(document.body, { childList: true, subtree: true }); // 立即尝试挂载(页面已加载的情况) const listEl = document.querySelector("ul.ant-list-items"); if (listEl) attachToList(listEl); } /* ═══════════════════════════════════════════ 5. 主扫描循环 ═══════════════════════════════════════════ */ let scanning = false; async function runScan() { if (scanning) return; if (store.get(KEY_ENABLED) === "false") return; if (!store.get(KEY_API_KEY)) { setStatus("⚠️ 请先在 API 面板填写 API Key"); return; } scanning = true; setStatus("检测中…"); try { // 先处理当前激活会话(无角标场景兜底),纳入串行队列 await new Promise((resolve) => enqueueReply(async () => { await checkActiveConversation(); resolve(); }), ); const unread = findUnreadConversations(); if (!unread.length) { setStatus(`无未读 · ${new Date().toLocaleTimeString()}`); return; } log(`发现 ${unread.length} 个未读会话`); for (const { item, username } of unread) { log(`打开会话:${username}`); // 切换会话前重置激活会话指纹,避免残留状态误判 lastActiveKey = ""; item.click(); // 等待消息列表渲染 try { await waitFor('[class*="message-text--"]', document, 5000); } catch { log(`⚠️ 消息加载超时,跳过:${username}`); continue; } await sleep(800); // 等剩余消息渲染 // 提取聊天记录 const history = extractChatHistory(); if (!history.length) { log(`⚠️ 无法获取聊天记录:${username}`); continue; } // 最后一条必须是买家消息,否则已回复过,跳过 const lastMsg = history[history.length - 1]; if (lastMsg?.role !== "user") { log(`最新消息是我发的,已跳过:${username}`); continue; } // 立即锁定指纹 + 标记正在回复,防止 Observer 路径在 OpenAI 调用期间并发触发 lastActiveKey = `${history.length}::${lastMsg.content}`; activeReplying = true; log(`🤖 调用 AI 生成回复…`); setStatus("AI 生成中…"); let reply; try { reply = await callOpenAI(history); } catch (e) { activeReplying = false; lastActiveKey = ""; // 失败时重置,允许下轮重试 log(`❌ API 错误:${e.message}`); setStatus(`API 错误 · ${new Date().toLocaleTimeString()}`); continue; } log(`💬 ${reply.length > 35 ? reply.slice(0, 35) + "…" : reply}`); try { await sendMessage(reply, username); log(`✅ 已发送给 ${username}`); addReplied(username); } catch (e) { log(`❌ 发送失败:${e.message}`); lastActiveKey = ""; // 发送失败时重置,允许下轮重试 } finally { activeReplying = false; } await sleep(1000); } } catch (e) { log(`扫描异常:${e.message}`); } finally { scanning = false; setStatus(`上次扫描:${new Date().toLocaleTimeString()}`); } } /* ═══════════════════════════════════════════ 6. 控制面板 UI ═══════════════════════════════════════════ */ function createPanel() { if (document.getElementById("xianyu-ai-panel")) return; const enabled = store.get(KEY_ENABLED) !== "false"; const apiKey = store.get(KEY_API_KEY) || ""; const apiBase = store.get(KEY_API_BASE) || DEFAULT_BASE; const model = store.get(KEY_MODEL) || DEFAULT_MODEL; const prompt = store.get(KEY_PROMPT) || DEFAULT_PROMPT; const replySuffix = store.get(KEY_REPLY_SUFFIX) ?? DEFAULT_REPLY_SUFFIX; const panel = document.createElement("div"); panel.id = "xianyu-ai-panel"; panel.style.cssText = ` position: fixed; bottom: 24px; right: 24px; width: 330px; background: #fff; border: 1px solid #dde3ee; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.16); z-index: 2147483647; font-family: -apple-system, "PingFang SC", "Helvetica Neue", sans-serif; font-size: 13px; user-select: none; overflow: hidden; `; panel.innerHTML = `
🤖 闲鱼 AI 智能客服
`; document.body.appendChild(panel); /* ── 拖拽 ── */ const hdr = document.getElementById("xy-hdr"); hdr.addEventListener("mousedown", (e) => { if (e.target.closest("label, input")) return; e.preventDefault(); const dx = e.clientX - panel.offsetLeft; const dy = e.clientY - panel.offsetTop; const onMove = (e) => { panel.style.left = `${e.clientX - dx}px`; panel.style.top = `${e.clientY - dy}px`; panel.style.bottom = "auto"; panel.style.right = "auto"; }; const onUp = () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); }; window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }); /* ── 标签切换 ── */ panel.querySelectorAll(".xy-tab").forEach((btn) => { btn.addEventListener("click", () => { panel.querySelectorAll(".xy-tab").forEach((t) => { t.style.fontWeight = "400"; t.style.color = "#999"; t.style.borderBottom = "2px solid transparent"; }); panel.querySelectorAll(".xy-panel").forEach((p) => { p.style.display = "none"; }); btn.style.fontWeight = "600"; btn.style.color = "#6C63FF"; btn.style.borderBottom = "2px solid #6C63FF"; document.getElementById(`xy-panel-${btn.dataset.tab}`).style.display = "block"; }); }); /* ── 启用 / 暂停 ── */ document.getElementById("xy-enabled").addEventListener("change", (e) => { store.set(KEY_ENABLED, e.target.checked); document.getElementById("xy-enabled-label").textContent = e.target.checked ? "运行中" : "已暂停"; log(e.target.checked ? "▶ 智能回复已恢复" : "⏸ 智能回复已暂停"); }); /* ── 保存 API 设置 ── */ document.getElementById("xy-save-api").addEventListener("click", () => { const key = document.getElementById("xy-api-key").value.trim(); const base = document.getElementById("xy-api-base").value.trim(); const mdl = document.getElementById("xy-model").value.trim(); if (!key) { log("❌ API Key 不能为空"); return; } store.set(KEY_API_KEY, key); store.set(KEY_API_BASE, base || DEFAULT_BASE); store.set(KEY_MODEL, mdl || DEFAULT_MODEL); log("✅ API 设置已保存"); panel.querySelector('[data-tab="log"]').click(); }); /* ── 保存 Prompt ── */ document.getElementById("xy-save-prompt").addEventListener("click", () => { const p = document.getElementById("xy-prompt").value; const suffix = document.getElementById("xy-reply-suffix").value; store.set(KEY_PROMPT, p); store.set(KEY_REPLY_SUFFIX, suffix); log("✅ Prompt 和回复小尾巴已保存"); panel.querySelector('[data-tab="log"]').click(); }); /* ── 立即扫描 ── */ document.getElementById("xy-scan-now").addEventListener("click", () => { panel.querySelector('[data-tab="log"]').click(); runScan(); }); /* ── 清空回复记录 ── */ document .getElementById("xy-clear-replied") .addEventListener("click", () => { saveReplied([]); updateRepliedUI(); }); // 初始化已回复列表 updateRepliedUI(); } /* ═══════════════════════════════════════════ 日志 & 状态 ═══════════════════════════════════════════ */ function setStatus(text) { const el = document.getElementById("xy-status"); if (el) el.textContent = text; } const logLines = []; function log(msg) { console.log("[闲鱼AI]", msg); const time = new Date().toLocaleTimeString("zh-CN", { hour12: false }); logLines.unshift(`${time} ${esc(msg)}`); if (logLines.length > 50) logLines.length = 50; const el = document.getElementById("xy-log"); if (el) el.innerHTML = logLines.map((l) => `
${l}
`).join(""); } /* ═══════════════════════════════════════════ 入口 ═══════════════════════════════════════════ */ async function init() { await sleep(3200); // 等页面稳定 createPanel(); watchActiveChatMessages(); // 启动激活会话实时监听 log("脚本已启动"); setInterval(() => { if (store.get(KEY_ENABLED) !== "false") runScan(); }, SCAN_INTERVAL); } if (document.readyState === "complete") { init(); } else { window.addEventListener("load", init); } })();