// 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(); })();