v1.7.0: 新增私信评论区用户(wbi签名+永久按mid去重+2~4分钟间隔)、安装统计上报、有新版本提示
This commit is contained in:
1 parent
38b90c1448
commit
0cce6cc2bb
5 files changed
+344
-9
No files matched your search
+41
-1
@@ -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
@@ -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
@@ -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": [
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
Reference in new issue
Block a user