// 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,
sendWhat: "comment",
dmCount: 2,
dmText: ""
};
const FIELDS = ["baseUrl", "apiKey", "model", "keyword", "order", "mode", "customText", "sendMode", "tid", "region", "dlQuality", "maxN", "minPlay", "sendWhat", "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 sel = $("sendWhat"), box = $("dmBox");
if (!sel || !box) return;
// 选了「只发私信」或「评论+私信」才展开私信设置区
box.style.display = (sel.value === "dm" || sel.value === "both") ? "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, "") + ')去更新 →';
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 += ' 详情 →';
return html;
}).join("
");
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,
sendWhat: ["comment", "dm", "both"].indexOf(settings.sendWhat) >= 0 ? settings.sendWhat : "comment",
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 → 触发保存
// 取视频流并封装成 Blob。
// 两个必须修的坑:
// 1) Blob 必须带 MIME —— 不带的话 Chrome 会去嗅探内容,HTML 错误页会被存成 .txt/.html
// 2) 必须校验内容魔数 —— CDN 有时回 200 + HTML 验证页,不校验就会把网页存成"视频"
async function fetchToBlob(url, onProgress, mime) {
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);
}
if (!chunks.length) throw new Error("CDN 返回了空内容");
// 魔数校验:MP4/FLV/M4S 都是二进制;返回 HTML/JSON 说明拿到的是错误页
const head = chunks[0];
const b0 = head[0], b1 = head[1], b2 = head[2], b3 = head[3];
const isFtyp = head.length >= 12 && b4(head,0)===0x66 && b4(head,1)===0x74 && b4(head,2)===0x79 && b4(head,3)===0x70; // "ftyp"
const isFlv = head.length >= 3 && b0===0x46 && b1===0x4C && b2===0x56; // "FLV"
const isIso = head.length >= 8 && b0===0x00 && b1===0x00; // MP4 长度前缀
const isHtml = (b0===0x3C) || (b0===0xEF && b1===0xBB && b2===0xBF) || // "<" 或 BOM+"<"
(b0===0x0A || b0===0x0D || b0===0x20) || // 空白开头
(b0===0x7B); // "{" JSON
if (isHtml) {
throw new Error("CDN 返回的不是视频(是网页/错误页),请刷新视频页后重试");
}
if (!isFtyp && !isFlv && !isIso) {
throw new Error("下载内容不是有效的视频格式(文件头异常),已放弃保存");
}
// MIME:优先用响应头,再按魔数判定,兜底 mp4 —— 绝不给空字符串
const ct = (resp.headers.get("content-type") || "").split(";")[0].trim();
let type = ct;
if (!/^video\//i.test(type) && !/^audio\//i.test(type)) {
type = isFtyp || isIso ? "video/mp4" : (isFlv ? "video/x-flv" : "video/mp4");
}
return new Blob(chunks, { type: type });
}
function b4(u8, i) { return u8.length > i ? u8[i] : -1; }
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 MIME_BY_EXT = { mp4: "video/mp4", flv: "video/x-flv", m4s: "video/mp4", m4a: "audio/mp4", aac: "audio/aac" };
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) + "…)");
}
}, MIME_BY_EXT[(f.ext || "mp4").toLowerCase()] || "video/mp4");
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 === "sendWhat") 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();
})();