// ==UserScript==
// @name X Reply AI - 自动回复助手
// @namespace https://github.com/x-reply-ai
// @version 1.2.2
// @description X 推文自动回复助手,支持多 AI 服务商,可设置浏览量/评论数/分享数限制
// @author X Reply AI Team
// @match https://x.com/*
// @match https://twitter.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @connect api.deepseek.com
// @connect api.openai.com
// @connect api.anthropic.com
// @connect generativelanguage.googleapis.com
// @connect 111.229.201.174
// @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();
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: `${AUTH_BASE}/v1/payments/activate?email=${encodeURIComponent(FIXED_EMAIL)}&licenseKey=${encodeURIComponent(licenseKey)}&hwid=${encodeURIComponent(hwid)}`,
headers: { "Content-Type": "application/x-www-form-urlencoded" },
onload: (res) => {
let data = {};
try {
const ct = res.getResponseHeader("content-type") || "";
if (ct.includes("application/json")) {
data = JSON.parse(res.responseText);
} else {
// 兜底:尝试解析 JSON,即使 content-type 不匹配
try {
data = JSON.parse(res.responseText);
} catch {
data = { msg: res.responseText.trim() };
}
}
} catch {}
resolve({ httpOk: res.status >= 200 && res.status < 300, code: data.code, msg: data.msg });
},
onerror: () => resolve({ httpOk: false, code: 0, msg: "网络错误" })
});
});
}
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 = `
`;
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 = `
请输入授权码以激活插件功能,授权码将永久绑定到本设备。
授权码
目前测试阶段,免费提供激活码: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();
})();