// ==UserScript== // @name X Reply AI - 自动回复助手 // @namespace https://github.com/x-reply-ai // @version 1.2.4 // @description X 推文自动回复助手,支持多 AI 服务商,可设置浏览量/评论数/分享数限制 // @author X Reply AI Team // @match https://x.com/* // @match https://twitter.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_notification // @grant unsafeWindow // @license MIT // ==/UserScript== (function() { 'use strict'; // ============================================ // 存储适配层(Chrome Storage -> GM_*) // ============================================ const chrome = { storage: { local: { get: (keys, callback) => { if (typeof keys === 'string') keys = [keys]; const result = {}; keys.forEach(key => { const val = GM_getValue(key, null); if (val !== null) result[key] = val; }); if (callback) callback(result); return Promise.resolve(result); }, set: (items, callback) => { Object.entries(items).forEach(([key, value]) => { GM_setValue(key, value); }); if (callback) callback(); return Promise.resolve(); }, remove: (keys, callback) => { if (typeof keys === 'string') keys = [keys]; keys.forEach(key => GM_setValue(key, undefined)); if (callback) callback(); return Promise.resolve(); } }, onChanged: { addListener: () => { // 脚本猫不支持存储变化监听,忽略 console.log('[X Reply AI] 存储变化监听不可用'); } } } }; // ============================================ // AI 调用适配层(chrome.runtime -> GM_xmlhttpRequest) // ============================================ const AI_API = { SYSTEM_PROMPT: `你是评论区老司机。根据产品和帖子内容,写一条混在评论区里的"神回复"。 核心指令: language 必须与帖子完全一致,像在路边偶遇网友聊天。 禁止:禁止自我介绍、禁止用引号、禁止列要点、禁止说"总之/综上所述"。 文风:句子要短,多用语气词(啊、呢、吧、哒、呢~),可以像碎碎念,直接说感受或扔出产品这个"亲测好物"。 潜规则:假装自己刚用过这产品,把产品当作解决帖子里问题的"顺带一提"。 字数:40-80 字,拒绝长篇大论。`, PROVIDER_CONFIGS: { deepseek: { name: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", models: [ { id: "deepseek-chat", name: "DeepSeek V3", recommended: true }, { id: "deepseek-reasoner", name: "DeepSeek R1" }, ], }, openai: { name: "OpenAI", baseUrl: "https://api.openai.com/v1", models: [ { id: "gpt-4o-mini", name: "GPT-4o mini", recommended: true }, { id: "gpt-4o", name: "GPT-4o" }, { id: "o3-mini", name: "o3-mini" }, ], }, claude: { name: "Claude (Anthropic)", baseUrl: "https://api.anthropic.com", models: [ { id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", recommended: true }, { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" }, { id: "claude-3-opus-20240229", name: "Claude 3 Opus" }, ], }, gemini: { name: "Gemini (Google)", baseUrl: "https://generativelanguage.googleapis.com/v1beta", models: [ { id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", recommended: true }, { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }, { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }, ], }, custom: { name: "自定义 (OpenAI 兼容)", baseUrl: "", models: [], }, }, async generateReply({ text, customPrompt, provider = "deepseek", model = "deepseek-chat", apiKey, customBaseUrl }) { if (!customPrompt?.trim()) throw new Error("未设置提示词"); if (!apiKey?.trim()) throw new Error("未设置 API Key"); const config = this.PROVIDER_CONFIGS[provider]; if (!config) throw new Error("未知的模型提供商:" + provider); const baseUrl = provider === "custom" ? customBaseUrl : config.baseUrl; if (!baseUrl) throw new Error("请配置 API 端点"); return new Promise((resolve, reject) => { let url, headers, body; if (provider === "claude") { url = baseUrl + "/v1/messages"; headers = { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01", }; body = JSON.stringify({ model, max_tokens: 300, system: this.SYSTEM_PROMPT, messages: [ { role: "user", content: `产品:${customPrompt.trim()}\n\n帖子:\n${text}` }, ], }); } else if (provider === "gemini") { url = baseUrl + "/openai/chat/completions?key=" + apiKey; headers = { "Content-Type": "application/json" }; body = JSON.stringify({ model, messages: [ { role: "system", content: this.SYSTEM_PROMPT }, { role: "user", content: `产品:${customPrompt.trim()}\n\n帖子:\n${text}` }, ], max_tokens: 200, temperature: 0.8, }); } else { url = baseUrl + "/chat/completions"; headers = { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`, }; body = JSON.stringify({ model, messages: [ { role: "system", content: this.SYSTEM_PROMPT }, { role: "user", content: `产品:${customPrompt.trim()}\n\n帖子:\n${text}` }, ], max_tokens: 200, temperature: 0.8, }); } GM_xmlhttpRequest({ method: "POST", url, headers, data: body, onload: (res) => { if (res.status >= 400) { try { const err = JSON.parse(res.responseText); reject(new Error(err.error?.message || err.message || `HTTP ${res.status}`)); } catch { reject(new Error(`HTTP ${res.status}`)); } return; } try { const data = JSON.parse(res.responseText); if (provider === "claude") { resolve(data.content[0].text.trim()); } else { resolve(data.choices[0].message.content.trim()); } } catch (e) { reject(new Error("解析响应失败:" + e.message)); } }, onerror: (err) => reject(new Error("网络错误:" + err.message)), ontimeout: () => reject(new Error("请求超时")) }); }); }, async fetchProviderModels(provider, apiKey, customBaseUrl) { const config = this.PROVIDER_CONFIGS[provider]; if (!config) throw new Error("未知的供应商:" + provider); return new Promise((resolve) => { if (provider === "custom") { if (!customBaseUrl) { resolve([]); return; } GM_xmlhttpRequest({ method: "GET", url: customBaseUrl + "/models", headers: { Authorization: `Bearer ${apiKey}` }, onload: (res) => { if (res.status >= 400) { resolve(config.models); return; } try { const data = JSON.parse(res.responseText); resolve((data.data || []).map(m => ({ id: m.id, name: m.id }))); } catch { resolve(config.models); } }, onerror: () => resolve(config.models) }); return; } if (provider === "claude") { resolve(config.models); return; } if (provider === "gemini") { GM_xmlhttpRequest({ method: "GET", url: `${config.baseUrl}/models?key=${apiKey}`, onload: (res) => { if (res.status >= 400) { resolve(config.models); return; } try { const data = JSON.parse(res.responseText); resolve((data.models || []) .filter(m => m.name.includes("gemini")) .map(m => ({ id: m.name.replace("models/", ""), name: m.name.replace("models/", "") }))); } catch { resolve(config.models); } }, onerror: () => resolve(config.models) }); return; } GM_xmlhttpRequest({ method: "GET", url: config.baseUrl + "/models", headers: { Authorization: `Bearer ${apiKey}` }, onload: (res) => { if (res.status >= 400) { resolve(config.models); return; } try { const data = JSON.parse(res.responseText); resolve((data.data || []).map(m => ({ id: m.id, name: m.id }))); } catch { resolve(config.models); } }, onerror: () => resolve(config.models) }); }); } }; // ============================================ // 授权验证 // ============================================ const AUTH_BASE = "http://111.229.201.174:13333"; const FIXED_EMAIL = "test@example.com"; async function getOrCreateDeviceId() { let deviceId = GM_getValue("deviceId", null); if (deviceId) return deviceId; deviceId = crypto.randomUUID(); GM_setValue("deviceId", deviceId); return deviceId; } async function callActivate(licenseKey) { const hwid = await getOrCreateDeviceId(); console.log("[callActivate] 发送请求", { url: `${AUTH_BASE}/v1/payments/activate`, hwid, email: FIXED_EMAIL }); return new Promise((resolve) => { // 使用 fetch 替代 GM_xmlhttpRequest,避免响应头获取问题 const url = `${AUTH_BASE}/v1/payments/activate?email=${encodeURIComponent(FIXED_EMAIL)}&licenseKey=${encodeURIComponent(licenseKey)}&hwid=${encodeURIComponent(hwid)}`; fetch(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, }) .then(async (res) => { console.log("[callActivate] 响应", { status: res.status, ok: res.ok, contentType: res.headers.get("content-type") }); let data = {}; try { data = await res.json(); } catch (e) { const text = await res.text(); console.warn("[callActivate] JSON 解析失败,尝试解析文本:", text); data = { msg: text.trim() }; } const result = { httpOk: res.status >= 200 && res.status < 300, code: data.code, msg: data.msg }; console.log("[callActivate] resolve:", result); resolve(result); }) .catch((err) => { console.error("[callActivate] 网络错误:", err); resolve({ httpOk: false, code: 0, msg: "网络错误:" + err.message }); }); }); } async function verifyToken(licenseKey) { try { const { httpOk, code } = await callActivate(licenseKey); return { ok: httpOk && code === 200 }; } catch { return { ok: false, networkError: true }; } } // ============================================ // 词袋推荐评分 // ============================================ function tokenize(text) { return text.toLowerCase().match(/[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]|[a-z0-9]+/g) || []; } function buildFreqMap(tokens) { const freq = {}; for (const t of tokens) freq[t] = (freq[t] || 0) + 1; return freq; } function computeScore(postText, promptFreq) { const postFreq = buildFreqMap(tokenize(postText)); let dot = 0, magA = 0, magB = 0; for (const [k, v] of Object.entries(promptFreq)) { dot += v * (postFreq[k] || 0); magA += v * v; } for (const v of Object.values(postFreq)) magB += v * v; if (magA === 0 || magB === 0) return 0; return Math.round(dot / Math.sqrt(magA * magB) * 100); } function updateScoreBadge(badge, score) { const color = score >= 60 ? "#16a34a" : score >= 25 ? "#d97706" : "#6b7280"; badge.innerText = score; badge.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 20px; flex-shrink: 0; font-size: 11px; font-weight: 700; border-radius: 6px; background: rgba(15,15,17,0.9); color: ${color}; border: 1px solid ${color}66; cursor: default; box-shadow: 0 1px 3px rgba(0,0,0,0.3); `; } // ============================================ // DOM 工具函数 // ============================================ function waitForElement(selector, timeout = 5000) { return new Promise((resolve, reject) => { const el = document.querySelector(selector); if (el) return resolve(el); const observer = new MutationObserver(() => { const el = document.querySelector(selector); if (el) { observer.disconnect(); clearTimeout(timer); resolve(el); } }); observer.observe(document.body, { childList: true, subtree: true }); const timer = setTimeout(() => { observer.disconnect(); reject(new Error("等待超时:" + selector)); }, timeout); }); } function waitForSendButton(timeout = 5000) { const selectors = ['[data-testid="tweetButtonInline"]', '[data-testid="tweetButton"]']; return new Promise((resolve, reject) => { const find = () => { for (const sel of selectors) { const btn = document.querySelector(sel); if (btn && btn.getAttribute("aria-disabled") !== "true" && !btn.disabled) return btn; } return null; }; const found = find(); if (found) return resolve(found); const observer = new MutationObserver(() => { const btn = find(); if (btn) { observer.disconnect(); clearTimeout(timer); resolve(btn); } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-disabled", "disabled"], }); const timer = setTimeout(() => { observer.disconnect(); reject(new Error("发送按钮未就绪")); }, timeout); }); } async function sendReplyToPost(post, replyText) { const replyBtn = post.querySelector('[data-testid="reply"]'); if (!replyBtn) throw new Error("找不到回复按钮"); replyBtn.click(); const wrapper = await waitForElement('div[data-testid="tweetTextarea_0"]'); const editable = wrapper.querySelector('[contenteditable="true"]') || wrapper; editable.click(); editable.focus(); await new Promise((r) => setTimeout(r, 200)); document.execCommand("insertText", false, replyText); const postBtn = await waitForSendButton(); await new Promise((r) => setTimeout(r, 200)); postBtn.click(); } // ============================================ // 共用样式 // ============================================ const _css = { input: `width:100%;background:linear-gradient(180deg,#1f2937,#111827);border:1px solid rgba(148,163,184,0.2);border-radius:10px;color:#f3f4f6;font-size:13px;padding:10px 12px;outline:none;transition:all .2s;box-sizing:border-box;box-shadow:0 1px 3px rgba(0,0,0,0.2);`, select: `appearance:none;width:100%;background:linear-gradient(180deg,#1f2937,#111827);border:1px solid rgba(148,163,184,0.2);border-radius:10px;color:#f3f4f6;font-size:13px;padding:10px 36px 10px 12px;outline:none;cursor:pointer;transition:all .2s;box-shadow:0 1px 3px rgba(0,0,0,0.2);color-scheme:dark;`, btn: (bg) => `width:100%;padding:11px;border:none;border-radius:10px;background:${bg};color:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:all .25s;box-shadow:0 2px 8px rgba(0,0,0,0.2);`, }; // ============================================ // 模型配置 // ============================================ const PROVIDER_CONFIGS = { deepseek: { name: "DeepSeek", models: [{ id: "deepseek-chat", name: "DeepSeek V3", recommended: true }, { id: "deepseek-reasoner", name: "DeepSeek R1" }] }, openai: { name: "OpenAI", models: [{ id: "gpt-4o-mini", name: "GPT-4o mini", recommended: true }, { id: "gpt-4o", name: "GPT-4o" }, { id: "o3-mini", name: "o3-mini" }] }, claude: { name: "Claude (Anthropic)", models: [{ id: "claude-sonnet-4-20250514", name: "Claude Sonnet 4", recommended: true }, { id: "claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet" }, { id: "claude-3-opus-20240229", name: "Claude 3 Opus" }] }, gemini: { name: "Gemini (Google)", models: [{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", recommended: true }, { id: "gemini-1.5-flash", name: "Gemini 1.5 Flash" }, { id: "gemini-1.5-pro", name: "Gemini 1.5 Pro" }] }, custom: { name: "自定义 (OpenAI 兼容)", models: [] }, }; // ============================================ // 全局状态 // ============================================ let _promptFreq = {}; let _hasPrompt = false; let _isAuthorized = false; let _autoReplyEnabled = false; let _autoReplyKeywords = []; let _autoLikeEnabled = false; let _autoRandomEnabled = false; let _autoRandomMin = 5; let _autoRandomMax = 15; let _autoViewLimitEnabled = false; let _autoViewLimit = 100; let _autoReplyLimitEnabled = false; let _autoReplyLimit = 10; let _autoShareLimitEnabled = false; let _autoShareLimit = 5; let _autoScrollTimer = null; let _isSending = false; let _autoReplyQueue = []; let _autoReplyRunning = false; // ============================================ // 获取帖子数据(浏览量、评论数、分享数) // ============================================ function getPostViewCount(post) { const text = post.innerText; const patterns = [ /(\d+(?:\.\d+)?)[KMB]?\s*Views/i, /(\d+(?:\.\d+)?)[KMB]?\s*查看/, /(\d+(?:\.\d+)?)[ 万千亿]?次查看/, ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) { let num = parseFloat(match[1]); const suffix = match[0].match(/([KMB])/i)?.[1]?.toUpperCase(); if (suffix === 'K') num *= 1000; else if (suffix === 'M') num *= 1000000; else if (suffix === 'B') num *= 1000000000; if (match[0].includes('万')) num *= 10000; else if (match[0].includes('亿')) num *= 100000000; return Math.round(num); } } return 0; } function getPostReplyCount(post) { const text = post.innerText; const patterns = [ /(\d+(?:\.\d+)?)[KMB]?\s*Reply/i, /(\d+(?:\.\d+)?)[KMB]?\s*回复/, /(\d+(?:\.\d+)?)[ 万千亿]?条回复/, ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) { let num = parseFloat(match[1]); const suffix = match[0].match(/([KMB])/i)?.[1]?.toUpperCase(); if (suffix === 'K') num *= 1000; else if (suffix === 'M') num *= 1000000; else if (suffix === 'B') num *= 1000000000; if (match[0].includes('万')) num *= 10000; else if (match[0].includes('亿')) num *= 100000000; return Math.round(num); } } return 0; } function getPostShareCount(post) { const text = post.innerText; const patterns = [ /(\d+(?:\.\d+)?)[KMB]?\s*Share/i, /(\d+(?:\.\d+)?)[KMB]?\s*Repost/i, /(\d+(?:\.\d+)?)[KMB]?\s*分享/, /(\d+(?:\.\d+)?)[ 万千亿]?次分享/, /(\d+(?:\.\d+)?)[ 万千亿]?次转发/, ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) { let num = parseFloat(match[1]); const suffix = match[0].match(/([KMB])/i)?.[1]?.toUpperCase(); if (suffix === 'K') num *= 1000; else if (suffix === 'M') num *= 1000000; else if (suffix === 'B') num *= 1000000000; if (match[0].includes('万')) num *= 10000; else if (match[0].includes('亿')) num *= 100000000; return Math.round(num); } } return 0; } // ============================================ // 随机等待 // ============================================ function randomWait(minSec, maxSec) { const ms = (Math.random() * (maxSec - minSec) + minSec) * 1000; return new Promise(resolve => setTimeout(resolve, ms)); } const AUTO_ICON = { "⏳": ["#d97706", "rgba(217,119,6,0.15)"], "✅": ["#16a34a", "rgba(22,163,74,0.15)"], "❌": ["#dc2626", "rgba(220,38,38,0.15)"], }; // ============================================ // 自动回复核心逻辑 // ============================================ async function triggerAutoReply(post) { if (!_autoReplyEnabled) return; if (post.dataset.autoReplying || post.dataset.autoReplied) return; post.dataset.autoReplying = "1"; const indicator = post.querySelector("[data-auto-indicator]"); function setIndicator(icon) { if (!indicator) return; const [color, bg] = AUTO_ICON[icon]; indicator.style.display = "inline-flex"; indicator.style.color = color; indicator.style.background = bg; indicator.style.borderColor = color + "88"; indicator.innerText = icon; } setIndicator("⏳"); if (_autoRandomEnabled) { await randomWait(_autoRandomMin, _autoRandomMax); } try { const customPrompt = GM_getValue("customPrompt", ""); const selectedProvider = GM_getValue("selectedProvider", "deepseek"); const selectedModel = GM_getValue("selectedModel", "deepseek-chat"); const apiKeys = GM_getValue("apiKeys", {}); const customBaseUrls = GM_getValue("customBaseUrls", {}); const provider = selectedProvider || "deepseek"; const model = selectedModel || "deepseek-chat"; const apiKey = (apiKeys || {})[provider] || ""; const customBaseUrl = (customBaseUrls || {})[provider] || ""; if (!customPrompt?.trim() || !apiKey) { if (indicator) indicator.style.display = "none"; return; } const reply = await AI_API.generateReply({ text: post.innerText, customPrompt, provider, model, apiKey, customBaseUrl }); if (reply && reply.length > 0) { _isSending = true; try { await sendReplyToPost(post, reply); post.dataset.autoReplied = "1"; setIndicator("✅"); if (_autoLikeEnabled) { await new Promise(r => setTimeout(r, 400)); const likeBtn = post.querySelector('[data-testid="like"]'); if (likeBtn) likeBtn.click(); } } finally { _isSending = false; } } else { setIndicator("❌"); } } catch (e) { console.error("[triggerAutoReply] 错误:", e); if (indicator) setIndicator("❌"); } finally { delete post.dataset.autoReplying; } } async function processAutoReplyQueue() { if (_autoReplyRunning) return; _autoReplyRunning = true; while (_autoReplyQueue.length > 0) { const post = _autoReplyQueue.shift(); await triggerAutoReply(post); await new Promise(r => setTimeout(r, 2000)); } _autoReplyRunning = false; } function checkLimits(post) { if (_autoViewLimitEnabled) { const viewCount = getPostViewCount(post); if (viewCount < _autoViewLimit) return false; } if (_autoReplyLimitEnabled) { const replyCount = getPostReplyCount(post); if (replyCount < _autoReplyLimit) return false; } if (_autoShareLimitEnabled) { const shareCount = getPostShareCount(post); if (shareCount < _autoShareLimit) return false; } return true; } function queueAutoReply(post) { if (post.dataset.autoReplying || post.dataset.autoReplied || post.dataset.autoQueued) return; if (!checkLimits(post)) return; post.dataset.autoQueued = "1"; _autoReplyQueue.push(post); processAutoReplyQueue(); } // ============================================ // 自动滑动 // ============================================ function startAutoScroll() { if (_autoScrollTimer) return; _autoScrollTimer = setInterval(() => { if (!_autoReplyEnabled) { stopAutoScroll(); return; } if (_isSending) return; window.scrollBy({ top: Math.round(window.innerHeight * 0.75), behavior: "smooth" }); }, 3500); } function stopAutoScroll() { clearInterval(_autoScrollTimer); _autoScrollTimer = null; _autoReplyQueue = []; } function rescoreAllPosts() { document.querySelectorAll("article[data-ai]").forEach((post) => { const badge = post.querySelector("[data-score-badge]"); if (!badge) return; if (_hasPrompt) { updateScoreBadge(badge, computeScore(post.innerText, _promptFreq)); } else { badge.style.display = "none"; } }); } // ============================================ // 初始化 // ============================================ function initFromStorage() { const customPrompt = GM_getValue("customPrompt", ""); if (customPrompt?.trim()) { _promptFreq = buildFreqMap(tokenize(customPrompt)); _hasPrompt = true; } _autoReplyEnabled = GM_getValue("autoReplyEnabled", false); _autoReplyKeywords = GM_getValue("autoReplyKeywords", []); _autoLikeEnabled = GM_getValue("autoLikeEnabled", false); _autoRandomEnabled = GM_getValue("autoReplyRandomEnabled", false); _autoRandomMin = GM_getValue("autoReplyRandomMin", 5); _autoRandomMax = GM_getValue("autoReplyRandomMax", 15); _autoViewLimitEnabled = GM_getValue("autoReplyViewLimitEnabled", false); _autoViewLimit = GM_getValue("autoReplyViewLimit", 100); _autoReplyLimitEnabled = GM_getValue("autoReplyReplyLimitEnabled", false); _autoReplyLimit = GM_getValue("autoReplyReplyLimit", 10); _autoShareLimitEnabled = GM_getValue("autoReplyShareLimitEnabled", false); _autoShareLimit = GM_getValue("autoReplyShareLimit", 5); if (_autoReplyEnabled) startAutoScroll(); } // ============================================ // 设置面板 // ============================================ function buildSettingsPanel() { // 注入全局样式 if (!document.getElementById("ai-settings-global-style")) { const style = document.createElement("style"); style.id = "ai-settings-global-style"; style.textContent = ` #ai-settings-panel select option { background-color: #111827 !important; color: #f3f4f6 !important; } #ai-settings-panel select { color-scheme: dark; } #ai-settings-panel input::placeholder { color: #6b7280; } #ai-settings-panel button:active { transform: scale(0.98); } `; document.head.appendChild(style); } const panel = document.createElement("div"); panel.id = "ai-settings-panel"; const _initLeft = Math.max(0, window.innerWidth - 384); panel.style.cssText = ` position:fixed; left:${_initLeft}px; top:80px; width:360px; background:rgba(15,15,17,0.98); color:#f1f1f1; z-index:999999; border-radius:16px; box-shadow:0 12px 40px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.1); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; overflow:hidden; max-height:700px; transition:opacity .22s ease,transform .22s ease,max-height .2s ease; opacity:0; transform:translateX(20px); backdrop-filter:blur(20px); `; panel.innerHTML = `
X Reply AI ● 自动中
推广产品描述(必填)
服务商
API 端点
模型
API Key
自动回复
开启后自动回复匹配帖子
已关闭
自动点赞
评论时同时点赞
已关闭
随机等待
开启后每次回复前随机等待
已关闭
最小 (秒)
最大 (秒)
回复条件限制(满足全部)
浏览量
评论数
分享数
触发关键词(一行一个)
`; document.body.appendChild(panel); requestAnimationFrame(() => { panel.style.opacity = "1"; panel.style.transform = "translateX(0)"; }); // 标签切换 const tabs = [...panel.querySelectorAll("[data-tab]")]; const panes = [...panel.querySelectorAll("[data-panel]")]; function switchTab(name) { tabs.forEach(t => { const on = t.dataset.tab === name; t.style.color = on ? "#60a5fa" : "#6b7280"; t.style.borderBottomColor = on ? "#3b82f6" : "transparent"; }); panes.forEach(p => p.style.display = p.dataset.panel === name ? "" : "none"); } tabs.forEach(t => { t.style.pointerEvents = "auto"; t.addEventListener("click", () => switchTab(t.dataset.tab)); }); panel._switchTab = switchTab; // 拖拽 const _titleBar = panel.querySelector("[data-sp-titlebar]"); let _drag = null; _titleBar.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; const r = panel.getBoundingClientRect(); panel.style.left = r.left + "px"; panel.style.top = r.top + "px"; panel.style.right = "auto"; panel.style.transition = "none"; _drag = { ox: e.clientX - r.left, oy: e.clientY - r.top }; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!_drag) return; const maxL = window.innerWidth - panel.offsetWidth; const maxT = window.innerHeight - panel.offsetHeight; panel.style.left = Math.min(Math.max(0, e.clientX - _drag.ox), maxL) + "px"; panel.style.top = Math.min(Math.max(0, e.clientY - _drag.oy), maxT) + "px"; }); document.addEventListener("mouseup", () => { if (!_drag) return; _drag = null; panel.style.transition = "opacity .22s ease,transform .22s ease"; }); // 关闭 panel.querySelector("[data-sp-close]").addEventListener("click", () => { if (panel.dataset.collapsed) { panel.style.maxHeight = "700px"; delete panel.dataset.collapsed; panel.querySelector("[data-sp-close]").innerText = "✕"; } else { panel.style.maxHeight = "50px"; panel.dataset.collapsed = "1"; panel.querySelector("[data-sp-close]").innerText = "▼"; } }); // 产品设置 const promptInput = panel.querySelector("[data-prompt-input]"); const savePromptBtn = panel.querySelector("[data-sp-save-prompt]"); const clearPromptBtn = panel.querySelector("[data-sp-clear-prompt]"); promptInput.value = GM_getValue("customPrompt", ""); savePromptBtn.addEventListener("click", () => { GM_setValue("customPrompt", promptInput.value.trim()); savePromptBtn.innerText = "✓ 已保存"; savePromptBtn.style.background = "#16a34a"; setTimeout(() => { savePromptBtn.innerText = "保存"; savePromptBtn.style.background = "#3b82f6"; }, 1400); }); clearPromptBtn.addEventListener("click", () => { promptInput.value = ""; GM_setValue("customPrompt", ""); clearPromptBtn.innerText = "✓ 已清除"; setTimeout(() => clearPromptBtn.innerText = "清除", 1200); }); // 模型设置 const providerSelect = panel.querySelector("[data-provider-select]"); const modelSelect = panel.querySelector("[data-model-select]"); const apikeyInput = panel.querySelector("[data-apikey-input]"); const saveModelBtn = panel.querySelector("[data-sp-save-model]"); const customUrlWrap = panel.querySelector("[data-custom-url-wrap]"); const customUrlInput = panel.querySelector("[data-custom-url]"); const refreshBtn = document.createElement("button"); const fillProviders = () => { providerSelect.innerHTML = Object.entries(PROVIDER_CONFIGS) .map(([id, p]) => ``).join(""); }; const fillModels = (models, provider) => { if (!models || models.length === 0) { modelSelect.innerHTML = ''; return; } const preset = PROVIDER_CONFIGS[provider]?.models || []; const recommended = preset.filter(m => m.recommended).map(m => m.id); modelSelect.innerHTML = models .map(m => { const isRecommended = recommended.includes(m.id) ? 'data-recommended="1"' : ''; const star = recommended.includes(m.id) ? ' ⭐' : ''; return ``; }).join(""); }; const fetchModels = async (provider) => { modelSelect.innerHTML = ''; modelSelect.disabled = true; const apiKeys = GM_getValue("apiKeys", {}); const customBaseUrls = GM_getValue("customBaseUrls", {}); const apiKey = (apiKeys || {})[provider] || ""; const customBaseUrl = (customBaseUrls || {})[provider] || ""; try { const models = await AI_API.fetchProviderModels(provider, apiKey, customBaseUrl); fillModels(models || [], provider); } catch { fillModels(PROVIDER_CONFIGS[provider]?.models || [], provider); } finally { modelSelect.disabled = false; } }; fillProviders(); fillModels(PROVIDER_CONFIGS["deepseek"]?.models || [], "deepseek"); modelSelect.style.paddingRight = "44px"; refreshBtn.innerHTML = "⟳"; refreshBtn.title = "刷新模型列表"; refreshBtn.style.cssText = "position:absolute;right:42px;top:50%;transform:translateY(-50%);background:transparent;border:1px solid rgba(148,163,184,0.15);color:#9ca3af;cursor:pointer;font-size:14px;width:26px;height:26px;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:all .2s;z-index:10;"; modelSelect.parentNode.appendChild(refreshBtn); refreshBtn.addEventListener("click", () => fetchModels(providerSelect.value)); const savedProvider = GM_getValue("selectedProvider", "deepseek"); const savedModel = GM_getValue("selectedModel", ""); const savedApiKeys = GM_getValue("apiKeys", {}); const savedCustomUrls = GM_getValue("customBaseUrls", {}); providerSelect.value = savedProvider; fillModels(PROVIDER_CONFIGS[savedProvider]?.models || [], savedProvider); if (savedProvider === "custom") { customUrlWrap.style.display = ""; customUrlInput.value = savedCustomUrls[savedProvider] || ""; } if (savedModel) modelSelect.value = savedModel; apikeyInput.value = savedApiKeys[savedProvider] || ""; providerSelect.addEventListener("change", () => { const p = providerSelect.value; customUrlWrap.style.display = p === "custom" ? "" : "none"; const apiKeys = GM_getValue("apiKeys", {}); const customBaseUrls = GM_getValue("customBaseUrls", {}); apikeyInput.value = (apiKeys || {})[p] || ""; if (p === "custom") customUrlInput.value = (customBaseUrls || {})[p] || ""; if ((apiKeys || {})[p]) { fetchModels(p); } else { fillModels(PROVIDER_CONFIGS[p]?.models || [], p); } }); saveModelBtn.addEventListener("click", () => { const provider = providerSelect.value; const model = modelSelect.value; const key = apikeyInput.value.trim(); const customUrl = customUrlInput.value.trim(); const apiKeys = GM_getValue("apiKeys", {}); const customBaseUrls = GM_getValue("customBaseUrls", {}); const keys = key ? { ...(apiKeys || {}), [provider]: key } : (apiKeys || {}); const urls = customUrl ? { ...(customBaseUrls || {}), [provider]: customUrl } : (customBaseUrls || {}); GM_setValue("selectedProvider", provider); GM_setValue("selectedModel", model || customUrl); GM_setValue("apiKeys", keys); GM_setValue("customBaseUrls", urls); saveModelBtn.innerText = "✓ 已保存"; saveModelBtn.style.background = "#16a34a"; setTimeout(() => { saveModelBtn.innerText = "保存模型设置"; saveModelBtn.style.background = "#6d28d9"; }, 1400); }); // 自动回复设置 const autoToggleWrap = panel.querySelector("[data-auto-toggle-wrap]"); const autoLabel = panel.querySelector("[data-auto-label]"); const autoTrack = panel.querySelector("[data-auto-track]"); const autoThumb = panel.querySelector("[data-auto-thumb]"); const autoRunning = panel.querySelector("[data-auto-running]"); const keywordsInput = panel.querySelector("[data-keywords-input]"); const saveKwBtn = panel.querySelector("[data-sp-save-keywords]"); const randomToggleWrap = panel.querySelector("[data-random-toggle-wrap]"); const randomLabel = panel.querySelector("[data-random-label]"); const randomTrack = panel.querySelector("[data-random-track]"); const randomThumb = panel.querySelector("[data-random-thumb]"); const randomTimeWrap = panel.querySelector("[data-random-time-wrap]"); const randomMinInput = panel.querySelector("[data-random-min]"); const randomMaxInput = panel.querySelector("[data-random-max]"); const viewToggleWrap = panel.querySelector("[data-view-toggle-wrap]"); const viewTrack = panel.querySelector("[data-view-track]"); const viewThumb = panel.querySelector("[data-view-thumb]"); const viewLimitInput = panel.querySelector("[data-view-limit]"); const replyToggleWrap = panel.querySelector("[data-reply-toggle-wrap]"); const replyTrack = panel.querySelector("[data-reply-track]"); const replyThumb = panel.querySelector("[data-reply-thumb]"); const replyLimitInput = panel.querySelector("[data-reply-limit]"); const shareToggleWrap = panel.querySelector("[data-share-toggle-wrap]"); const shareTrack = panel.querySelector("[data-share-track]"); const shareThumb = panel.querySelector("[data-share-thumb]"); const shareLimitInput = panel.querySelector("[data-share-limit]"); const likeToggleWrap = panel.querySelector("[data-like-toggle-wrap]"); const likeLabel = panel.querySelector("[data-like-label]"); const likeTrack = panel.querySelector("[data-like-track]"); const likeThumb = panel.querySelector("[data-like-thumb]"); // 加载设置 const autoEnabled = GM_getValue("autoReplyEnabled", false); autoTrack.style.background = autoEnabled ? "#0f766e" : "#374151"; autoThumb.style.left = autoEnabled ? "18px" : "2px"; autoLabel.innerText = autoEnabled ? "已开启" : "已关闭"; autoLabel.style.color = autoEnabled ? "#34d399" : "#6b7280"; autoRunning.style.display = autoEnabled ? "inline" : "none"; keywordsInput.value = (GM_getValue("autoReplyKeywords", []) || []).join("\n"); const randomEnabled = GM_getValue("autoReplyRandomEnabled", false); randomTrack.style.background = randomEnabled ? "#0f766e" : "#374151"; randomThumb.style.left = randomEnabled ? "18px" : "2px"; randomLabel.innerText = randomEnabled ? "已开启" : "已关闭"; randomLabel.style.color = randomEnabled ? "#34d399" : "#6b7280"; randomTimeWrap.style.display = randomEnabled ? "" : "none"; randomMinInput.value = GM_getValue("autoReplyRandomMin", 5); randomMaxInput.value = GM_getValue("autoReplyRandomMax", 15); const viewLimitEnabled = GM_getValue("autoReplyViewLimitEnabled", false); viewTrack.style.background = viewLimitEnabled ? "#0f766e" : "#374151"; viewThumb.style.left = viewLimitEnabled ? "14px" : "2px"; viewLimitInput.disabled = !viewLimitEnabled; viewLimitInput.value = GM_getValue("autoReplyViewLimit", 100); const replyLimitEnabled = GM_getValue("autoReplyReplyLimitEnabled", false); replyTrack.style.background = replyLimitEnabled ? "#0f766e" : "#374151"; replyThumb.style.left = replyLimitEnabled ? "14px" : "2px"; replyLimitInput.disabled = !replyLimitEnabled; replyLimitInput.value = GM_getValue("autoReplyReplyLimit", 10); const shareLimitEnabled = GM_getValue("autoReplyShareLimitEnabled", false); shareTrack.style.background = shareLimitEnabled ? "#0f766e" : "#374151"; shareThumb.style.left = shareLimitEnabled ? "14px" : "2px"; shareLimitInput.disabled = !shareLimitEnabled; shareLimitInput.value = GM_getValue("autoReplyShareLimit", 5); const likeEnabled = GM_getValue("autoLikeEnabled", false); likeTrack.style.background = likeEnabled ? "#e11d48" : "#374151"; likeThumb.style.left = likeEnabled ? "18px" : "2px"; likeLabel.innerText = likeEnabled ? "已开启" : "已关闭"; likeLabel.style.color = likeEnabled ? "#fb7185" : "#6b7280"; // 开关事件 autoToggleWrap.addEventListener("click", () => { const newState = autoTrack.style.background === "#0f766e" ? false : true; autoTrack.style.background = newState ? "#0f766e" : "#374151"; autoThumb.style.left = newState ? "18px" : "2px"; autoLabel.innerText = newState ? "已开启" : "已关闭"; autoLabel.style.color = newState ? "#34d399" : "#6b7280"; autoRunning.style.display = newState ? "inline" : "none"; GM_setValue("autoReplyEnabled", newState); if (newState) startAutoScroll(); else stopAutoScroll(); }); randomToggleWrap.addEventListener("click", () => { const newState = randomTrack.style.background === "#0f766e" ? false : true; randomTrack.style.background = newState ? "#0f766e" : "#374151"; randomThumb.style.left = newState ? "18px" : "2px"; randomLabel.innerText = newState ? "已开启" : "已关闭"; randomLabel.style.color = newState ? "#34d399" : "#6b7280"; randomTimeWrap.style.display = newState ? "" : "none"; GM_setValue("autoReplyRandomEnabled", newState); }); viewToggleWrap.addEventListener("click", () => { const newState = viewTrack.style.background === "#0f766e" ? false : true; viewTrack.style.background = newState ? "#0f766e" : "#374151"; viewThumb.style.left = newState ? "14px" : "2px"; viewLimitInput.disabled = !newState; GM_setValue("autoReplyViewLimitEnabled", newState); }); replyToggleWrap.addEventListener("click", () => { const newState = replyTrack.style.background === "#0f766e" ? false : true; replyTrack.style.background = newState ? "#0f766e" : "#374151"; replyThumb.style.left = newState ? "14px" : "2px"; replyLimitInput.disabled = !newState; GM_setValue("autoReplyReplyLimitEnabled", newState); }); shareToggleWrap.addEventListener("click", () => { const newState = shareTrack.style.background === "#0f766e" ? false : true; shareTrack.style.background = newState ? "#0f766e" : "#374151"; shareThumb.style.left = newState ? "14px" : "2px"; shareLimitInput.disabled = !newState; GM_setValue("autoReplyShareLimitEnabled", newState); }); likeToggleWrap.addEventListener("click", () => { const newState = likeTrack.style.background === "#e11d48" ? false : true; likeTrack.style.background = newState ? "#e11d48" : "#374151"; likeThumb.style.left = newState ? "18px" : "2px"; likeLabel.innerText = newState ? "已开启" : "已关闭"; likeLabel.style.color = newState ? "#fb7185" : "#6b7280"; GM_setValue("autoLikeEnabled", newState); }); // 保存设置 saveKwBtn.addEventListener("click", () => { const kws = keywordsInput.value.split("\n").map(s => s.trim()).filter(Boolean); const viewLimit = parseInt(viewLimitInput.value) || 100; const replyLimit = parseInt(replyLimitInput.value) || 10; const shareLimit = parseInt(shareLimitInput.value) || 5; GM_setValue("autoReplyKeywords", kws); GM_setValue("autoReplyViewLimit", viewLimit); GM_setValue("autoReplyReplyLimit", replyLimit); GM_setValue("autoReplyShareLimit", shareLimit); saveKwBtn.innerText = "✓ 已保存"; saveKwBtn.style.background = "#16a34a"; setTimeout(() => { saveKwBtn.innerText = "保存设置"; saveKwBtn.style.background = "#0f766e"; }, 1400); }); const saveRandomBtn = document.createElement("button"); saveRandomBtn.innerText = "保存随机等待"; saveRandomBtn.style.cssText = _css.btn("#0f766e") + "margin-top:12px;"; randomTimeWrap.appendChild(saveRandomBtn); saveRandomBtn.addEventListener("click", () => { const min = parseInt(randomMinInput.value) || 5; const max = parseInt(randomMaxInput.value) || 15; if (min < 1 || max < min) { saveRandomBtn.innerText = "⚠️ 最小值 1,最大≥最小"; saveRandomBtn.style.background = "#dc2626"; setTimeout(() => { saveRandomBtn.innerText = "保存随机等待"; saveRandomBtn.style.background = "#0f766e"; }, 2000); return; } GM_setValue("autoReplyRandomMin", min); GM_setValue("autoReplyRandomMax", max); saveRandomBtn.innerText = "✓ 已保存"; saveRandomBtn.style.background = "#16a34a"; setTimeout(() => { saveRandomBtn.innerText = "保存随机等待"; saveRandomBtn.style.background = "#0f766e"; }, 1400); }); return panel; } function openSettingsPanel(tab = "product") { let panel = document.getElementById("ai-settings-panel"); if (!panel) { panel = buildSettingsPanel(); } else if (panel.dataset.collapsed) { panel.style.maxHeight = "700px"; delete panel.dataset.collapsed; panel.querySelector("[data-sp-close]").innerText = "✕"; } panel._switchTab?.(tab); } // ============================================ // 授权卡片 // ============================================ function showActivationCard() { if (document.getElementById("xai-auth-card")) return; const card = document.createElement("div"); card.id = "xai-auth-card"; const _initLeft = Math.max(0, window.innerWidth - 384); card.style.cssText = ` position:fixed; left:${_initLeft}px; top:80px; width:360px; background:rgba(15,15,17,0.98); color:#f1f1f1; z-index:999999; border-radius:16px; box-shadow:0 12px 40px rgba(0,0,0,0.6); border:1px solid rgba(255,255,255,0.1); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; overflow:hidden; transition:opacity .22s ease,transform .22s ease; opacity:0; transform:translateX(20px); backdrop-filter:blur(20px); `; card.innerHTML = `
X Reply AI 未激活
请输入授权码以激活插件功能,授权码将永久绑定到本设备。
授权码
目前测试阶段,免费提供激活码:123
`; document.body.appendChild(card); requestAnimationFrame(() => { card.style.opacity = "1"; card.style.transform = "translateX(0)"; }); const titlebar = card.querySelector("[data-ac-titlebar]"); let _drag = null; titlebar.addEventListener("mousedown", (e) => { if (e.target.closest("button")) return; const r = card.getBoundingClientRect(); card.style.left = r.left + "px"; card.style.top = r.top + "px"; card.style.transition = "none"; _drag = { ox: e.clientX - r.left, oy: e.clientY - r.top }; e.preventDefault(); }); document.addEventListener("mousemove", (e) => { if (!_drag) return; card.style.left = Math.min(Math.max(0, e.clientX - _drag.ox), window.innerWidth - card.offsetWidth) + "px"; card.style.top = Math.min(Math.max(0, e.clientY - _drag.oy), window.innerHeight - card.offsetHeight) + "px"; }); document.addEventListener("mouseup", () => { if (!_drag) return; _drag = null; card.style.transition = "opacity .22s ease,transform .22s ease"; }); card.querySelector("[data-ac-close]").addEventListener("click", () => { card.style.opacity = "0"; card.style.transform = "translateX(20px)"; setTimeout(() => card.remove(), 220); }); const input = card.querySelector("[data-ac-input]"); const errorEl = card.querySelector("[data-ac-error]"); const btn = card.querySelector("[data-ac-btn]"); async function doActivate() { const key = input.value.trim(); console.log("[activate] start", { keyLength: key.length }); if (!key) { errorEl.innerText = "请输入授权码"; errorEl.style.display = "block"; console.warn("[activate] empty key"); return; } errorEl.style.display = "none"; // UI 状态统一管理 setLoadingState(true, "验证中…"); try { const startTime = Date.now(); const res = await callActivate(key); const cost = Date.now() - startTime; console.log("[activate] response", { cost, res }); if (res?.httpOk && res?.code === 200) { console.info("[activate] success"); GM_setValue("license_key", key); _isAuthorized = true; // 动画退出 card.style.opacity = "0"; card.style.transform = "translateX(20px)"; setTimeout(() => { card.remove(); openSettingsPanel(); if (_autoReplyEnabled) { console.log("[activate] auto reply enabled -> start scroll"); startAutoScroll(); } }, 220); return; } // 失败分类型提示 const msg = res?.msg || "授权码无效,请确认后重试 1111"; errorEl.innerText = msg; errorEl.style.display = "block"; console.warn("[activate] failed", { msg, res }); } catch (err) { console.error("[activate] network error", err); errorEl.innerText = "网络错误,请检查连接后重试"; errorEl.style.display = "block"; } setLoadingState(false); } // UI 状态统一抽离(避免重复写) function setLoadingState(loading, text = "") { btn.disabled = loading; btn.innerText = text || (loading ? "处理中…" : "激活使用"); btn.style.opacity = loading ? "0.7" : "1"; } btn.addEventListener("click", doActivate); input.addEventListener("keydown", e => { if (e.key === "Enter") doActivate(); }); } // ============================================ // 添加 AI 按钮到帖子 // ============================================ function addButtons() { document.querySelectorAll("article").forEach((post) => { if (post.dataset.ai) return; post.dataset.ai = "1"; const btn = document.createElement("button"); btn.innerText = "AI"; btn.style.cssText = ` width:28px; height:28px; flex-shrink:0; padding:0; border:none; border-radius:8px; background:transparent; color:#888; font-size:12px; font-weight:600; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; transition:all .15s ease; `; btn.addEventListener("mouseenter", () => { btn.style.background = "rgba(59,130,246,0.15)"; btn.style.color = "#60a5fa"; }); btn.addEventListener("mouseleave", () => { btn.style.background = "transparent"; btn.style.color = "#888"; }); btn.addEventListener("click", async () => { if (btn.dataset.loading) return; if (!_isAuthorized) { showActivationCard(); return; } const customPrompt = GM_getValue("customPrompt", ""); const selectedProvider = GM_getValue("selectedProvider", "deepseek"); const selectedModel = GM_getValue("selectedModel", "deepseek-chat"); const apiKeys = GM_getValue("apiKeys", {}); const customBaseUrls = GM_getValue("customBaseUrls", {}); const provider = selectedProvider || "deepseek"; const model = selectedModel || "deepseek-chat"; const apiKey = (apiKeys || {})[provider] || ""; const customBaseUrl = (customBaseUrls || {})[provider] || ""; if (!customPrompt?.trim()) { GM_notification({ text: "请先设置产品描述", timeout: 3000 }); return; } if (!apiKey) { GM_notification({ text: "请先设置 API Key", timeout: 3000 }); return; } btn.dataset.loading = "1"; btn.innerText = "..."; try { const reply = await AI_API.generateReply({ text: post.innerText, customPrompt, provider, model, apiKey, customBaseUrl }); if (reply) { // 复制到剪贴板 await navigator.clipboard.writeText(reply); btn.innerText = "✓"; GM_notification({ text: "回复已生成并复制到剪贴板", timeout: 2000 }); setTimeout(() => { btn.innerText = "AI"; delete btn.dataset.loading; }, 2000); } else { btn.innerText = "✕"; setTimeout(() => { btn.innerText = "AI"; delete btn.dataset.loading; }, 2000); } } catch (e) { btn.innerText = "✕"; GM_notification({ text: "生成失败:" + e.message, timeout: 3000 }); setTimeout(() => { btn.innerText = "AI"; delete btn.dataset.loading; }, 2000); } }); const badge = document.createElement("span"); badge.dataset.scoreBadge = "1"; badge.title = "推荐度(词袋模型)"; badge.style.cssText = "display:none;align-items:center;justify-content:center;width:24px;height:20px;flex-shrink:0;font-size:11px;border-radius:6px;"; if (_hasPrompt) updateScoreBadge(badge, computeScore(post.innerText, _promptFreq)); const autoIndicator = document.createElement("span"); autoIndicator.dataset.autoIndicator = "1"; autoIndicator.style.cssText = "display:none;align-items:center;justify-content:center;width:24px;height:20px;flex-shrink:0;font-size:12px;border-radius:6px;border:1px solid transparent;"; const actionGroup = post.querySelector('[role="group"]'); if (actionGroup) { actionGroup.appendChild(autoIndicator); actionGroup.appendChild(badge); actionGroup.appendChild(btn); } // 关键词匹配 if (_autoReplyEnabled && _autoReplyKeywords.length > 0) { const lower = post.innerText.toLowerCase(); const matchedKeyword = _autoReplyKeywords.find(kw => kw && lower.includes(kw.toLowerCase())); if (matchedKeyword) { if (checkLimits(post)) { queueAutoReply(post); } } } }); } // ============================================ // 菜单命令 // ============================================ GM_registerMenuCommand("⚙️ 打开 X Reply AI 设置", () => { openSettingsPanel("auto"); }); GM_registerMenuCommand("🔄 重置授权", () => { GM_setValue("license_key", ""); _isAuthorized = false; showActivationCard(); }); // ============================================ // 初始化 // ============================================ async function init() { const licenseKey = GM_getValue("license_key", ""); if (!licenseKey) { showActivationCard(); return; } const res = await verifyToken(licenseKey); if (res?.ok) { _isAuthorized = true; initFromStorage(); } else { GM_setValue("license_key", ""); showActivationCard(); } } // 每 2 秒扫描新帖子 setInterval(addButtons, 2000); // 启动 init(); })();