// ==UserScript== // @name 哔哩发评发弹幕反诈 // @version beta-20260227-1 // @description 检测b站发评论发弹幕后的ShadowBan(仅自己可见)状态。 // @author naaammme // @match *://*.bilibili.com/* // @run-at document-start // @noframes // @license GPL // @grant GM_registerMenuCommand // @namespace https://greasyfork.org/users/1508061 // ==/UserScript== (function () { "use strict"; const LOG_ENABLED = false; const logger = { log: (...args) => LOG_ENABLED && logger.log(...args), warn: (...args) => LOG_ENABLED && logger.warn(...args), error: (...args) => LOG_ENABLED && logger.error(...args), }; class WbiSign { constructor() { this.mixinKeyEncTab = [ 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, ]; } getMixinKey = (orig) => this.mixinKeyEncTab.map((n) => orig[n]).join("").slice(0, 32); encWbi(params, img_key, sub_key) { const mixin_key = this.getMixinKey(img_key + sub_key), curr_time = Math.round(Date.now() / 1000), chr_filter = /[!'()*]/g; Object.assign(params, { wts: curr_time }); const query = Object.keys(params) .sort() .map((key) => { const value = params[key].toString().replace(chr_filter, ""); return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; }) .join("&"); const wbi_sign = SparkMD5.hash(query + mixin_key); return query + "&w_rid=" + wbi_sign; } } const SparkMD5 = (function () { "use strict"; var add32 = function (a, b) { return (a + b) & 0xffffffff; }, hex_chr = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"]; function md5cycle(x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); } function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - 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 md51(s) { var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= n; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); var tail = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); tail[i >> 2] |= 0x80 << ((i % 4) << 3); if (i > 55) { md5cycle(state, tail); tail = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; } tail[14] = n * 8; md5cycle(state, tail); return state; } function md5blk(s) { var md5blks = [], i; for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i+1) << 8) + (s.charCodeAt(i+2) << 16) + (s.charCodeAt(i+3) << 24); } return md5blks; } function rhex(n) { var s = ""; for (var j = 0; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; return s; } function hex(x) { for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(""); } return { hash: function (str) { return hex(md51(str)); } }; })(); const web_key_urls = { img_key_url: localStorage.getItem("wbi_img_url") || "", sub_key_url: localStorage.getItem("wbi_sub_url") || "", }; const getWebKey = function (str) { if (!str) return ""; return str.slice(str.lastIndexOf("/") + 1, str.lastIndexOf(".")); }; let img_key = getWebKey(web_key_urls.img_key_url); let sub_key = getWebKey(web_key_urls.sub_key_url); async function ensureWbiKeys() { if (img_key && sub_key) return; try { const resp = await originalFetch("https://api.bilibili.com/x/web-interface/nav", { credentials: 'include' }); const data = await resp.json(); if (data.data && data.data.wbi_img) { const imgUrl = data.data.wbi_img.img_url || ""; const subUrl = data.data.wbi_img.sub_url || ""; img_key = getWebKey(imgUrl); sub_key = getWebKey(subUrl); localStorage.setItem("wbi_img_url", imgUrl); localStorage.setItem("wbi_sub_url", subUrl); } } catch (e) { logger.error("获取Wbi密钥失败", e); } } const originalFetch = window.fetch; const waitTime = 5000; const danmakuWaitTime = 90000; const sortByTime = 0; const SORT_MODE_TIME = 2; function sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); } logger.log("[发评反诈] 脚本已加载"); const TAB_ID = sessionStorage.getItem('biliAntifraud_tabId') || (() => { const id = 'tab_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8); sessionStorage.setItem('biliAntifraud_tabId', id); return id; })(); const STORAGE_KEY = 'biliAntifraud_taskQueue'; const MAX_TASKS = 200; const LOCK_TIMEOUT = 120000; function makeTaskId() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); } const TaskQueue = { _read() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch { return []; } }, _write(tasks) { if (tasks.length > MAX_TASKS) { const done = tasks.filter(t => t.status === 'done' || t.status === 'error') .sort((a, b) => (a.completedAt || 0) - (b.completedAt || 0)); const removeIds = new Set(done.slice(0, tasks.length - MAX_TASKS).map(t => t.id)); tasks = tasks.filter(t => !removeIds.has(t.id)); } localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); }, add(task) { const tasks = this._read(); tasks.unshift(task); this._write(tasks); return task; }, getAll() { return this._read(); }, update(taskId, updates) { const tasks = this._read(); const t = tasks.find(t => t.id === taskId); if (!t) return null; Object.assign(t, updates); this._write(tasks); return t; }, getPendingTasks() { const now = Date.now(); return this._read().filter(t => (t.status === 'waiting' && now >= t.checkAfter) || (t.status === 'checking' && t.lockedAt && (now - t.lockedAt > LOCK_TIMEOUT)) ); }, tryLock(taskId, tabId) { const tasks = this._read(); const t = tasks.find(t => t.id === taskId); if (!t) return false; const now = Date.now(); if (t.status === 'checking' && t.lockedBy && (now - t.lockedAt < LOCK_TIMEOUT)) return false; t.status = 'checking'; t.lockedBy = tabId; t.lockedAt = now; t.startedAt = now; this._write(tasks); return true; }, remove(taskId) { this._write(this._read().filter(t => t.id !== taskId)); } }; async function getMainCommentList(oid, type, next, mode, isLogin, seek_rpid) { const pagination = (!next || next === "" || next === 0) ? '{"offset":""}' : `{"offset":"${next}"}`; const params = { oid, type, mode, plat: 1, web_location: 1315875, pagination_str: pagination, seek_rpid: seek_rpid || "" }; const query = new WbiSign().encWbi(params, img_key, sub_key); let url = `https://api.bilibili.com/x/v2/reply/wbi/main?${query}`; try { const response = await originalFetch(url, isLogin ? { credentials: 'include' } : { credentials: 'omit' }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { return { code: -1, message: error.message }; } } async function fetchBilibiliCommentReplies(oid, type, root, pn, sort, hasCookie) { const params = { oid, type, root, pn, sort }; const query = new WbiSign().encWbi(params, img_key, sub_key); const url = `https://api.bilibili.com/x/v2/reply/reply?${query}`; try { const response = await originalFetch(url, hasCookie ? { credentials: 'include' } : { credentials: 'omit' }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { return { code: -1, message: error.message }; } } async function fetchDynamic(id, hasCookie) { const url = new URL('https://api.bilibili.com/x/polymer/web-dynamic/v1/detail'); const params = { id }; url.search = new URLSearchParams(params).toString(); try { const response = await originalFetch(url.toString(), hasCookie ? { credentials: 'include' } : { credentials: 'omit' }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { return { code: -1, message: error.message }; } } function findReplies(replies, rpid) { if (!replies) return null; for (const item of replies) { if (String(item.rpid) === String(rpid)) return item; } return null; } async function findReplyUsingSeekRpid(oid, type, rpid, root, isLogin) { var resp = await getMainCommentList(oid, type, "", SORT_MODE_TIME, isLogin, rpid); if (resp.code != 0) return null; var replies = resp.data.replies || []; if (resp.data.top_replies) replies = replies.concat(resp.data.top_replies); for (var comment of replies) { if (comment.rpid == root) { if (comment.replies) { var found = findReplies(comment.replies, rpid); if (found) return found; } } if (comment.rpid == rpid) return comment; if (comment.replies) { var found = findReplies(comment.replies, rpid); if (found) return found; } } return null; } async function findDanmakuInSegment(oid, pid, segmentIndex, content, hasCookie, referrerUrl) { var params; if (segmentIndex === 1) { params = { type: 1, oid, pid, segment_index: segmentIndex, pull_mode: 1, ps: 0, pe: 120000, web_location: 1315873 }; } else { params = { type: 1, oid, pid, segment_index: segmentIndex, web_location: 1315873 }; } var query = new WbiSign().encWbi(params, img_key, sub_key); var url = `https://api.bilibili.com/x/v2/dm/wbi/web/seg.so?${query}`; var resp = await originalFetch(url, { method: 'GET', headers: { "accept": "application/json, text/plain, */*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache" }, referrer: referrerUrl || window.location.href, mode: 'cors', credentials: hasCookie ? 'include' : 'omit' }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); var buffer = await resp.arrayBuffer(); if (buffer.byteLength === 0) return false; var utf8Text = new TextDecoder('utf-8').decode(buffer); if (utf8Text.includes(content)) return true; var bytes = new Uint8Array(buffer); var textContent = extractTextsFromProtobuf(bytes); for (var dm of textContent) { if (dm === content) return true; } return false; } function extractTextsFromProtobuf(bytes) { var texts = [], pos = 0; while (pos < bytes.length) { var tagResult = readVarint(bytes, pos); if (tagResult === null) break; var tag = tagResult.value; pos = tagResult.pos; var fieldNumber = tag >>> 3, wireType = tag & 0x7; if (wireType === 2) { var lenResult = readVarint(bytes, pos); if (lenResult === null) break; var len = lenResult.value; pos = lenResult.pos; if (pos + len > bytes.length) break; if (fieldNumber === 1) { var dmContent = parseDanmakuElem(bytes, pos, pos + len); if (dmContent !== null) texts.push(dmContent); } pos += len; } else if (wireType === 0) { var vResult = readVarint(bytes, pos); if (vResult === null) break; pos = vResult.pos; } else if (wireType === 5) { pos += 4; } else if (wireType === 1) { pos += 8; } else break; } return texts; } function parseDanmakuElem(bytes, start, end) { var pos = start; while (pos < end) { var tagResult = readVarint(bytes, pos); if (tagResult === null) break; var tag = tagResult.value; pos = tagResult.pos; var fieldNumber = tag >>> 3, wireType = tag & 0x7; if (wireType === 2) { var lenResult = readVarint(bytes, pos); if (lenResult === null) break; var len = lenResult.value; pos = lenResult.pos; if (pos + len > end) break; if (fieldNumber === 7) return new TextDecoder('utf-8').decode(bytes.slice(pos, pos + len)); pos += len; } else if (wireType === 0) { var vResult = readVarint(bytes, pos); if (vResult === null) break; pos = vResult.pos; } else if (wireType === 5) { pos += 4; } else if (wireType === 1) { pos += 8; } else break; } return null; } function readVarint(bytes, pos) { var result = 0, shift = 0; while (pos < bytes.length) { var b = bytes[pos++]; result |= (b & 0x7F) << shift; if ((b & 0x80) === 0) return { value: result >>> 0, pos }; shift += 7; if (shift >= 35) return null; } return null; } const Detection = { async checkComment(ctx) { const { oid, type, rpid, root, ctime, message } = ctx; if (root == 0 || root === "0") { let found = null, nextOffset = ""; for (let page = 0; page < 30; page++) { var resp = await getMainCommentList(oid, type, nextOffset, SORT_MODE_TIME, false); if (resp.code != 0) return { verdict: 'error', message: `获取评论列表失败 code=${resp.code} msg=${resp.message}` }; if (page === 0 && resp.data.top_replies) { found = findReplies(resp.data.top_replies, rpid); if (found) break; } var replies = resp.data.replies; if (!replies || replies.length === 0) break; found = findReplies(replies, rpid); if (found) break; if (replies[replies.length - 1].ctime < ctime) break; var pagination = resp.data.cursor?.pagination_reply; if (pagination?.next_offset) nextOffset = pagination.next_offset; else break; } if (found) { if (found.invisible) return { verdict: 'invisible', message: `评论被标记为invisible(前端隐藏):「${message}」` }; return { verdict: 'ok', message: `评论正常显示:「${message}」` }; } var resp2 = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, true); if (resp2.code == 12022) return { verdict: 'deleted', message: `评论被系统秒删:「${message}」` }; if (resp2.code == 0) { var resp3 = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, false); if (resp3.code == 12022) return { verdict: 'shadowban', message: `评论被ShadowBan(仅自己可见):「${message}」` }; if (resp3.code == 0) { if (resp3.data?.root?.invisible) return { verdict: 'invisible', message: `评论invisible:「${message}」` }; return { verdict: 'suspicious', message: `评论疑似审核中或评论区被戒严:「${message}」` }; } return { verdict: 'error', message: `无账号回复列表异常 code=${resp3.code}` }; } return { verdict: 'error', message: `有账号回复列表异常 code=${resp2.code}` }; } else { var foundNoLogin = await findReplyUsingSeekRpid(oid, type, rpid, root, false); if (foundNoLogin) return { verdict: 'ok', message: `回复评论正常显示:「${message}」` }; var foundLogin = await findReplyUsingSeekRpid(oid, type, rpid, root, true); if (foundLogin) return { verdict: 'shadowban', message: `回复评论被ShadowBan:「${message}」` }; return { verdict: 'deleted', message: `回复评论被系统秒删:「${message}」` }; } }, async checkDanmaku(ctx) { const { text, cid, aid, segmentIndex, referrerUrl } = ctx; var foundNoLogin = await findDanmakuInSegment(cid, aid, segmentIndex, text, false, referrerUrl); if (foundNoLogin) return { verdict: 'ok', message: `弹幕正常显示:「${text}」` }; var foundLogin = await findDanmakuInSegment(cid, aid, segmentIndex, text, true, referrerUrl); if (foundLogin) return { verdict: 'shadowban', message: `弹幕被ShadowBan(仅自己可见):「${text}」` }; return { verdict: 'deleted', message: `弹幕被系统删除:「${text}」` }; }, async checkDynamic(ctx) { var resp = await fetchDynamic(ctx.dynamicId, false); if (resp.code == -352) return { verdict: 'shadowban', message: '无账号获取此动态返回-352(风控拦截),可能动态被ShadowBan、请求IP异常或频率过高' }; if (resp.code == 4101131) return { verdict: 'shadowban', message: '无账号获取此动态返回4101131,此动态可能被ShadowBan' }; if (resp.code == 500) return { verdict: 'suspicious', message: '无账号获取此动态返回500,此动态可能被ShadowBan或审核中' }; if (resp.code == 0) return { verdict: 'ok', message: '此动态正常,没被ShadowBan' }; return { verdict: 'error', message: `动态检测返回未知code=${resp.code}` }; } }; const POLL_INTERVAL = 3000; const Coordinator = { _timer: null, _running: false, start() { if (this._timer) return; const jitter = Math.random() * 1000; this._timer = setInterval(() => this.tick(), POLL_INTERVAL + jitter); window.addEventListener('storage', (e) => { if (e.key === STORAGE_KEY) ManagementUI.refresh(); }); }, async tick() { if (this._running) return; this._running = true; try { const pending = TaskQueue.getPendingTasks(); for (const task of pending) { if (TaskQueue.tryLock(task.id, TAB_ID)) { await this.executeTask(task); } } } catch (e) { logger.error('[调度器]tick出错', e); } this._running = false; }, async executeTask(task) { try { await ensureWbiKeys(); let result; if (task.type === 'comment') result = await Detection.checkComment(task.context); else if (task.type === 'danmaku') result = await Detection.checkDanmaku(task.context); else if (task.type === 'dynamic') result = await Detection.checkDynamic(task.context); TaskQueue.update(task.id, { status: 'done', result, completedAt: Date.now() }); Notification.show(task, result); if (task.type === 'dynamic' && result.verdict !== 'ok') { const currentId = this._getCurrentDynamicId(); if (currentId && currentId === task.context.dynamicId) { addDynamicShadowBannedHint(result.message); } } } catch (e) { TaskQueue.update(task.id, { status: 'error', result: { verdict: 'error', message: e.message }, completedAt: Date.now() }); Notification.show(task, { verdict: 'error', message: '检测出错:' + e.message }); } ManagementUI.refresh(); }, _getCurrentDynamicId() { const hostname = window.location.hostname; if (hostname === 't.bilibili.com') return window.location.pathname.split('/')[1]; if (hostname === 'www.bilibili.com') { const parts = window.location.pathname.split('/'); if (parts[1] === 'opus') return parts[2]; } return null; } }; const Notification = { _container: null, _ensureContainer() { if (this._container) return; const c = document.createElement('div'); c.id = 'antifraud-toast-container'; c.style.cssText = 'position:fixed;top:20px;right:20px;z-index:10000;display:flex;flex-direction:column;gap:8px;pointer-events:none;max-width:380px;'; document.body.appendChild(c); this._container = c; }, show(task, result) { this._ensureContainer(); const colors = { ok:'#4393E2', shadowban:'#FB7299', deleted:'#FB7299', invisible:'#E5A84B', suspicious:'#E5A84B', error:'#FB7299', info:'#999' }; const typeLabels = { comment:'评论', danmaku:'弹幕', dynamic:'动态' }; const color = colors[result.verdict] || '#999'; const toast = document.createElement('div'); toast.style.cssText = `background:#fff;color:#333;padding:12px 16px;border-radius:4px;border-left:4px solid ${color};box-shadow:0 2px 8px rgba(0,0,0,0.15);pointer-events:auto;font-size:13px;line-height:1.5;cursor:pointer;opacity:0;transform:translateX(100%);transition:all 0.3s ease;`; const title = result.verdict === 'info' ? '已加入检测队列' : `${typeLabels[task.type] || ''}检测完成`; toast.innerHTML = `
暂无检测记录
'; return; } container.innerHTML = tasks.map(t => this._renderItem(t)).join(''); container.querySelectorAll('[data-recheck]').forEach(btn => { btn.addEventListener('click', (e) => this._recheck(e.target.getAttribute('data-recheck'))); }); container.querySelectorAll('[data-delete]').forEach(btn => { btn.addEventListener('click', (e) => { TaskQueue.remove(e.target.getAttribute('data-delete')); this.refresh(); }); }); }, _renderItem(task) { const stCfg = { waiting: { l:'等待中', c:'#999' }, checking: { l:'检查中', c:'#FB7299' }, done: { l:'已完成', c:'#6ba8e6' }, error: { l:'出错', c:'#FB7299' } }; const vCfg = { ok: { l:'正常', c:'#6ba8e6' }, shadowban: { l:'ShadowBan', c:'#FB7299' }, deleted: { l:'已删除', c:'#FB7299' }, invisible: { l:'不可见', c:'#E5A84B' }, suspicious: { l:'疑似审核', c:'#E5A84B' }, error: { l:'错误', c:'#FB7299' } }; const typeLabels = { comment:'评论', danmaku:'弹幕', dynamic:'动态' }; const st = stCfg[task.status] || stCfg.waiting; const content = task.context.message || task.context.text || task.context.dynamicId || ''; const truncated = content.length > 60 ? content.substring(0, 60) + '...' : content; const timeStr = new Date(task.createdAt).toLocaleString(); let countdown = ''; if (task.status === 'waiting') { const secs = Math.max(0, Math.ceil((task.checkAfter - Date.now()) / 1000)); countdown = ` (${secs}s)`; } let verdict = ''; if (task.status === 'done' && task.result) { const vc = vCfg[task.result.verdict] || vCfg.error; verdict = `${vc.l}`; } let resultMsg = ''; if (task.result?.message) { resultMsg = `${this._esc(task.result.message)}
`; } const canRecheck = task.status === 'done' || task.status === 'error'; const actions = `${this._esc(truncated)}
${timeStr}
${resultMsg}${actions}