// ==UserScript== // @name AI 管家 · 记忆助手 // @namespace ai-hub // @version 0.2.0 // @description 自动采集 ChatGPT / DeepSeek / 豆包网页版 的对话存入本机记忆库(127.0.0.1:8765),任意 AI 里都能搜索记忆并一键注入输入框。数据只发往本机。 // @author AI Hub // @match https://chatgpt.com/* // @match https://chat.openai.com/* // @match https://chat.deepseek.com/* // @match https://www.doubao.com/* // @match https://doubao.com/* // @run-at document-idle // @noframes // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @connect 127.0.0.1 // @connect localhost // ==/UserScript== /* eslint-disable no-empty */ (function () { "use strict"; /* ============================================================ * 0. 基础配置 * ============================================================ */ var DEFAULT_API = "http://127.0.0.1:8765"; var API_BASE = DEFAULT_API; try { API_BASE = (GM_getValue("aihub_api", DEFAULT_API) || DEFAULT_API).replace(/\/+$/, ""); } catch (e) {} var QUEUE_KEY = "aihub_queue"; var AUTO_KEY = "aihub_auto"; var MAX_QUEUE = 300; function log() { var args = Array.prototype.slice.call(arguments); args.unshift("[AI 管家]"); console.log.apply(console, args); } function warn() { var args = Array.prototype.slice.call(arguments); args.unshift("[AI 管家]"); console.warn.apply(console, args); } function getValue(key, def) { try { return GM_getValue(key, def); } catch (e) { return def; } } function setValue(key, val) { try { GM_setValue(key, val); } catch (e) {} } /* ============================================================ * 1. 本地服务通信(GM_xmlhttpRequest 优先,fetch 兜底) * ============================================================ */ function request(opts) { // opts: {method, path, body, timeout} var url = API_BASE + opts.path; var method = opts.method || "GET"; var timeout = opts.timeout || 6000; var data = opts.body ? JSON.stringify(opts.body) : null; return new Promise(function (resolve) { var done = function (ok, payload) { resolve({ ok: ok, data: payload }); }; if (typeof GM_xmlhttpRequest === "function") { try { GM_xmlhttpRequest({ method: method, url: url, data: data, headers: data ? { "Content-Type": "application/json" } : undefined, timeout: timeout, onload: function (r) { var parsed = null; try { parsed = JSON.parse(r.responseText); } catch (e) { parsed = null; } done(r.status >= 200 && r.status < 300 && parsed !== null, parsed); }, onerror: function () { done(false, null); }, ontimeout: function () { done(false, null); }, onabort: function () { done(false, null); }, }); return; } catch (e) { warn("GM_xmlhttpRequest 异常,改用 fetch:", e); } } try { var init = { method: method }; if (data) { init.headers = { "Content-Type": "application/json" }; init.body = data; } fetch(url, init) .then(function (r) { return r.json(); }) .then(function (j) { done(true, j); }) .catch(function () { done(false, null); }); } catch (e) { done(false, null); } }); } function health() { return request({ path: "/api/health" }); } function postMemories(payload) { return request({ method: "POST", path: "/api/memories", body: payload }); } function searchMemories(q, limit) { return request({ path: "/api/memories?q=" + encodeURIComponent(q) + "&limit=" + (limit || 12) }); } function fetchStats() { return request({ path: "/api/stats" }); } /* ---- 离线队列:服务没开时先攒着,服务起来后自动补发 ---- */ function readQueue() { var q = getValue(QUEUE_KEY, []); return Array.isArray(q) ? q : []; } function pushQueue(item) { var q = readQueue(); q.push(item); while (q.length > MAX_QUEUE) q.shift(); setValue(QUEUE_KEY, q); } function clearQueue() { setValue(QUEUE_KEY, []); } var flushing = false; function flushQueue() { if (flushing) return; var q = readQueue(); if (!q.length) return; flushing = true; postMemories(q[0]).then(function (r) { flushing = false; if (r.ok && r.data && r.data.ok) { var rest = readQueue(); rest.shift(); setValue(QUEUE_KEY, rest); log("离线补发成功,剩余 " + rest.length + " 批"); if (rest.length) setTimeout(flushQueue, 400); } }); } function sendToServer(payload) { return postMemories(payload).then(function (r) { if (r.ok && r.data && r.data.ok) { if (r.data.added > 0) log("已存入 " + r.data.added + " 条新记忆 → " + payload.source); flushQueue(); return { ok: true, added: r.data.added || 0, deduped: r.data.deduped || 0 }; } pushQueue(payload); warn("本地服务未响应,已暂存到离线队列(共 " + readQueue().length + " 批)"); return { ok: false, added: 0, deduped: 0 }; }); } /* ============================================================ * 2. 站点适配器 * ============================================================ */ function cleanText(s) { return String(s || "") .replace(/\u00a0/g, " ") .replace(/\r\n?/g, "\n") .split("\n") .map(function (l) { return l.replace(/[ \t]+/g, " ").trim(); }) .filter(Boolean) .join("\n") .trim(); } function uniqMessages(msgs) { var seen = {}; var out = []; msgs.forEach(function (m) { var c = (m.content || "").trim(); if (!c) return; var k = m.role + "\u0000" + c; if (seen[k]) return; seen[k] = 1; out.push({ role: m.role, content: c }); }); return out; } /* ---- 通用兜底:按 DOM 顺序抓「消息块」,估算角色 ---- */ function genericExtract() { var out = []; var nodes = Array.prototype.slice.call( document.querySelectorAll( "[data-message-author-role],[data-testid*='message'],[class*='message-bubble'],[class*='markdown']" ) ); // 只保留最外层消息节点,避免父子重复 var top = nodes.filter(function (n) { return !nodes.some(function (o) { return o !== n && o.contains(n); }); }); top.forEach(function (n) { var text = cleanText(n.innerText); if (!text || text.length < 1) return; var role = n.getAttribute("data-message-author-role") || ""; if (!role) { var testid = (n.getAttribute("data-testid") || "").toLowerCase(); var cls = (n.className && String(n.className) || "").toLowerCase(); if (/assistant|receive|ai|bot/.test(testid) || /assistant|receive|markdown/.test(cls)) role = "assistant"; else if (/user|send/.test(testid) || /user|send/.test(cls)) role = "user"; else role = "unknown"; } if (role === "system") return; out.push({ role: role, content: text }); }); return uniqMessages(out); } function findByRoleAttr() { var out = []; Array.prototype.slice.call(document.querySelectorAll("[data-message-author-role]")).forEach(function (el) { var role = el.getAttribute("data-message-author-role"); if (!role || role === "system") return; var text = cleanText(el.innerText); if (text) out.push({ role: role, content: text }); }); return uniqMessages(out); } var ADAPTERS = { /* ---------------- ChatGPT ---------------- */ "chatgpt.com": { hosts: ["chatgpt.com", "chat.openai.com"], label: "ChatGPT", source: "gpt", extract: function () { var byRole = findByRoleAttr(); if (byRole.length) return byRole; return genericExtract(); }, getInputBox: function () { return document.querySelector("#prompt-textarea") || document.querySelector("div[contenteditable='true'].ProseMirror") || document.querySelector("div[contenteditable='true']") || document.querySelector("textarea"); }, }, /* ---------------- DeepSeek ---------------- */ "chat.deepseek.com": { hosts: ["chat.deepseek.com"], label: "DeepSeek", source: "deepseek", extract: function () { // 策略 1:官方 role 标记(部分版本有) var byRole = findByRoleAttr(); if (byRole.length) return byRole; // 策略 2:助手回答固定是 .ds-markdown;用户提问是它前面的兄弟块 var mds = Array.prototype.slice.call(document.querySelectorAll(".ds-markdown")); if (mds.length) { var out = []; mds.forEach(function (md) { var wrap = md; for (var i = 0; i < 4; i++) { var p = wrap.parentElement; if (!p || p === document.body) break; if (p.children.length > 1 || /message|chat|item/i.test(p.className || "")) break; wrap = p; } var prev = wrap.previousElementSibling; if (prev && !prev.querySelector(".ds-markdown")) { var ut = cleanText(prev.innerText); if (ut && ut.length < 5000 && !/^(深度思考|联网搜索|内容由 AI 生成)/.test(ut)) { out.push({ role: "user", content: ut }); } } else if (prev) { var ut2 = cleanText(prev.innerText); if (ut2) out.push({ role: "user", content: ut2 }); } out.push({ role: "assistant", content: cleanText(md.innerText) }); }); var res = uniqMessages(out); if (res.length) return res; } // 策略 3:通用兜底 return genericExtract(); }, getInputBox: function () { return document.querySelector("textarea#chat-input") || document.querySelector("textarea") || document.querySelector("div[contenteditable='true']"); }, }, /* ---------------- 豆包网页版 ---------------- */ "www.doubao.com": { hosts: ["www.doubao.com", "doubao.com"], label: "豆包网页版", source: "doubao-web", extract: function () { var byRole = findByRoleAttr(); if (byRole.length) return byRole; var out = []; // 豆包的正文容器(不同版本类名会变,这里做组合匹配) var nodes = Array.prototype.slice.call(document.querySelectorAll( "[data-testid='message_text_content'],[data-testid*='message_text'],[class*='message-content'],[class*='messageContent'],[class*='markdown']" )); var top = nodes.filter(function (n) { return !nodes.some(function (o) { return o !== n && o.contains(n); }); }); top.forEach(function (n) { var text = cleanText(n.innerText); if (!text) return; var role = "unknown"; var host = n; for (var i = 0; i < 6 && host; i++) { var tid = (host.getAttribute && host.getAttribute("data-testid") || "").toLowerCase(); var cls = (host.className && String(host.className) || "").toLowerCase(); var mark = tid + " " + cls; if (/receive|assistant|answer|bot/.test(mark)) { role = "assistant"; break; } if (/send|user|question/.test(mark)) { role = "user"; break; } host = host.parentElement; } if (role === "unknown") role = /markdown/.test(String(n.className || "")) ? "assistant" : "unknown"; out.push({ role: role, content: text }); }); var res = uniqMessages(out); return res.length ? res : genericExtract(); }, getInputBox: function () { return document.querySelector("textarea[data-testid='chat_input_input']") || document.querySelector("textarea") || document.querySelector("div[contenteditable='true']"); }, }, }; var SITE = null; var ADAPTER = null; (function pickSite() { var host = location.hostname; // 自测/调试用:可在页面里设 window.__AIHUB_SITE__ 强制指定适配器 try { if (window.__AIHUB_SITE__) host = String(window.__AIHUB_SITE__); } catch (e) {} Object.keys(ADAPTERS).forEach(function (k) { if (!ADAPTER) { var a = ADAPTERS[k]; if (a.hosts.indexOf(host) >= 0) { SITE = k; ADAPTER = a; } } }); })(); if (!ADAPTER) { warn("当前站点未适配:" + location.hostname + "(脚本继续运行,但不会采集)"); } /* ============================================================ * 3. 输入框注入 * ============================================================ */ function injectText(text) { if (!ADAPTER) return false; var box = null; try { box = ADAPTER.getInputBox(); } catch (e) { box = null; } if (!box) return false; try { box.focus(); if (box.tagName === "TEXTAREA" || box.tagName === "INPUT") { var setter = Object.getOwnPropertyDescriptor( box.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype, "value" ); if (setter && setter.set) setter.set.call(box, text); else box.value = text; box.dispatchEvent(new Event("input", { bubbles: true })); box.dispatchEvent(new Event("change", { bubbles: true })); return true; } // contenteditable(ChatGPT / ProseMirror 等) box.focus(); var sel = window.getSelection(); var range = document.createRange(); range.selectNodeContents(box); sel.removeAllRanges(); sel.addRange(range); var ok = false; try { ok = document.execCommand("insertText", false, text); } catch (e) { ok = false; } if (!ok) { box.textContent = text; box.dispatchEvent(new InputEvent("input", { bubbles: true, data: text, inputType: "insertText", })); } return true; } catch (e) { warn("注入失败:", e); return false; } } /* ============================================================ * 4. 采集引擎 * ============================================================ */ var autoCapture = !!getValue(AUTO_KEY, true); var lastKey = ""; var debounceTimer = null; var lastResult = { at: null, count: 0, added: 0, ok: null }; function snapshotKey(messages) { return messages.map(function (m) { return m.role + ":" + m.content.length + ":" + m.content.slice(0, 40); }).join("|"); } function captureOnce(force) { if (!ADAPTER) return Promise.resolve(0); var messages; try { messages = ADAPTER.extract(); } catch (e) { warn("采集异常:", e); return Promise.resolve(0); } if (!messages || !messages.length) return Promise.resolve(0); var key = snapshotKey(messages); if (!force && key === lastKey) return Promise.resolve(0); lastKey = key; return sendToServer({ source: ADAPTER.source, messages: messages }).then(function (r) { lastResult = { at: new Date(), count: messages.length, added: r.added, ok: r.ok }; return messages.length; }); } function scheduleCapture() { if (!autoCapture) return; clearTimeout(debounceTimer); debounceTimer = setTimeout(function () { captureOnce(false); }, 1800); } function startObserver() { if (!document.body) return; var observer = new MutationObserver(scheduleCapture); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } /* ============================================================ * 5. 页面浮窗 UI(Shadow DOM,避免被站点样式污染) * ============================================================ */ var ui = {}; var ROOT_ID = "aihub-root"; var CSS = [ ":host{all:initial}", "*{box-sizing:border-box;font-family:'Microsoft YaHei',system-ui,-apple-system,sans-serif}", "#fab{position:fixed;right:22px;bottom:110px;z-index:2147483000;width:46px;height:46px;border-radius:50%;", "border:none;cursor:grab;background:linear-gradient(135deg,#5b8cff,#3a6bff);color:#fff;font-size:16px;", "font-weight:700;box-shadow:0 6px 18px rgba(58,107,255,.45);display:flex;align-items:center;justify-content:center}", "#fab:active{cursor:grabbing}", "#fab .badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;border-radius:8px;", "background:#e5484d;color:#fff;font-size:10px;line-height:16px;text-align:center;padding:0 4px;display:none}", "#panel{position:fixed;right:22px;bottom:166px;z-index:2147483001;width:322px;max-height:70vh;", "background:#fff;color:#1f2329;border-radius:14px;box-shadow:0 10px 34px rgba(0,0,0,.2);", "display:none;flex-direction:column;overflow:hidden;border:1px solid #e5e6eb}", "#panel.open{display:flex}", ".head{padding:10px 12px;border-bottom:1px solid #f0f1f3;display:flex;justify-content:space-between;align-items:center}", ".head b{font-size:13px;color:#3a6bff}", ".status{font-size:11px;color:#8f959e;display:flex;align-items:center;gap:5px}", ".dot{width:7px;height:7px;border-radius:50%;background:#c9cdd4;display:inline-block}", ".dot.ok{background:#4caf7d}.dot.bad{background:#e05b5b}", ".body{padding:10px 12px 6px;display:flex;gap:6px}", "input.q{flex:1;min-width:0;padding:7px 9px;font-size:13px;border:1px solid #d9dbe0;border-radius:7px;outline:none;color:#1f2329;background:#fff}", "input.q:focus{border-color:#5b8cff}", "button{cursor:pointer;font-size:12px;border-radius:7px;border:none;padding:7px 10px;white-space:nowrap;", "background:#5b8cff;color:#fff}", "button.ghost{background:#f2f3f5;color:#4e5969}", "button:disabled{opacity:.5;cursor:default}", ".list{margin:4px 12px 10px;overflow-y:auto;max-height:34vh}", ".item{padding:8px 10px;border:1px solid #eef0f2;border-radius:9px;margin-bottom:6px;cursor:pointer;", "font-size:12px;line-height:1.5;word-break:break-word}", ".item:hover{border-color:#5b8cff;background:#f6f9ff}", ".item .tag{font-size:10px;color:#8f959e;margin-right:6px}", ".empty{color:#8f959e;text-align:center;padding:14px 0;font-size:12px}", ".foot{padding:8px 12px;border-top:1px solid #f0f1f3;font-size:11px;color:#8f959e;", "display:flex;justify-content:space-between;align-items:center;gap:6px;flex-wrap:wrap}", ".foot label{display:flex;align-items:center;gap:4px;cursor:pointer}", ".foot a{color:#3a6bff;text-decoration:none}", ".tools{display:flex;gap:6px;padding:0 12px 10px;flex-wrap:wrap}", ".tools button{font-size:11px;padding:5px 8px}", ".toast{margin:0 12px 10px;font-size:11px;color:#4e5969;background:#f7f8fa;border-radius:7px;padding:6px 9px;", "line-height:1.5;word-break:break-all;display:none}", ".toast.show{display:block}", ].join(""); function el(tag, cls, text) { var n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; } function buildUI() { if (document.getElementById(ROOT_ID)) return; var host = document.createElement("div"); host.id = ROOT_ID; host.style.cssText = "all:initial"; var shadow = host.attachShadow({ mode: "open" }); var style = document.createElement("style"); style.textContent = CSS; shadow.appendChild(style); var fab = el("button"); fab.id = "fab"; fab.title = "AI 管家 · 跨 AI 记忆"; fab.textContent = "AI"; var badge = el("span", "badge"); fab.appendChild(badge); var panel = el("div"); panel.id = "panel"; var head = el("div", "head"); var title = el("b", null, "AI 管家 · 记忆"); var status = el("span", "status"); var dot = el("span", "dot"); dot.id = "aihub-dot"; var statusText = el("span", null, "检测中…"); statusText.id = "aihub-status"; status.appendChild(dot); status.appendChild(statusText); head.appendChild(title); head.appendChild(status); var body = el("div", "body"); var qInput = el("input", "q"); qInput.id = "aihub-q"; qInput.placeholder = "搜记忆,点结果注入输入框"; var searchBtn = el("button", null, "搜索"); searchBtn.id = "aihub-search"; body.appendChild(qInput); body.appendChild(searchBtn); var list = el("div", "list"); list.id = "aihub-list"; var empty = el("div", "empty", "输入关键词后点「搜索」,或先随便聊聊让记忆积累起来"); list.appendChild(empty); var tools = el("div", "tools"); var capNow = el("button", "ghost", "立即采集本页"); capNow.id = "aihub-cap-now"; var capSel = el("button", "ghost", "采集选中文字"); capSel.id = "aihub-cap-sel"; var dashboard = el("button", "ghost", "打开管理面板"); dashboard.id = "aihub-dash"; tools.appendChild(capNow); tools.appendChild(capSel); tools.appendChild(dashboard); var toast = el("div", "toast"); toast.id = "aihub-toast"; var foot = el("div", "foot"); var autoLabel = el("label"); var autoBox = el("input"); autoBox.type = "checkbox"; autoBox.id = "aihub-auto"; autoLabel.appendChild(autoBox); autoLabel.appendChild(document.createTextNode(" 自动采集")); var siteSpan = el("span", null, ADAPTER ? ADAPTER.label : "未适配站点"); var installLink = el("a"); installLink.href = API_BASE + "/install"; installLink.target = "_blank"; installLink.textContent = "安装/更新脚本"; foot.appendChild(autoLabel); foot.appendChild(siteSpan); foot.appendChild(installLink); panel.appendChild(head); panel.appendChild(body); panel.appendChild(list); panel.appendChild(tools); panel.appendChild(toast); panel.appendChild(foot); shadow.appendChild(fab); shadow.appendChild(panel); document.documentElement.appendChild(host); ui = { host: host, shadow: shadow, fab: fab, panel: panel, dot: dot, statusText: statusText, qInput: qInput, searchBtn: searchBtn, list: list, toast: toast, autoBox: autoBox, badge: badge, capNow: capNow, capSel: capSel, }; /* ---- 交互 ---- */ autoBox.checked = autoCapture; autoBox.addEventListener("change", function () { autoCapture = autoBox.checked; setValue(AUTO_KEY, autoCapture); showToast(autoCapture ? "自动采集已开启" : "自动采集已关闭"); }); fab.addEventListener("click", function () { if (fab.dataset.dragged === "1") return; // 刚刚是拖动,不当作点击 var open = panel.classList.toggle("open"); if (open) { refreshHealth(); qInput.focus(); } }); searchBtn.addEventListener("click", doSearch); qInput.addEventListener("keydown", function (e) { if (e.key === "Enter") doSearch(); }); capNow.addEventListener("click", function () { capNow.disabled = true; captureOnce(true).then(function (n) { capNow.disabled = false; if (!n) { showToast("本页没抓到消息。可选中文字后用「采集选中文字」,或到控制台看 [AI 管家] 日志。"); return; } showToast("已采集 " + n + " 条消息" + (lastResult.ok ? ",新增 " + lastResult.added + " 条" : ";本地服务未启动,已暂存待补发")); refreshHealth(); }); }); capSel.addEventListener("click", function () { var text = cleanText(window.getSelection && window.getSelection().toString()); if (!text) { showToast("先在页面上选中一段文字,再点这个按钮。"); return; } sendToServer({ source: (ADAPTER ? ADAPTER.source : "web") + "-selected", messages: [{ role: "note", content: text }] }) .then(function (r) { showToast(r.ok ? "已存入记忆库" : "本地服务未启动,已暂存待补发"); }); }); dashboard.addEventListener("click", function () { window.open(API_BASE + "/", "_blank"); }); makeDraggable(fab); refreshHealth(); setInterval(refreshHealth, 15000); flushQueue(); setInterval(flushQueue, 30000); } function showToast(msg) { if (!ui.toast) return; ui.toast.textContent = msg; ui.toast.classList.add("show"); clearTimeout(showToast._t); showToast._t = setTimeout(function () { ui.toast.classList.remove("show"); }, 4200); } function refreshHealth() { health().then(function (r) { if (!ui.dot) return; var pending = readQueue().length; if (r.ok && r.data && r.data.ok) { ui.dot.className = "dot ok"; ui.statusText.textContent = "已连接 · v" + (r.data.version || "?") + (pending ? " · 待补发 " + pending : ""); ui.badge.style.display = "none"; } else { ui.dot.className = "dot bad"; ui.statusText.textContent = "服务未启动" + (pending ? " · 待补发 " + pending : ""); if (pending) { ui.badge.textContent = String(pending); ui.badge.style.display = "block"; } } }); } function doSearch() { var q = ui.qInput.value.trim(); if (!q) return; ui.list.innerHTML = ""; ui.list.appendChild(el("div", "empty", "搜索中…")); searchMemories(q).then(function (r) { var rows = (r.data && r.data.memories) || []; ui.list.innerHTML = ""; if (!rows.length) { ui.list.appendChild(el("div", "empty", r.ok ? "没有匹配的记忆" : "本地服务未启动,无法检索")); return; } rows.forEach(function (m) { var item = el("div", "item"); var tag = el("span", "tag", (m.source || "?") + "/" + (m.role || "?")); item.appendChild(tag); var text = String(m.content || ""); item.appendChild(document.createTextNode(text.length > 110 ? text.slice(0, 110) + "…" : text)); item.title = "点击注入到当前输入框\n\n" + text; item.addEventListener("click", function () { if (injectText(text)) { ui.panel.classList.remove("open"); showToast("已注入到输入框,确认后按回车发送"); } else { showToast("没找到输入框,请手动复制:" + text.slice(0, 40) + "…"); } }); ui.list.appendChild(item); }); }); } function makeDraggable(node) { var dragging = false, moved = false, sx = 0, sy = 0, ox = 0, oy = 0; node.addEventListener("mousedown", function (e) { dragging = true; moved = false; var rect = node.getBoundingClientRect(); sx = e.clientX; sy = e.clientY; ox = rect.left; oy = rect.top; node.style.right = "auto"; node.style.bottom = "auto"; node.style.left = ox + "px"; node.style.top = oy + "px"; e.preventDefault(); }); document.addEventListener("mousemove", function (e) { if (!dragging) return; moved = true; node.style.left = (ox + e.clientX - sx) + "px"; node.style.top = (oy + e.clientY - sy) + "px"; }); document.addEventListener("mouseup", function () { if (!dragging) return; dragging = false; if (moved) { node.dataset.dragged = "1"; setTimeout(function () { node.dataset.dragged = ""; }, 300); } }); node.addEventListener("click", function (e) { if (node.dataset.dragged === "1") e.stopImmediatePropagation(); }, true); } /* ============================================================ * 6. 启动 * ============================================================ */ function boot() { buildUI(); startObserver(); setTimeout(function () { captureOnce(false); }, 3500); // 单页应用切会话时,body 可能被重建,兜底重建 UI setInterval(function () { if (!document.getElementById(ROOT_ID)) buildUI(); }, 8000); log("已加载 v0.2.0 · 站点:" + (ADAPTER ? ADAPTER.label : location.hostname)); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } if (typeof GM_registerMenuCommand === "function") { try { GM_registerMenuCommand("AI 管家:立即采集本页", function () { captureOnce(true); }); GM_registerMenuCommand("AI 管家:打开管理面板", function () { window.open(API_BASE + "/", "_blank"); }); } catch (e) {} } })();