653 lines
25 KiB
JavaScript
653 lines
25 KiB
JavaScript
// sidepanel: 配置 + 运行控制 + 实时日志
|
||
(() => {
|
||
const DEFAULTS = {
|
||
baseUrl: "https://ai.88531.cn/v1",
|
||
apiKey: "",
|
||
model: "mimo-v2.6-flash",
|
||
keyword: "",
|
||
order: "hot",
|
||
mode: "tpl",
|
||
customText: "",
|
||
sendMode: "human",
|
||
tid: 0,
|
||
region: "",
|
||
dlQuality: "80",
|
||
maxN: 3,
|
||
minPlay: 0,
|
||
dmEnabled: false,
|
||
dmCount: 2,
|
||
dmText: ""
|
||
};
|
||
const FIELDS = ["baseUrl", "apiKey", "model", "keyword", "order", "mode", "customText", "sendMode", "tid", "region", "dlQuality", "maxN", "minPlay", "dmEnabled", "dmCount", "dmText"];
|
||
|
||
let settings = Object.assign({}, DEFAULTS);
|
||
let running = false;
|
||
let timerHandle = null;
|
||
let timerStart = 0;
|
||
|
||
const $ = id => document.getElementById(id);
|
||
|
||
// ============ 日志 / 状态 ============
|
||
|
||
function log(text, cls) {
|
||
const box = $("logBox");
|
||
if (!box) return;
|
||
const line = document.createElement("div");
|
||
if (cls) line.className = cls;
|
||
line.textContent = new Date().toTimeString().slice(0, 8) + " " + text;
|
||
box.appendChild(line);
|
||
while (box.children.length > 300) box.removeChild(box.firstChild);
|
||
box.scrollTop = box.scrollHeight;
|
||
}
|
||
|
||
function setStatus(text, isErr) {
|
||
const el = $("status");
|
||
if (!el) return;
|
||
el.textContent = text || "";
|
||
el.style.color = isErr ? "#d93025" : "#0a7";
|
||
}
|
||
|
||
// ============ 设置读写(自动保存,无保存按钮) ============
|
||
|
||
function fillForm() {
|
||
for (const k of FIELDS) {
|
||
const el = $(k);
|
||
if (!el) continue;
|
||
if (el.type === "checkbox") el.checked = (settings[k] === true || settings[k] === "true");
|
||
else el.value = settings[k];
|
||
}
|
||
refreshDmVisibility();
|
||
// 地区不记忆:每次打开默认「全部地区」,只在本次会话内有效
|
||
settings.region = "";
|
||
const reg = $("region");
|
||
if (reg) reg.value = "";
|
||
refreshCustomVisibility();
|
||
refreshModeWarn();
|
||
}
|
||
|
||
async function saveField(key, value) {
|
||
settings[key] = value;
|
||
const toSave = Object.assign({}, settings);
|
||
delete toSave.region; // 地区不写入存储(默认永远是不限)
|
||
await chrome.storage.local.set({ settings: toSave });
|
||
}
|
||
|
||
function refreshDmVisibility() {
|
||
const cb = $("dmEnabled"), det = $("dmDetail");
|
||
if (!cb || !det) return;
|
||
det.style.display = cb.checked ? "block" : "none";
|
||
}
|
||
|
||
function refreshCustomVisibility() {
|
||
const ta = $("customText");
|
||
if (ta) ta.style.display = settings.mode === "custom" ? "block" : "none";
|
||
}
|
||
|
||
function customValidLine(s) {
|
||
if (s.length < 3 || s.length > 200) return false;
|
||
if (/(?:https?:\/\/|www\.[a-z0-9-]+\.[a-z]{2,})/i.test(s) && !/bilibili\.com|b23\.tv/i.test(s)) return false;
|
||
if (new Set(s).size < Math.min(6, Math.ceil(s.length / 2))) return false;
|
||
return true;
|
||
}
|
||
|
||
function refreshModeWarn() {
|
||
const warn = $("modeWarn");
|
||
if (!warn) return;
|
||
const hasModel = !!(settings.baseUrl && settings.model);
|
||
if (settings.mode === "custom") {
|
||
const raw = String(settings.customText || "").split("\n").map(s => s.trim()).filter(Boolean);
|
||
const bad = raw.filter(line => !customValidLine(line));
|
||
if (!raw.length) {
|
||
warn.textContent = "⚠ 选了自定义但还没填内容,开始执行会被拒绝";
|
||
warn.style.display = "block";
|
||
} else if (bad.length === raw.length) {
|
||
warn.textContent = "⚠ 自定义内容全部不合规(每行需3~200字、不含链接、不能全是重复字),开始执行会被拒绝";
|
||
warn.style.display = "block";
|
||
} else if (bad.length) {
|
||
warn.textContent = "⚠ " + bad.length + " 行不合规(每行需3~200字),这些行不会被使用;有效 " + (raw.length - bad.length) + " 条";
|
||
warn.style.display = "block";
|
||
} else {
|
||
warn.style.display = "none";
|
||
}
|
||
} else if (settings.mode === "ai" && !settings.apiKey) {
|
||
warn.textContent = "⚠ 选了 AI 生成但 API Key 为空,将自动改用固定模板";
|
||
warn.style.display = "block";
|
||
} else if (settings.mode === "ai" && !hasModel) {
|
||
warn.textContent = "⚠ 选了 AI 生成但模型配置不全,将自动改用固定模板";
|
||
warn.style.display = "block";
|
||
} else if (settings.mode === "tpl" && settings.apiKey) {
|
||
warn.textContent = "⚠ 已填 API Key,但评论方式是固定模板(AI 配置暂不生效)";
|
||
warn.style.display = "block";
|
||
} else {
|
||
warn.style.display = "none";
|
||
}
|
||
}
|
||
|
||
// ============ 授权状态 ============
|
||
|
||
let licState = null; // {authorized, offline, realAuthorized, expire, trialEnd, trialLeftMs, machine}
|
||
let licTimer = null;
|
||
|
||
function fmtRemain(ms) {
|
||
if (ms <= 0) return "00:00:00";
|
||
const s = Math.floor(ms / 1000);
|
||
const h = String(Math.floor(s / 3600)).padStart(2, "0");
|
||
const m = String(Math.floor((s % 3600) / 60)).padStart(2, "0");
|
||
const ss = String(s % 60).padStart(2, "0");
|
||
return h + ":" + m + ":" + ss;
|
||
}
|
||
|
||
function renderLicense() {
|
||
const icon = $("licIcon"), text = $("licText"), sub = $("licSub"),
|
||
actRow = $("licActRow"), buy = $("licBuy");
|
||
if (!licState || !icon) return;
|
||
const lic = licState;
|
||
|
||
if (lic.offline) {
|
||
icon.textContent = "🌐";
|
||
text.textContent = "授权服务器暂不可用,已临时放行";
|
||
text.style.color = "#666";
|
||
sub.textContent = "网络恢复后会自动重新校验";
|
||
actRow.style.display = "none";
|
||
} else if (lic.authorized) {
|
||
icon.textContent = "✅";
|
||
text.textContent = "已授权" + (lic.expire ? " · 到期 " + lic.expire : "(长期)");
|
||
text.style.color = "#0a7";
|
||
sub.textContent = "感谢支持!全部功能可用";
|
||
actRow.style.display = "none";
|
||
if (buy) buy.style.display = "none";
|
||
} else if (lic.trialLeftMs > 0) {
|
||
icon.textContent = "🎁";
|
||
text.textContent = "免费试用中 · 剩余 " + fmtRemain(lic.trialLeftMs);
|
||
text.style.color = "#e67e22";
|
||
sub.textContent = "未授权可试用 1 天,购买卡密激活后不限时使用";
|
||
actRow.style.display = "flex";
|
||
if (buy) buy.style.display = "";
|
||
} else {
|
||
icon.textContent = "🔒";
|
||
text.textContent = "试用已到期,自动评论已锁定";
|
||
text.style.color = "#d93025";
|
||
sub.textContent = "购买卡密激活即可解锁(下载视频不受影响)";
|
||
actRow.style.display = "flex";
|
||
if (buy) buy.style.display = "";
|
||
}
|
||
}
|
||
|
||
function tickCountdown() {
|
||
if (!licState || licState.authorized || licState.offline) return;
|
||
const left = licState.trialEnd - Date.now();
|
||
licState.trialLeftMs = Math.max(0, left);
|
||
if (left <= 0) { renderLicense(); if (licTimer) { clearInterval(licTimer); licTimer = null; } return; }
|
||
const text = $("licText");
|
||
if (text) text.textContent = "免费试用中 · 剩余 " + fmtRemain(left);
|
||
}
|
||
|
||
async function loadLicense() {
|
||
let res = null;
|
||
try { res = await chrome.runtime.sendMessage({ action: "licenseStatus" }); }
|
||
catch (e) { res = null; }
|
||
if (!res || !res.ok || !res.license) {
|
||
licState = { authorized: true, offline: true, trialEnd: Date.now() };
|
||
renderLicense();
|
||
return;
|
||
}
|
||
licState = res.license;
|
||
const mt = $("machineIdText");
|
||
if (mt) mt.textContent = licState.machine || "";
|
||
renderLicense();
|
||
// 有新版本时提示(数据来自安装上报时服务端返回的 latest_version)
|
||
const upd = res.update || {};
|
||
const updMsg = $("updMsg");
|
||
if (updMsg && upd.available && upd.latest && upd.latest !== upd.current) {
|
||
updMsg.style.display = "block";
|
||
updMsg.innerHTML = "⬆ 有新版本 v" + String(upd.latest).replace(/[<>&"]/g, "") + "(当前 v" + String(upd.current).replace(/[<>&"]/g, "") + ')<a href="#" id="updGo" style="color:#00A1D6;margin-left:6px">去更新 →</a>';
|
||
const go = $("updGo");
|
||
if (go) go.addEventListener("click", ev => { ev.preventDefault(); chrome.tabs.create({ url: "https://dl.88531.cn/bili.html" }); });
|
||
}
|
||
// 倒计时(仅试用中)
|
||
if (licTimer) { clearInterval(licTimer); licTimer = null; }
|
||
if (licState && !licState.authorized && !licState.offline && licState.trialLeftMs > 0) {
|
||
licTimer = setInterval(tickCountdown, 1000);
|
||
}
|
||
// 公告
|
||
const notice = $("licNotice");
|
||
const anns = Array.isArray(res.announcements) ? res.announcements : [];
|
||
if (notice && anns.length) {
|
||
notice.style.display = "block";
|
||
notice.innerHTML = anns.map(a => {
|
||
const t = document.createElement("span");
|
||
t.textContent = "📌 " + a.title + (a.content ? ":" + a.content : "");
|
||
let html = "";
|
||
try { html = t.innerHTML; } catch (e) { html = "📌 " + a.title; }
|
||
if (a.link) html += ' <a href="#" class="annLink" data-url="' + String(a.link).replace(/"/g, """) + '" style="color:#00A1D6">详情 →</a>';
|
||
return html;
|
||
}).join("<br>");
|
||
for (const a of notice.querySelectorAll(".annLink")) {
|
||
a.addEventListener("click", ev => {
|
||
ev.preventDefault();
|
||
const url = a.getAttribute("data-url");
|
||
if (url) chrome.tabs.create({ url: url });
|
||
});
|
||
}
|
||
} else if (notice) {
|
||
notice.style.display = "none";
|
||
}
|
||
}
|
||
|
||
async function activateCard() {
|
||
const btn = $("btnActivate"), input = $("cardKey"), msg = $("actMsg");
|
||
if (!btn || !input || !msg) return;
|
||
const card = String(input.value || "").trim();
|
||
if (!card) { msg.textContent = "请输入卡密"; msg.style.color = "#d93025"; return; }
|
||
btn.disabled = true;
|
||
msg.textContent = "激活中…";
|
||
msg.style.color = "#666";
|
||
let res = null;
|
||
try { res = await chrome.runtime.sendMessage({ action: "activateCard", card: card }); }
|
||
catch (e) { res = { ok: false, error: String(e) }; }
|
||
if (res && res.ok) {
|
||
msg.textContent = "✓ " + (res.message || "激活成功");
|
||
msg.style.color = "#0a7";
|
||
log("授权激活成功:" + (res.message || ""), "ok");
|
||
input.value = "";
|
||
await loadLicense(); // 重新拉状态,横幅变已授权
|
||
} else {
|
||
msg.textContent = "✗ " + ((res && (res.message || res.error)) || "激活失败");
|
||
msg.style.color = "#d93025";
|
||
log("授权激活失败:" + ((res && (res.message || res.error)) || ""), "fail");
|
||
}
|
||
btn.disabled = false;
|
||
}
|
||
|
||
function licenseBlocked() {
|
||
return !!(licState && !licState.authorized && !licState.offline && licState.trialLeftMs <= 0);
|
||
}
|
||
|
||
// ============ 运行控制 ============
|
||
|
||
function fmtElapsed(ms) {
|
||
const s = Math.floor(ms / 1000);
|
||
return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
|
||
}
|
||
|
||
function startTimer() {
|
||
stopTimer(false);
|
||
timerStart = Date.now();
|
||
const el = $("runTimer");
|
||
if (el) el.textContent = "计时 00:00";
|
||
timerHandle = setInterval(() => {
|
||
const t = $("runTimer");
|
||
if (t) t.textContent = "计时 " + fmtElapsed(Date.now() - timerStart);
|
||
}, 1000);
|
||
}
|
||
|
||
function stopTimer(freeze) {
|
||
if (timerHandle) { clearInterval(timerHandle); timerHandle = null; }
|
||
if (freeze) {
|
||
const t = $("runTimer");
|
||
if (t && timerStart) t.textContent = "完成 " + fmtElapsed(Date.now() - timerStart);
|
||
}
|
||
}
|
||
|
||
function setRunning(on) {
|
||
running = on;
|
||
const btnStart = $("btnStart"), stopRow = $("stopRow");
|
||
if (btnStart) btnStart.style.display = on ? "none" : "block";
|
||
if (stopRow) stopRow.style.display = on ? "flex" : "none";
|
||
if (!on) { stopTimer(true); }
|
||
}
|
||
|
||
async function startRun() {
|
||
const kw = String(settings.keyword || "").trim();
|
||
if (!kw) { setStatus("请先填搜索关键词", true); const el = $("keyword"); if (el) el.focus(); return; }
|
||
// 授权闸门(本地先拦,后台还有一道)
|
||
if (licenseBlocked()) {
|
||
setStatus("试用已到期,自动评论已锁定。购买卡密激活后即可使用(下载视频不受影响)", true);
|
||
log("⛔ 试用已到期,自动评论被锁定", "fail");
|
||
const ck = $("cardKey"); if (ck) ck.focus();
|
||
return;
|
||
}
|
||
// 自定义内容本地预检:无效就不开始,绝不换成模板乱评
|
||
if (settings.mode === "custom") {
|
||
const raw = String(settings.customText || "").split("\n").map(s => s.trim()).filter(Boolean);
|
||
const valid = raw.filter(customValidLine);
|
||
if (!raw.length) {
|
||
setStatus("选了自定义评论但内容为空:请在「评论方式=自定义」下方填写,每行一条", true);
|
||
const ta = $("customText"); if (ta) ta.focus();
|
||
return;
|
||
}
|
||
if (!valid.length) {
|
||
setStatus("自定义内容全部不合规(每行需3~200字、不含外链),请修改后再开始", true);
|
||
log("⛔ 自定义内容不合规,已拒绝开始", "fail");
|
||
const ta = $("customText"); if (ta) ta.focus();
|
||
return;
|
||
}
|
||
log("自定义评论 " + valid.length + " 条已启用" + (raw.length > valid.length ? "(" + (raw.length - valid.length) + " 行不合规已忽略)" : ""), "step");
|
||
}
|
||
if (settings.mode === "ai" && !(settings.baseUrl && settings.model)) {
|
||
log("⚠ 未配置模型,本次将使用固定模板", "fail");
|
||
}
|
||
$("doneList").innerHTML = "";
|
||
setStatus("正在连接B站页面…");
|
||
setRunning(true);
|
||
startTimer();
|
||
log("开始执行:关键词「" + kw + "」最多 " + settings.maxN + " 条,每条间隔 1~2 分钟随机", "step");
|
||
|
||
let res = null;
|
||
try {
|
||
res = await chrome.runtime.sendMessage({ action: "startRun", config: Object.assign({}, settings, {
|
||
keyword: kw,
|
||
maxN: Number(settings.maxN) || 3,
|
||
minPlay: Number(settings.minPlay) || 0,
|
||
dmEnabled: settings.dmEnabled === true || settings.dmEnabled === "true",
|
||
dmCount: Math.max(1, Math.min(5, Number(settings.dmCount) || 2)),
|
||
dmText: String(settings.dmText || "").trim()
|
||
}) });
|
||
} catch (e) {
|
||
res = { ok: false, error: String(e) };
|
||
}
|
||
if (!res || !res.ok) {
|
||
const err = (res && res.error) || "启动失败";
|
||
setStatus(err, true);
|
||
log("❌ " + err, "fail");
|
||
setRunning(false);
|
||
} else {
|
||
setStatus("执行中,进度见页面右上角浮层");
|
||
}
|
||
}
|
||
|
||
async function stopRun() {
|
||
setStatus("停止中,将在当前步骤后停止…");
|
||
log("发送停止指令", "step");
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({ action: "stopRun" });
|
||
if (res && !res.ok) setStatus("停止失败:" + res.error, true);
|
||
} catch (e) {
|
||
setStatus("停止失败:" + String(e), true);
|
||
}
|
||
}
|
||
|
||
// ============ 下载当前页视频 ============
|
||
|
||
function fmtMb(n) { return (n / 1048576).toFixed(1) + " MB"; }
|
||
|
||
// 面板页自己抓文件(扩展页 fetch 免 CORS,DNR 规则注入 Referer)→ Blob → 触发保存
|
||
async function fetchToBlob(url, onProgress) {
|
||
const resp = await fetch(url);
|
||
if (!resp.ok) throw new Error("HTTP " + resp.status + "(CDN拒绝,可能需要重新打开视频页)");
|
||
const total = Number(resp.headers.get("content-length") || 0);
|
||
const reader = resp.body.getReader();
|
||
const chunks = [];
|
||
let got = 0;
|
||
for (;;) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
chunks.push(value);
|
||
got += value.length;
|
||
if (onProgress) onProgress(got, total);
|
||
}
|
||
return new Blob(chunks);
|
||
}
|
||
|
||
async function downloadCurrent() {
|
||
const btn = $("btnDownload"), msg = $("dlMsg");
|
||
if (!btn || !msg) return;
|
||
btn.disabled = true;
|
||
const setText = (t, color) => { msg.textContent = t; msg.style.color = color || "#666"; };
|
||
setText("获取视频地址…");
|
||
log("⬇ 开始下载当前页视频(清晰度 " + (settings.dlQuality === "80" ? "最高可用" : settings.dlQuality + "P") + ")", "step");
|
||
|
||
let res = null;
|
||
try {
|
||
res = await chrome.runtime.sendMessage({ action: "getDownloadInfo", qn: Number(settings.dlQuality) || 80 });
|
||
} catch (e) {
|
||
res = { ok: false, error: String(e) };
|
||
}
|
||
if (!res || !res.ok || !res.info) {
|
||
const err = (res && res.error) || "获取失败";
|
||
setText("✗ " + err, "#d93025");
|
||
log("⬇ 下载失败:" + err, "fail");
|
||
btn.disabled = false;
|
||
return;
|
||
}
|
||
|
||
const info = res.info;
|
||
const files = info.files;
|
||
const base = String(info.title || "bilibili视频").replace(/[\\/:*?"<>|]/g, "_").slice(0, 80);
|
||
const saved = [], errors = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
const f = files[i];
|
||
let name = base;
|
||
if (f.tag) name += "_" + f.tag;
|
||
else if (files.length > 1) name += "_第" + (i + 1) + "段";
|
||
name += "." + (f.ext || "mp4");
|
||
try {
|
||
let lastShow = 0;
|
||
const blob = await fetchToBlob(f.url, (got, total) => {
|
||
const now = Date.now();
|
||
if (now - lastShow > 500) {
|
||
lastShow = now;
|
||
setText("下载中 " + fmtMb(got) + (total ? " / " + fmtMb(total) : "") + "(" + name.slice(0, 18) + "…)");
|
||
}
|
||
});
|
||
const objUrl = URL.createObjectURL(blob);
|
||
await chrome.downloads.download({
|
||
url: objUrl, filename: "B站视频/" + name, conflictAction: "uniquify", saveAs: false
|
||
});
|
||
setTimeout(() => URL.revokeObjectURL(objUrl), 60000); // 给下载留足时间
|
||
saved.push(name + "(" + fmtMb(blob.size) + ")");
|
||
log("⬇ 已保存:" + name + "(" + fmtMb(blob.size) + ")", "ok");
|
||
} catch (e) {
|
||
errors.push(name + ":" + String(e && e.message || e));
|
||
log("⬇ 失败:" + name + ":" + String(e && e.message || e), "fail");
|
||
}
|
||
}
|
||
|
||
if (saved.length) {
|
||
const note = info.note ? "," + info.note : "";
|
||
const partial = errors.length ? ",部分失败:" + errors.join(";") : "";
|
||
setText("✓ " + saved.length + " 个文件(" + info.quality + ")" + (info.note ? "(分离格式)" : ""), "#0a7");
|
||
log("⬇ 完成:" + saved.join("、") + note + partial, "ok");
|
||
} else {
|
||
setText("✗ 下载失败:" + errors.join(";"), "#d93025");
|
||
log("⬇ 下载失败:" + errors.join(";"), "fail");
|
||
}
|
||
btn.disabled = false;
|
||
}
|
||
|
||
// ============ 事件回传 ============
|
||
|
||
function addDoneItem(item) {
|
||
const box = $("doneList");
|
||
if (!box) return;
|
||
const bvid = item.bvid || "";
|
||
// 同一视频只显示一条(防止重复消息把成功列表刷成两条)
|
||
let exist = null;
|
||
for (const child of box.children) {
|
||
if (child.dataset && child.dataset.bvid === bvid) { exist = child; break; }
|
||
}
|
||
const div = exist || document.createElement("div");
|
||
div.className = "done";
|
||
div.dataset.bvid = bvid;
|
||
div.innerHTML = "";
|
||
// 标题可点击 → 打开视频页
|
||
const t = document.createElement("a");
|
||
t.className = "dt";
|
||
t.textContent = item.title || bvid;
|
||
t.href = "https://www.bilibili.com/video/" + bvid;
|
||
t.target = "_blank";
|
||
t.rel = "noopener";
|
||
t.title = "点击打开视频";
|
||
const c = document.createElement("div"); c.className = "dc"; c.textContent = item.text || "";
|
||
const m = document.createElement("div"); m.className = "dm";
|
||
m.textContent = "BV:" + bvid + "(点标题查看视频)";
|
||
div.appendChild(t); div.appendChild(c); div.appendChild(m);
|
||
if (!exist) box.prepend(div);
|
||
}
|
||
|
||
function onFinished(ev) {
|
||
setRunning(false);
|
||
if (ev.ok) {
|
||
const msg = "执行完成:成功 " + (ev.done || 0) + " 条" + (ev.failed ? ",失败 " + ev.failed + " 条" : "");
|
||
setStatus(msg);
|
||
log("✅ " + msg, "ok");
|
||
} else {
|
||
const msg = ev.reason || "执行未完成";
|
||
setStatus(msg, true);
|
||
log("⏹ " + msg, "fail");
|
||
}
|
||
refreshStats();
|
||
}
|
||
|
||
chrome.runtime.onMessage.addListener(msg => {
|
||
if (!msg || !msg.relay) return;
|
||
const ev = msg.relay;
|
||
if (ev.type === "log") log(ev.text, ev.cls === "ok" ? "ok" : ev.cls === "fail" ? "fail" : "step");
|
||
else if (ev.type === "done") addDoneItem(ev);
|
||
else if (ev.type === "finished") onFinished(ev);
|
||
else if (ev.type === "ready") log("B站页面内容脚本就绪", "ok");
|
||
});
|
||
|
||
// ============ 评论记录统计 ============
|
||
|
||
async function refreshStats() {
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({ action: "getStats" });
|
||
if (res && res.stats) {
|
||
$("stTotal").textContent = res.stats.total || 0;
|
||
$("stWeek").textContent = res.stats.week || 0;
|
||
$("stToday").textContent = res.stats.today || 0;
|
||
}
|
||
} catch (e) { /* 忽略 */ }
|
||
}
|
||
|
||
let clearArmed = false, clearTimer = null;
|
||
async function onClearHist() {
|
||
const btn = $("btnClearHist");
|
||
if (!clearArmed) {
|
||
clearArmed = true;
|
||
if (btn) btn.textContent = "再点一次清空";
|
||
clearTimer = setTimeout(() => { clearArmed = false; if (btn) btn.textContent = "清空"; }, 3000);
|
||
return;
|
||
}
|
||
clearTimeout(clearTimer); clearArmed = false;
|
||
if (btn) btn.textContent = "清空";
|
||
try {
|
||
await chrome.runtime.sendMessage({ action: "clearHistory" });
|
||
setStatus("评论记录已清空");
|
||
log("评论记录已清空", "step");
|
||
} catch (e) { setStatus("清空失败:" + String(e), true); }
|
||
refreshStats();
|
||
}
|
||
|
||
// ============ LLM 测试连接 ============
|
||
|
||
async function testCfg() {
|
||
const btn = $("testCfg"), msg = $("testMsg");
|
||
if (btn) btn.disabled = true;
|
||
if (msg) msg.textContent = "测试中…";
|
||
try {
|
||
const res = await chrome.runtime.sendMessage({
|
||
action: "callAiApi",
|
||
baseUrl: settings.baseUrl, apiKey: settings.apiKey, model: settings.model,
|
||
prompt: "只回复两个字:正常", maxTokens: 60, timeoutMs: 60000
|
||
});
|
||
if (res && res.ok) {
|
||
if (msg) { msg.textContent = "✓ 成功:" + String(res.content).slice(0, 20); msg.style.color = "#0a7"; }
|
||
log("LLM 测试成功", "ok");
|
||
} else {
|
||
if (msg) { msg.textContent = "✗ 失败:" + ((res && res.error) || "未知"); msg.style.color = "#d93025"; }
|
||
log("LLM 测试失败:" + ((res && res.error) || "未知"), "fail");
|
||
}
|
||
} catch (e) {
|
||
if (msg) { msg.textContent = "✗ 失败:" + String(e); msg.style.color = "#d93025"; }
|
||
}
|
||
if (btn) btn.disabled = false;
|
||
}
|
||
|
||
// ============ 初始化 ============
|
||
|
||
async function init() {
|
||
let saved = {};
|
||
try { saved = (await chrome.storage.local.get("settings")).settings || {}; } catch (e) {}
|
||
settings = Object.assign({}, DEFAULTS, saved);
|
||
fillForm();
|
||
refreshStats();
|
||
log("就绪:填关键词后点「▶ 开始执行」");
|
||
|
||
for (const k of FIELDS) {
|
||
const el = $(k);
|
||
if (!el) continue;
|
||
el.addEventListener("change", () => {
|
||
const v = (el.type === "checkbox") ? el.checked
|
||
: (el.type === "number") ? (Number(el.value) || 0) : el.value;
|
||
saveField(k, v);
|
||
if (k === "mode") refreshCustomVisibility();
|
||
if (k === "dmEnabled") refreshDmVisibility();
|
||
if (k === "mode" || k === "apiKey" || k === "baseUrl" || k === "model" || k === "customText") refreshModeWarn();
|
||
});
|
||
if (el.tagName === "TEXTAREA" || el.type === "text" || el.type === "password") {
|
||
el.addEventListener("input", () => {
|
||
settings[k] = el.value;
|
||
saveField(k, el.value);
|
||
if (k === "customText") refreshModeWarn(); // 边填边消警告
|
||
});
|
||
}
|
||
}
|
||
|
||
const promo = $("promoLink");
|
||
if (promo) promo.addEventListener("click", () => {
|
||
if (!settings.baseUrl) saveField("baseUrl", DEFAULTS.baseUrl);
|
||
chrome.tabs.create({ url: "https://ai.88531.cn/register" });
|
||
});
|
||
|
||
const start = $("btnStart");
|
||
if (start) start.addEventListener("click", startRun);
|
||
const stop = $("btnStopRun");
|
||
if (stop) stop.addEventListener("click", stopRun);
|
||
const test = $("testCfg");
|
||
if (test) test.addEventListener("click", testCfg);
|
||
const clear = $("btnClearHist");
|
||
if (clear) clear.addEventListener("click", onClearHist);
|
||
const dl = $("btnDownload");
|
||
if (dl) dl.addEventListener("click", downloadCurrent);
|
||
|
||
// 获取最新版本 → 官网页(新标签页)
|
||
const latest = $("btnLatest");
|
||
if (latest) latest.addEventListener("click", () => {
|
||
chrome.tabs.create({ url: "https://dl.88531.cn/bili.html" });
|
||
log("已打开官网页面(新标签页):dl.88531.cn/bili.html", "step");
|
||
});
|
||
|
||
// 授权区
|
||
const buy = $("licBuy");
|
||
if (buy) buy.addEventListener("click", () => chrome.tabs.create({ url: "https://auth.88531.cn" }));
|
||
const act = $("btnActivate");
|
||
if (act) act.addEventListener("click", activateCard);
|
||
const card = $("cardKey");
|
||
if (card) card.addEventListener("input", () => {
|
||
const msg = $("actMsg");
|
||
if (msg && msg.textContent) { msg.textContent = ""; }
|
||
});
|
||
const mach = $("licMachine");
|
||
if (mach) mach.addEventListener("click", async () => {
|
||
const id = licState && licState.machine;
|
||
if (!id) return;
|
||
try {
|
||
await navigator.clipboard.writeText(id);
|
||
mach.title = "已复制:" + id;
|
||
} catch (e) {
|
||
const tmp = document.createElement("textarea");
|
||
tmp.value = id; document.body.appendChild(tmp); tmp.select();
|
||
try { document.execCommand("copy"); } catch (e2) {}
|
||
document.body.removeChild(tmp);
|
||
}
|
||
const t = $("machineIdText");
|
||
if (t) { const old = t.textContent; t.textContent = old + "(已复制)"; setTimeout(() => { t.textContent = old; }, 1500); }
|
||
});
|
||
loadLicense();
|
||
}
|
||
|
||
init();
|
||
})();
|