Files
bili-auto-comment/sidepanel.js
T

682 lines
27 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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, "") + ')<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, "&quot;") + '" 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,
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();
})();