v1.7.0: 新增私信评论区用户(wbi签名+永久按mid去重+2~4分钟间隔)、安装统计上报、有新版本提示

This commit is contained in:
personal82555 committed 2026-10-13 18:38:58 +08:00
1 parent 38b90c1448
commit 0cce6cc2bb
5 files changed
+344 -9

No files matched your search

+41 -1
View File
@@ -29,6 +29,41 @@ async function ensureDownloadHeadersRule() {
}
ensureDownloadHeadersRule();
// ============ 安装统计(CardAuth installations:只上报机器码+版本,不碰B站账号) ============
// 用 check-update 接口登记(服务端会写 installations 表并返回 install_id),
// 顺带拿到 latest_version → 面板可提示「有新版本」
let installReported = false;
async function reportInstall(force) {
if (installReported && !force) return;
try {
const machine = await getMachineId();
const ver = chrome.runtime.getManifest().version;
const ua = (typeof navigator !== "undefined" && navigator.userAgent) || "";
const m = ua.match(/(Chrome|Edg)\/[\d.]+/);
const r = await authFetch("/public/installations/check-update", {
method: "POST",
headers: { "Content-Type": "application/json", "X-Api-Key": CARD_API_KEY },
body: JSON.stringify({
project_id: PROJECT_ID,
machine_id: machine,
client_version: ver,
device_info: (m ? m[0] : "chrome-extension")
})
}, 8000);
if (r && r.code === 200 && r.data) {
installReported = true;
await chrome.storage.local.set({
updateAvailable: !!r.data.update_available,
latestVersion: String(r.data.latest_version || "")
});
}
} catch (e) { /* 统计失败不影响任何功能,也不阻塞用户 */ }
}
if (chrome.runtime.onInstalled) {
chrome.runtime.onInstalled.addListener(() => reportInstall(true));
}
reportInstall(false); // SW 启动即登记一次(同一浏览器只记一条)
// ============ 授权(CardAuth: auth.88531.cn,项目=B站评论插件) ============
const AUTH_BASE = "https://auth.88531.cn/api";
@@ -517,7 +552,12 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
const license = await checkLicense();
let announcements = [];
try { announcements = await getAnnouncements(); } catch (e) { announcements = []; }
sendResponse({ ok: true, license: license, announcements: announcements });
const st = await chrome.storage.local.get(["updateAvailable", "latestVersion"]);
reportInstall(false); // 面板打开时补一次登记(SW 冷启动后 onInstalled 不会再触发)
sendResponse({
ok: true, license: license, announcements: announcements,
update: { available: !!st.updateAvailable, latest: String(st.latestVersion || ""), current: chrome.runtime.getManifest().version }
});
})();
return true;
}
+243 -1
View File
@@ -136,6 +136,194 @@
} catch (e) { return false; }
}
// ============ 私信(web_im send_msg,带 wbi 签名) ============
// B站私信接口:POST https://api.vc.bilibili.com/web_im/v1/web_im/send_msg
// 鉴权 wbi(w_rid + wts)+ csrf;只支持 msg_type=1(文字)/2(图)/5(撤回)
// 关键风控:21047「对方主动回复或关注你前,最多发送1条」→ 必须按 mid 永久去重
const MIXIN_INDEX = [46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,33,9,42,19,29,28,14,39,12,38,41,13,37,48,7,16,24,55,40,61,26,17,0,1,60,51,30,4,22,25,54,21,56,59,6,63,57,62,11,36,20,34,44,52];
/* MD5 (RFC 1321),私信 w_rid 需要;下面用标准实现,测试向量见下方 selftest */
function md5(str) {
function rl(n, c) { return (n << c) | (n >>> (32 - c)); }
function au(x, y) { var l = (x & 0xFFFF) + (y & 0xFFFF); var m = (x >> 16) + (y >> 16) + (l >> 16); return (m << 16) | (l & 0xFFFF); }
function cmn(q, a, b, x, s, t) { return au(rl(au(au(a, q), au(x, t)), s), b); }
function ff(a, b, c, d, x, s, t) { return cmn((b & c) | (~b & d), a, b, x, s, t); }
function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & ~d), a, b, x, s, t); }
function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); }
function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | ~d), a, b, x, s, t); }
function utf8Bytes(s) {
var out = [], i, c;
for (i = 0; i < s.length; i++) {
c = s.charCodeAt(i);
if (c < 0x80) out.push(c);
else if (c < 0x800) { out.push(0xC0 | (c >> 6), 0x80 | (c & 0x3F)); }
else if (c >= 0xD800 && c <= 0xDBFF && i + 1 < s.length) {
var c2 = s.charCodeAt(++i);
var cp = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00);
out.push(0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3F), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F));
} else { out.push(0xE0 | (c >> 12), 0x80 | ((c >> 6) & 0x3F), 0x80 | (c & 0x3F)); }
}
return out;
}
function toBlocks(s) {
var b = utf8Bytes(s), n = b.length;
var nb = ((n + 8) >> 6) + 1, blks = new Array(nb * 16).fill(0);
for (var i = 0; i < n; i++) blks[i >> 2] |= b[i] << ((i % 4) * 8);
blks[i >> 2] |= 0x80 << ((i % 4) * 8);
blks[nb * 16 - 2] = n * 8;
return blks;
}
var x = toBlocks(str), a = 1732584193, b = -271733879, c = -1732584194, d = 271733878;
for (var i = 0; i < x.length; i += 16) {
var oa = a, ob = b, oc = c, od = d;
a = ff(a, b, c, d, x[i], 7, -680876936); d = ff(d, a, b, c, x[i + 1], 12, -389564586); c = ff(c, d, a, b, x[i + 2], 17, 606105819); b = ff(b, c, d, a, x[i + 3], 22, -1044525330);
a = ff(a, b, c, d, x[i + 4], 7, -176418897); d = ff(d, a, b, c, x[i + 5], 12, 1200080426); c = ff(c, d, a, b, x[i + 6], 17, -1473231341); b = ff(b, c, d, a, x[i + 7], 22, -45705983);
a = ff(a, b, c, d, x[i + 8], 7, 1770035416); d = ff(d, a, b, c, x[i + 9], 12, -1958414417); c = ff(c, d, a, b, x[i + 10], 17, -42063); b = ff(b, c, d, a, x[i + 11], 22, -1990404162);
a = ff(a, b, c, d, x[i + 12], 7, 1804603682); d = ff(d, a, b, c, x[i + 13], 12, -40341101); c = ff(c, d, a, b, x[i + 14], 17, -1502002290); b = ff(b, c, d, a, x[i + 15], 22, 1236535329);
a = gg(a, b, c, d, x[i + 1], 5, -165796510); d = gg(d, a, b, c, x[i + 6], 9, -1069501632); c = gg(c, d, a, b, x[i + 11], 14, 643717713); b = gg(b, c, d, a, x[i], 20, -373897302);
a = gg(a, b, c, d, x[i + 5], 5, -701558691); d = gg(d, a, b, c, x[i + 10], 9, 38016083); c = gg(c, d, a, b, x[i + 15], 14, -660478335); b = gg(b, c, d, a, x[i + 4], 20, -405537848);
a = gg(a, b, c, d, x[i + 9], 5, 568446438); d = gg(d, a, b, c, x[i + 14], 9, -1019803690); c = gg(c, d, a, b, x[i + 3], 14, -187363961); b = gg(b, c, d, a, x[i + 8], 20, 1163531501);
a = gg(a, b, c, d, x[i + 13], 5, -1444681467); d = gg(d, a, b, c, x[i + 2], 9, -51403784); c = gg(c, d, a, b, x[i + 7], 14, 1735328473); b = gg(b, c, d, a, x[i + 12], 20, -1926607734);
a = hh(a, b, c, d, x[i + 5], 4, -378558); d = hh(d, a, b, c, x[i + 8], 11, -2022574463); c = hh(c, d, a, b, x[i + 11], 16, 1839030562); b = hh(b, c, d, a, x[i + 14], 23, -35309556);
a = hh(a, b, c, d, x[i + 1], 4, -1530992060); d = hh(d, a, b, c, x[i + 4], 11, 1272893353); c = hh(c, d, a, b, x[i + 7], 16, -155497632); b = hh(b, c, d, a, x[i + 10], 23, -1094730640);
a = hh(a, b, c, d, x[i + 13], 4, 681279174); d = hh(d, a, b, c, x[i], 11, -358537222); c = hh(c, d, a, b, x[i + 3], 16, -722521979); b = hh(b, c, d, a, x[i + 6], 23, 76029189);
a = hh(a, b, c, d, x[i + 9], 4, -640364487); d = hh(d, a, b, c, x[i + 12], 11, -421815835); c = hh(c, d, a, b, x[i + 15], 16, 530742520); b = hh(b, c, d, a, x[i + 2], 23, -995338651);
a = ii(a, b, c, d, x[i], 6, -198630844); d = ii(d, a, b, c, x[i + 7], 10, 1126891415); c = ii(c, d, a, b, x[i + 14], 15, -1416354905); b = ii(b, c, d, a, x[i + 5], 21, -57434055);
a = ii(a, b, c, d, x[i + 12], 6, 1700485571); d = ii(d, a, b, c, x[i + 3], 10, -1894986606); c = ii(c, d, a, b, x[i + 10], 15, -1051523); b = ii(b, c, d, a, x[i + 1], 21, -2054922799);
a = ii(a, b, c, d, x[i + 8], 6, 1873313359); d = ii(d, a, b, c, x[i + 15], 10, -30611744); c = ii(c, d, a, b, x[i + 6], 15, -1560198380); b = ii(b, c, d, a, x[i + 13], 21, 1309151649);
a = ii(a, b, c, d, x[i + 4], 6, -145523070); d = ii(d, a, b, c, x[i + 11], 10, -1120210379); c = ii(c, d, a, b, x[i + 2], 15, 718787259); b = ii(b, c, d, a, x[i + 9], 21, -343485551);
a = au(a, oa); b = au(b, ob); c = au(c, oc); d = au(d, od);
}
function hx(n) { var s = "", k; for (k = 0; k < 4; k++) s += "0123456789abcdef".charAt((n >> (k * 8 + 4)) & 0x0F) + "0123456789abcdef".charAt((n >> (k * 8)) & 0x0F); return s; }
return hx(a) + hx(b) + hx(c) + hx(d);
}
let WBI_CACHE = null;
async function getWbiMixin() {
if (WBI_CACHE) return WBI_CACHE;
const data = await apiJSON("https://api.bilibili.com/x/web-interface/nav");
const wi = data && data.data && data.data.wbi_img;
if (!wi || !wi.img_url || !wi.sub_url) throw new Error("拿不到 wbi 密钥(未登录或接口异常)");
const k1 = String(wi.img_url).split("/").pop().split(".")[0];
const k2 = String(wi.sub_url).split("/").pop().split(".")[0];
const raw = k1 + k2;
let mixin = "";
for (const i of MIXIN_INDEX) mixin += raw[i] || "";
WBI_CACHE = mixin.slice(0, 32);
return WBI_CACHE;
}
async function signParams(params) {
const mixin = await getWbiMixin();
const p = Object.assign({}, params);
p.wts = String(Math.floor(Date.now() / 1000));
const clean = {};
for (const k of Object.keys(p).sort()) clean[k] = String(p[k]).replace(/[!'()*]/g, "");
const q = Object.keys(clean).map(k => k + "=" + encodeURIComponent(clean[k])).join("&");
const w_rid = md5(q + mixin);
return { q: q + "&w_rid=" + w_rid, w_rid: w_rid };
}
async function getMyMid() {
const data = await apiJSON("https://api.bilibili.com/x/web-interface/nav");
const mid = data && data.data && data.data.mid;
return Number(mid) || 0;
}
// 已发过私信的 mid(永久去重:B站 21047 限制同一陌生人只能发 1 条)
async function dmSentSet() {
try {
const c = await chrome.storage.local.get("dmSentMids");
const arr = c.dmSentMids;
return new Set(Array.isArray(arr) ? arr : []);
} catch (e) { return new Set(); }
}
async function dmMarkSent(mid) {
try {
const c = await chrome.storage.local.get("dmSentMids");
const arr = Array.isArray(c.dmSentMids) ? c.dmSentMids : [];
const s = String(mid);
if (!arr.includes(s)) { arr.push(s); if (arr.length > 5000) arr.splice(0, arr.length - 5000); }
await chrome.storage.local.set({ dmSentMids: arr });
} catch (e) { /* 忽略 */ }
}
// 取视频评论区用户 mid(去重、排除自己、排除已发过)
async function getDmTargets(aid, limit, sentSet, myMid) {
let mids = [];
try {
const data = await apiJSON("https://api.bilibili.com/x/v2/reply/main?oid=" + aid + "&type=1&ps=20&mode=3");
const replies = (data && data.data && (data.data.replies || [])) || [];
const hot = (data && data.data && (data.data.top_replies || [])) || [];
const all = hot.concat(replies);
for (const r of all) {
const mid = Number(r && r.member && r.member.mid);
if (!mid || mid === myMid) continue;
const name = String((r.member && r.member.uname) || "");
if (sentSet.has(String(mid))) continue;
if (!mids.some(x => x.mid === mid)) mids.push({ mid: mid, name: name });
if (mids.length >= limit) break;
}
} catch (e) { /* 接口不可用则返回空 */ }
return mids;
}
const DM_CODE_HINT = {
"-101": "账号未登录或登录已过期",
"-400": "请求错误(参数或签名问题)",
"21015": "需要绑定手机号才能发私信",
"21020": "发私信频率过快,已停止",
"21026": "不能给自己发私信",
"21046": "发私信频率过高,24小时后再发(已停止)",
"21047": "对方未回复/未关注你前,最多只能发 1 条(该用户已跳过)",
"25003": "对方隐私设置,无法发送",
"25005": "你已拉黑对方"
};
async function sendDm(mid, text) {
const csrf = getCookie("bili_jct");
if (!csrf) return { code: -999, message: "未读取到 bili_jct(登录态)" };
const myMid = await getMyMid();
if (!myMid) return { code: -998, message: "拿不到自己的 mid(未登录)" };
const ts = Math.floor(Date.now() / 1000);
const devId = (typeof crypto !== "undefined" && crypto.randomUUID)
? crypto.randomUUID()
: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c === "x" ? r : (r & 0x3 | 0x8); return v.toString(16);
});
const params = {
"msg[sender_uid]": String(myMid),
"msg[receiver_id]": String(mid),
"msg[receiver_type]": "1",
"msg[msg_type]": "1",
"msg[msg_status]": "0",
"msg[dev_id]": devId,
"msg[timestamp]": String(ts),
"msg[new_face_version]": "1",
"msg[content]": JSON.stringify({ content: text }),
"csrf_token": csrf,
"csrf": csrf,
"mobi_app": "web",
"build": "0"
};
const signed = await signParams(params);
const body = new URLSearchParams(signed.q.split("&").reduce((acc, kv) => {
const i = kv.indexOf("="); acc[kv.slice(0, i)] = decodeURIComponent(kv.slice(i + 1)); return acc;
}, {})).toString();
const data = await apiJSON("https://api.vc.bilibili.com/web_im/v1/web_im/send_msg?" + signed.q, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Referer": "https://message.bilibili.com/",
"Origin": "https://message.bilibili.com"
},
body: body
});
const code = data && data.code;
return { code: code, message: DM_CODE_HINT[String(code)] || (data && data.message) || "" };
}
// ============ 关键词搜索 ============
async function searchVideos(keyword, order, page, tid) {
@@ -686,6 +874,55 @@
}
}
// ============ 私信执行步:对当前视频的评论区用户发送(独立频率、永久按 mid 去重) ============
// B站风控:21047 同一陌生人未回复/未关注前最多只能发 1 条;21046 频繁发会被限 24 小时
// 所以默认间隔 2~4 分钟、每视频最多 3 人、同一 mid 永不重复
const DM_GAP_MIN = 2 * 60 * 1000, DM_GAP_MAX = 4 * 60 * 1000;
async function runDmStep(task, video) {
const cfg = task.config;
const limit = Math.max(1, Math.min(5, Number(cfg.dmCount) || 1));
try {
step("准备私信:抓取评论区用户…");
const myMid = await getMyMid();
if (!myMid) { step("⚠️ 未登录,跳过私信", "fail"); return; }
const sentSet = await dmSentSet();
const targets = await getDmTargets(video.aid, limit, sentSet, myMid);
if (!targets.length) { step("本视频无可私信对象(都已发过或无评论)"); return; }
step("评论区可私信 " + targets.length + " 人");
for (let i = 0; i < targets.length; i++) {
if (stopRequested()) { step("已手动停止,私信中断", "fail"); return; }
const t = targets[i];
const text = video.dmText || pickTemplate(video.title, cfg.keyword);
const v = verifyMessage(text);
if (!v.ok) { step("❌ 私信内容不合规:" + v.reason, "fail"); return; }
step("私信 @" + t.name + "(" + (i + 1) + "/" + targets.length + "):" + text.slice(0, 24) + (text.length > 24 ? "…" : ""));
const res = await sendDm(t.mid, text);
if (res.code === 0) {
task.dmSent = (task.dmSent || 0) + 1;
await dmMarkSent(t.mid);
relay({ type: "dmDone", mid: t.mid, name: t.name });
step("✅ 私信已发送:@" + t.name, "ok");
} else {
step("❌ 私信失败 @" + t.name + ":" + res.message, "fail");
// 频率类致命错误 → 直接停,避免把号搞废
if (["21020", "21046", "-101", "-999"].indexOf(String(res.code)) >= 0) {
step("触发私信风控,已停止后续私信", "fail");
return;
}
}
if (i < targets.length - 1) {
const g = DM_GAP_MIN + Math.random() * (DM_GAP_MAX - DM_GAP_MIN);
step("等待 " + Math.round(g / 1000) + " 秒后发下一条私信…");
await interruptibleWait(g);
}
}
} catch (e) {
step("私信出错:" + (e && e.message || e), "fail");
}
}
// 视频页内处理一条 → 等待 → 跳下一个(导航后由新页面的脚本续跑)
async function processVideo(task) {
running = true;
@@ -713,6 +950,11 @@
await recordOne(task, video, text, false);
}
// 私信(可选,评论后对本视频评论区用户发送;风控更严,间隔更长)
if (cfg.dmEnabled && !stopRequested()) {
await runDmStep(task, video);
}
task.index += 1;
if (task.index < task.queue.length && !stopRequested()) {
@@ -726,7 +968,7 @@
location.href = next.url;
return; // 本页生命周期结束
}
finish(task, true, "执行完成:成功 " + (task.sent || 0) + " / 失败 " + (task.failed || 0));
finish(task, true, "执行完成:评论成功 " + (task.sent || 0) + " / 失败 " + (task.failed || 0) + (task.dmSent ? ",私信 " + task.dmSent + " 条" : ""));
} catch (e) {
finish(task, false, "执行出错:" + (e && e.message || e));
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "B站评论助手",
"version": "1.6.6",
"version": "1.7.0",
"description": "B站网页版关键词评论助手:按搜索关键词找视频,默认模拟真人(打开视频页、滚到评论区、逐字输入、点发布),也可快速API直发;支持下载当前页视频;带防重复记录、可中断执行与页面实时进度浮层。",
"permissions": ["storage", "tabs", "scripting", "sidePanel", "downloads", "declarativeNetRequest"],
"host_permissions": [
+24
View File
@@ -54,6 +54,7 @@
<a id="licBuy" style="color:#e91e63;font-weight:bold;text-decoration:underline;cursor:pointer;white-space:nowrap">购买授权 →</a>
</div>
<div id="licSub" style="font-size:11px;color:#888;margin-top:3px"></div>
<div id="updMsg" style="display:none;font-size:12px;color:#005f8b;background:#e6f6fd;border:1px solid #9ed8f0;border-radius:4px;padding:4px 8px;margin-top:6px"></div>
<div id="licNotice" style="display:none;font-size:12px;color:#7a5c00;background:#fff8e1;border:1px solid #ffe082;border-radius:4px;padding:4px 8px;margin-top:6px;line-height:1.5"></div>
<div id="licActRow" style="display:none;margin-top:6px;align-items:center;gap:6px">
<input type="text" id="cardKey" placeholder="已有卡密?粘贴到这里" style="flex:1">
@@ -236,6 +237,29 @@
<input type="number" id="minPlay" value="0" min="0">
<span class="unit">次(0 = 不过滤)</span>
</div>
<div id="dmBox" style="margin-top:10px;padding:9px 10px;border:1px dashed #d0a0a0;border-radius:8px;background:#fdf7f7">
<label style="display:flex;align-items:center;gap:6px;font-size:13px;font-weight:bold;color:#c0392b;cursor:pointer">
<input type="checkbox" id="dmEnabled"> ✉ 私信评论区用户(高风险,默认关闭)
</label>
<div id="dmDetail" style="display:none;margin-top:7px">
<div class="row">
<span class="unit">每个视频最多</span>
<select id="dmCount">
<option value="1">1 人</option>
<option value="2" selected>2 人</option>
<option value="3">3 人</option>
<option value="5">5 人</option>
</select>
<span class="unit">条私信 · 间隔 2~4 分钟</span>
</div>
<textarea id="dmText" rows="3" placeholder="私信内容(选填):留空则复用上方评论内容。建议口语化、不带链接、不群发广告感" style="width:100%;min-height:64px;margin-top:5px;padding:7px 9px;border:1px solid #ccc;border-radius:4px;font-size:13px;line-height:1.6;resize:vertical;box-sizing:border-box"></textarea>
<div style="font-size:11px;color:#c0392b;line-height:1.6;margin-top:5px">
⚠ B站限制:对方未回复/未关注你前<b>最多只能发 1 条</b>;频繁发送会被限流 24 小时甚至禁言。<br>
插件已按此设计:同一用户永不重复、间隔 2~4 分钟、每日建议不超过 10 条。<b>使用风险自负。</b>
</div>
</div>
</div>
<button id="btnStart" class="primary">▶ 开始执行</button>
<div class="row" style="display:none" id="stopRow">
<button id="btnStopRun">⏹ 停止</button>
+35 -6
View File
@@ -13,9 +13,12 @@
region: "",
dlQuality: "80",
maxN: 3,
minPlay: 0
minPlay: 0,
dmEnabled: false,
dmCount: 2,
dmText: ""
};
const FIELDS = ["baseUrl", "apiKey", "model", "keyword", "order", "mode", "customText", "sendMode", "tid", "region", "dlQuality", "maxN", "minPlay"];
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;
@@ -50,8 +53,10 @@
for (const k of FIELDS) {
const el = $(k);
if (!el) continue;
el.value = settings[k];
if (el.type === "checkbox") el.checked = (settings[k] === true || settings[k] === "true");
else el.value = settings[k];
}
refreshDmVisibility();
// 地区不记忆:每次打开默认「全部地区」,只在本次会话内有效
settings.region = "";
const reg = $("region");
@@ -67,7 +72,13 @@
await chrome.storage.local.set({ settings: toSave });
}
function refreshCustomVisibility() {
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";
}
@@ -184,6 +195,15 @@
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) {
@@ -315,7 +335,14 @@
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 }) });
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) };
}
@@ -552,9 +579,11 @@
const el = $(k);
if (!el) continue;
el.addEventListener("change", () => {
const v = (el.type === "number") ? (Number(el.value) || 0) : el.value;
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") {