// ==UserScript== // @name 容器插件 // @namespace 可乐 // @version 5.1 // @description 可乐 // @author 可乐 // @match *://manager.bilibili.co/annotation/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== (function () { 'use strict'; // ==================== 常量定义 ==================== /** CSS选择器集中管理,避免字符串重复 */ var SELECTORS = { taskInfo: ".task-info", textItem: ".text-item", matchResult: "heis_ss", label: ".label", radio: ".el-radio", checkboxLabel: ".el-checkbox__label", submitBtn: ".el-button.operation-btn.submit-operation-btn.el-button--primary.el-button--medium", topBtn: ".el-button.top-btn.operation-btn.el-button--default.el-button--medium", skipBtn: ".el-button.skip-btn.operation-btn.el-button--default.el-button--medium" }; /** 规则→复选框索引映射表(含全部8种规则) */ var RULE_CHECKBOX_MAP = { "见光死": 0, "主体拦截": 1, "封面拦截": 2, "通过需监控": 3, "通过需进标": 4, "正常通过": 5, "跳过": 6, "根据图片自行判断": 7 }; /** 可选规则列表 */ var RULE_OPTIONS = [ "见光死", "主体拦截", "封面拦截", "通过需监控", "通过需进标", "正常通过", "跳过", "根据图片自行判断" ]; /** 规则标签颜色映射 */ var RULE_TAG_CLASS_MAP = { "见光死": "gc-tag-red", "主体拦截": "gc-tag-orange", "封面拦截": "gc-tag-blue", "通过需监控": "gc-tag-cyan", "通过需进标": "gc-tag-green", "正常通过": "gc-tag-green", "跳过": "gc-tag-gray", "根据图片自行判断": "gc-tag-gray" }; // ==================== 快捷键配置 ==================== /** 默认快捷键: { action: keyCode } */ var DEFAULT_SHORTCUTS = { prioritySubmit: 49, // 数字1 → 优先+提交 rejectSubmit: 50, // 数字2 → 不通过+提交 submitOnly: 13 // Enter → 仅提交 }; /** 快捷键动作显示名称 */ var SHORTCUT_LABELS = { prioritySubmit: "命中+提交", rejectSubmit: "未命中+提交", submitOnly: "仅提交" }; /** keyCode → 可读名称映射 */ function keyNameFromCode(code) { if (code >= 48 && code <= 57) return String.fromCharCode(code); // 0-9 if (code >= 65 && code <= 90) return String.fromCharCode(code); // A-Z if (code === 13) return "Enter"; if (code === 32) return "Space"; if (code === 27) return "Esc"; if (code === 9) return "Tab"; if (code === 8) return "Backspace"; if (code === 37) return "←"; if (code === 38) return "↑"; if (code === 39) return "→"; if (code === 40) return "↓"; if (code === 112) return "F1"; if (code === 113) return "F2"; if (code === 114) return "F3"; if (code === 115) return "F4"; if (code === 116) return "F5"; if (code === 117) return "F6"; if (code === 118) return "F7"; if (code === 119) return "F8"; if (code === 120) return "F9"; if (code === 121) return "F10"; if (code === 122) return "F11"; if (code === 123) return "F12"; if (code === 187) return "="; if (code === 189) return "-"; if (code === 219) return "["; if (code === 221) return "]"; if (code === 186) return ";"; if (code === 222) return "'"; if (code === 188) return ","; if (code === 190) return "."; if (code === 191) return "/"; return "Code:" + code; } /** 快捷键管理器 */ var ShortcutManager = { _cache: null, load: function () { if (this._cache) return this._cache; var saved = GM_getValue("shortcuts_custom", null); if (saved && typeof saved === "object") { this._cache = {}; for (var k in DEFAULT_SHORTCUTS) { this._cache[k] = saved[k] !== undefined ? saved[k] : DEFAULT_SHORTCUTS[k]; } } else { this._cache = {}; for (var d in DEFAULT_SHORTCUTS) { this._cache[d] = DEFAULT_SHORTCUTS[d]; } } return this._cache; }, save: function (shortcuts) { this._cache = shortcuts; GM_setValue("shortcuts_custom", shortcuts); }, reset: function () { GM_deleteValue("shortcuts_custom"); this._cache = null; return this.load(); }, get: function (action) { return this.load()[action]; } }; // ==================== 默认词库 ==================== var DEFAULT_GLOSSARY = "果然很豆 见光死|黄之锋 主体拦截|房辉峰 主体拦截,封面拦截|胡乃文 见光死|黄明志 见光死|孙力军 主体拦截,封面拦截|Vanessa 正常通过|郑圣哲 见光死|杨晶 主体拦截,封面拦截|郭伯雄 主体拦截,封面拦截|孙政才 主体拦截,封面拦截|周永康 见光死|薄熙来 见光死|陈良宇 主体拦截,封面拦截|房峰辉 主体拦截,封面拦截|令计划 见光死|王立军 主体拦截,封面拦截|徐才厚 见光死|傅政华 主体拦截,封面拦截|席明海 主体拦截|林昶佐 见光死|大支 主体拦截|多里坤·艾沙 主体拦截,封面拦截|热比娅 主体拦截|李卓人 主体拦截|黄耀明 主体拦截,封面拦截|何韵诗 主体拦截|罗冠聪 主体拦截|陈秋实 见光死|洛桑桑洁 见光死|达赖 见光死|陈光诚 主体拦截|伊内斯·坎特 主体拦截|李洪志 见光死|江峰 主体拦截|迈克陈 主体拦截,封面拦截|文睿 见光死|石涛 见光死|张华 主体拦截|果果和豆豆 主体拦截|刘晓波 见光死|柴玲 见光死|刘霞 见光死|王丹 见光死|吾尔开希 见光死|郝海东 见光死|郭文贵 见光死|释净空 见光死|cici|li 见光死|叶钊颖 见光死|徐晓冬 见光死|姜光宇 主体拦截|文昭 见光死|周庭 主体拦截|郭竞雄 见光死|彭波 主体拦截,封面拦截|苏荣 主体拦截,封面拦截|赵昕 见光死|陈破空 见光死|刘新云 主体拦截,封面拦截|刘仲敬 主体拦截|池子 主体拦截|cicili 见光死|达赖young 见光死|傅振华 主体拦截,封面拦截|王越池 主体拦截|徐晓东 见光死|梁振英 通过需监控|李小鹏 通过需监控|胡海峰 通过需监控|李小琳 通过需监控|董建华 通过需监控|彭帅 通过需监控|杨洁篪 根据图片自行判断|林郑月娥 通过需监控|华春莹 通过需监控|汪文斌 根据图片自行判断|佩洛西 通过需监控|乔·拜登 正常通过|弗拉基米尔·泽连斯基 通过需监控|张澜澜 通过需监控|马元 通过需监控|李书福 通过需监控|侯德健 通过需监控|崔健 通过需监控|老八 根据图片自行判断|佟丽娅 通过需监控|孟建柱 主体拦截,封面拦截|毛宁 根据图片自行判断|赵立坚 根据图片自行判断|李佳琦 通过需监控|南希佩洛西 通过需监控|戈尔巴乔夫 通过需监控|胡锡进 通过需监控,通过需进标|蔡英文 通过需监控|马英九 通过需监控|金正恩 通过需监控|金日成 通过需监控|金正日 通过需监控|金与正 通过需监控|列宁 通过需监控|弗拉基米尔·弗拉基米罗维奇·普京 通过需监控|唐纳德·特朗普 正常通过|伊丽莎白二世 通过需监控|希特勒 通过需监控|蔡霞 通过需监控|姚文元 通过需监控|江青 通过需监控|张春桥 通过需监控|邓小平mask 通过需监控|毛泽东mask 通过需监控|宋祖英 通过需监控,通过需进标|韩国瑜 正常通过|李登辉(台湾前领导人) 正常通过|赖清德 通过需监控|陈水扁 正常通过|李登辉 正常通过|安倍晋三 通过需进标|斯大林 通过需进标|陈一发儿 通过需进标|马保国 正常通过|古月 通过需监控|贺一诚 正常通过|玄松月 正常通过|付凌晖(国家统计局司长) 正常通过|朱凤莲 正常通过|曾荫权 正常通过|虎子 正常通过|刀哥 正常通过|李云迪 正常通过|虎哥 正常通过|彭清华 正常通过|付凌晖 正常通过|吴尊友 正常通过|孙业礼 正常通过|陈睿 正常通过|罗翔 正常通过|江泽民 通过需监控,通过需进标|邓小平 通过需监控|毛泽东 通过需监控|刘少奇 通过需监控|周恩来 通过需监控|吴亦凡 通过需进标|张哲瀚 通过需进标|赵薇 通过需进标|邓伦 通过需进标|郑爽 通过需进标|江泽民mask 通过需监控,通过需进标|蔡奇 主体拦截,封面拦截|刘鹤 主体拦截,封面拦截|王立科 主体拦截,封面拦截|陈吉宁 主体拦截,封面拦截|慎海雄 主体拦截,封面拦截|俞正声 主体拦截,封面拦截|毛新宇 主体拦截,封面拦截|应勇 根据图片自行判断|江绵恒 主体拦截,封面拦截|张德江 主体拦截,封面拦截|刘云山 主体拦截,封面拦截|华国锋 主体拦截,封面拦截|朱镕基 主体拦截,封面拦截|郭声琨 主体拦截,封面拦截|任志强(贪官) 主体拦截,封面拦截|秦刚(外交部发言人) 根据图片自行判断|黄坤明(中央宣传部部长) 主体拦截,封面拦截|比约克 主体拦截,封面拦截|曾庆红 主体拦截,封面拦截|许其亮 主体拦截,封面拦截|沈彤 主体拦截,封面拦截|张又侠 主体拦截,封面拦截|谢春涛 主体拦截,封面拦截|习近平 主体拦截,封面拦截|汪洋 主体拦截,封面拦截|韩正 主体拦截,封面拦截|栗战书 主体拦截,封面拦截|王沪宁 主体拦截,封面拦截|李克强 主体拦截,封面拦截|王岐山 主体拦截,封面拦截|赵乐际 主体拦截,封面拦截|李强 主体拦截,封面拦截|胡春华 主体拦截,封面拦截|陈希 主体拦截,封面拦截|丁薛祥 主体拦截,封面拦截|孙春兰 主体拦截,封面拦截|杨晓渡 主体拦截,封面拦截|胡锦涛 主体拦截,封面拦截|胡耀邦 主体拦截,封面拦截|张高丽 主体拦截,封面拦截|温家宝 主体拦截,封面拦截|习仲勋 主体拦截,封面拦截|彭丽媛 主体拦截,封面拦截|任志强 主体拦截,封面拦截|陈敏尔 主体拦截,封面拦截|徐若瑄 主体拦截,封面拦截|陈之汉 主体拦截,封面拦截|黎智英 主体拦截,封面拦截|陈方安生 主体拦截,封面拦截|杜汶泽 主体拦截,封面拦截|何俊仁 主体拦截,封面拦截|石光磊 主体拦截,封面拦截|赵紫阳 主体拦截,封面拦截|王洪文 主体拦截,封面拦截|李鹏 主体拦截,封面拦截|瓦列里·萨布林 主体拦截,封面拦截|jreg 主体拦截,封面拦截|乔瑟夫 主体拦截,封面拦截|李志 主体拦截,封面拦截|方舟子 主体拦截,封面拦截|黑手 正常通过|盛嘉奎 主体拦截,封面拦截|黄坤明 主体拦截,封面拦截|贺军科 主体拦截,封面拦截|李希 主体拦截,封面拦截|刘克清 主体拦截,封面拦截|龚正 主体拦截,封面拦截|李锐 主体拦截,封面拦截|秦刚 根据图片自行判断|徐勤根 主体拦截,封面拦截|王晨 主体拦截,封面拦截|李鸿忠 主体拦截,封面拦截|陈全国 主体拦截,封面拦截|胡锦涛mask 主体拦截,封面拦截|李锐1917 主体拦截,封面拦截|习近平mask2 主体拦截,封面拦截|王毅 通过需监控|柯文哲 正常通过|黄之峰 主体拦截|蒋万安 正常通过|赵少康 正常通过|涉政图片 跳过|吴谦 正常通过|庄志刚 封面拦截,主体拦截|萧美琴 正常通过|侯友宜 正常通过|萨沙 封面拦截,主体拦截|石国鹏 封面拦截,主体拦截|吴欣盈 正常通过|涉政图片0 跳过|莫迪 通过需监控|马克龙 通过需监控|董军 通过需监控|罗玉凤 正常通过|马朝旭 通过需监控|柴静 正常通过|刘华清 通过需进标|丁子霖 见光死|周锋锁 见光死|封从德 见光死|李尚福 见光死|魏凤和 见光死|基利安·姆巴佩 正常通过|唐仁健 主体拦截,封面拦截|王小鲁 正常通过|王小洪 通过需监控,通过需进标|彼得森 正常通过|毛泽东卡通 通过需监控|毛泽东橘子洲 通过需监控|习近平左脸 主体拦截,封面拦截|习近平卡通 主体拦截,封面拦截|习近平右脸 主体拦截,封面拦截|蓝佛安 正常通过|刘建超 正常通过|江泽民卡通 通过需监控,通过需进标|戴晴 主体拦截|界立建 见光死|习远平 主体拦截,封面拦截|涉政其他集合 跳过|聊天 正常通过|两会 主体拦截,封面拦截|发布会 主体拦截,封面拦截|天安门 通过需监控,通过需进标|人民大会堂 正常通过|新华门 正常通过|横幅 正常通过|化学实验 正常通过|中国地图 正常通过|青天白日旗 根据图片自行判断|纳粹旗 正常通过|旭日旗 正常通过|isis旗帜 见光死|雪山狮子旗 见光死|全能神旗 见光死|蒙独旗 见光死|蓝星月旗帜 见光死|法轮功旗 见光死|香港区旗 正常通过|壹电视 主体拦截,封面拦截|苹果日报 主体拦截,封面拦截|国际特赦 主体拦截,封面拦截|美国之声 主体拦截|新唐人 见光死|自由亚洲电台 主体拦截,封面拦截|枪支 正常通过|虚拟枪支 正常通过|刀 正常通过|战舰 正常通过|坦克 正常通过|中国地图 正常通过|国旗 正常通过|党徽 正常通过|国徽 正常通过|人民币 正常通过|维尼熊 通过需监控,通过需进标|其他卡通熊 正常通过|64纪念碑 见光死|二维码 正常通过|头巾 正常通过|哔咔 主体拦截,封面拦截|pornhub 主体拦截,封面拦截|坦克人 见光死|民主女神 见光死|民主纪念碑 见光死|党旗 正常通过|变形国旗 正常通过"; // ==================== 词库管理器 ==================== var GlossaryManager = { /** 从GM存储加载词库,若无则使用默认值 */ load: function () { var saved = GM_getValue("glossary_custom", ""); return (saved && saved.trim()) ? saved.trim() : DEFAULT_GLOSSARY; }, /** 将词库保存到GM存储 */ save: function (glossaryStr) { GM_setValue("glossary_custom", glossaryStr); }, /** 重置为默认词库 */ reset: function () { GM_deleteValue("glossary_custom"); return DEFAULT_GLOSSARY; }, /** * 将词库字符串解析为对象数组 * @returns {Array<{name: string, rules: string[]}>} */ parseToArray: function (glossaryStr) { if (!glossaryStr) return []; var entries = glossaryStr.split("|"); var result = []; for (var i = 0; i < entries.length; i++) { var parts = entries[i].split("\t"); if (parts.length >= 2 && parts[0].trim()) { result.push({ name: parts[0].trim(), rules: parts[1].split(",").map(function (s) { return s.trim(); }).filter(function (s) { return s; }) }); } } return result; }, /** 从对象数组还原为词库字符串 */ arrayToString: function (arr) { var parts = []; for (var i = 0; i < arr.length; i++) { if (arr[i].name && arr[i].rules.length > 0) { parts.push(arr[i].name + "\t" + arr[i].rules.join(",")); } } return parts.join("|"); }, /** 搜索词条(支持按名称和规则关键词搜索) */ search: function (glossaryStr, keyword) { keyword = (keyword || "").toLowerCase(); if (!keyword) return this.parseToArray(glossaryStr); var all = this.parseToArray(glossaryStr); return all.filter(function (item) { return item.name.toLowerCase().indexOf(keyword) > -1 || item.rules.some(function (r) { return r.toLowerCase().indexOf(keyword) > -1; }); }); } }; // ==================== 词库查找索引(Map,O(1)匹配) ==================== var Glossary1 = GlossaryManager.load(); /** 哈希索引:name → rulesStr */ var glossaryIndex = new Map(); /** 从词库字符串构建查找索引 */ function buildGlossaryIndex(glossaryStr) { glossaryIndex.clear(); if (!glossaryStr) return; var entries = glossaryStr.split("|"); for (var i = 0; i < entries.length; i++) { var tabIdx = entries[i].indexOf("\t"); if (tabIdx > -1) { var name = entries[i].substring(0, tabIdx).trim(); var rules = entries[i].substring(tabIdx + 1).trim(); if (name) glossaryIndex.set(name, rules); } } } /** 初始化索引 */ buildGlossaryIndex(Glossary1); /** 词库变更后重建索引 */ function onGlossaryChanged() { buildGlossaryIndex(Glossary1); } // ==================== 配置面板UI模块 ==================== var ConfigPanel = { panelEl: null, isOpen: false, editingIndex: -1, /** 样式注入(幂等) */ injectStyles: function () { if (document.getElementById("glossary-config-styles")) return; var css = document.createElement("style"); css.id = "glossary-config-styles"; css.textContent = [ ".gc-settings-btn {", " position: fixed; top: 10px; right: 10px; z-index: 99999;", " width: 40px; height: 40px; border-radius: 50%;", " background: linear-gradient(135deg, #667eea, #764ba2);", " border: none; color: #fff; font-size: 20px;", " cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,.3);", " display: flex; align-items: center; justify-content: center;", " transition: transform .2s, box-shadow .2s;", "}", ".gc-settings-btn:hover { transform: scale(1.1); box-shadow: 0 4px 16px rgba(0,0,0,.4); }", ".gc-overlay {", " position: fixed; top: 0; left: 0; right: 0; bottom: 0;", " background: rgba(0,0,0,.5); z-index: 100000;", " display: flex; align-items: center; justify-content: center;", "}", ".gc-panel {", " width: 720px; max-height: 80vh; background: #fff; border-radius: 12px;", " box-shadow: 0 8px 40px rgba(0,0,0,.3);", " display: flex; flex-direction: column; overflow: hidden;", " font-family: -apple-system, 'Microsoft YaHei', sans-serif;", "}", ".gc-header {", " padding: 16px 20px; background: linear-gradient(135deg, #667eea, #764ba2);", " color: #fff; display: flex; justify-content: space-between; align-items: center;", "}", ".gc-header h3 { margin: 0; font-size: 18px; font-weight: 600; }", ".gc-close-btn {", " background: rgba(255,255,255,.2); border: none; color: #fff;", " width: 30px; height: 30px; border-radius: 50%; font-size: 18px;", " cursor: pointer; line-height: 1; transition: background .2s;", "}", ".gc-close-btn:hover { background: rgba(255,255,255,.35); }", ".gc-toolbar {", " padding: 12px 20px; border-bottom: 1px solid #eee; display: flex; gap: 10px; align-items: center; flex-wrap: wrap;", "}", ".gc-search-input {", " flex: 1; min-width: 180px; padding: 7px 14px; border: 1px solid #dcdfe6; border-radius: 6px;", " font-size: 14px; outline: none; transition: border-color .2s;", "}", ".gc-search-input:focus { border-color: #667eea; }", ".gc-toolbar-btn {", " padding: 7px 16px; border-radius: 6px; border: none; font-size: 13px; cursor: pointer;", " transition: opacity .2s; white-space: nowrap; font-weight: 500;", "}", ".gc-btn-primary { background: #667eea; color: #fff; } .gc-btn-primary:hover { opacity: .85; }", ".gc-btn-success { background: #67c23a; color: #fff; } .gc-btn-success:hover { opacity: .85; }", ".gc-btn-warning { background: #e6a23c; color: #fff; } .gc-btn-warning:hover { opacity: .85; }", ".gc-btn-danger { background: #f56c6c; color: #fff; } .gc-btn-danger:hover { opacity: .85; }", ".gc-btn-default { background: #f4f4f5; color: #606266; border: 1px solid #dcdfe6; } .gc-btn-default:hover { opacity: .75; }", ".gc-table-wrap { flex: 1; overflow-y: auto; padding: 0 20px; }", ".gc-table { width: 100%; border-collapse: collapse; font-size: 13px; }", ".gc-table th { position: sticky; top: 0; z-index: 1; background: #fafafa; padding: 10px 12px; text-align: left; border-bottom: 2px solid #ebeef5; font-weight: 600; color: #303133; }", ".gc-table td { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; vertical-align: middle; word-break: break-all; }", ".gc-table tr:hover td { background: #f5f7ff; }", ".gc-name-cell { font-weight: 500; color: #303133; min-width: 120px; }", ".gc-rule-tags { display: flex; flex-wrap: wrap; gap: 4px; }", ".gc-tag { padding: 2px 8px; border-radius: 4px; font-size: 12px; color: #fff; }", ".gc-tag-red { background: #f56c6c; } .gc-tag-orange { background: #e6a23c; }", ".gc-tag-blue { background: #409eff; } .gc-tag-green { background: #67c23a; }", ".gc-tag-cyan { background: #13ce66; } .gc-tag-gray { background: #909399; }", ".gc-action-cell { white-space: nowrap; width: 90px; }", ".gc-action-btn { padding: 3px 10px; margin-right: 4px; border: none; border-radius: 4px; font-size: 12px; cursor: pointer; color: #fff; transition: opacity .2s; }", ".gc-action-btn:hover { opacity: .8; }", ".gc-edit-btn { background: #409eff; } .gc-del-btn { background: #f56c6c; }", ".gc-footer { padding: 10px 20px; border-top: 1px solid #eee; font-size: 13px; color: #909399; display: flex; justify-content: space-between; align-items: center; }", ".gc-modal-overlay { position: fixed; top:0; left:0; right:0; bottom:0; background: rgba(0,0,0,.45); z-index: 200000; display: flex; align-items: center; justify-content: center; }", ".gc-form-modal { width: 420px; background: #fff; border-radius: 10px; padding: 24px; box-shadow: 0 8px 30px rgba(0,0,0,.25); font-family: inherit; }", ".gc-form-title { margin: 0 0 16px; font-size: 16px; color: #303133; }", ".gc-form-group { margin-bottom: 16px; }", ".gc-form-label { display: block; margin-bottom: 6px; font-size: 13px; color: #606266; font-weight: 500; }", ".gc-form-input { width: 100%; padding: 8px 12px; border: 1px solid #dcdfe6; border-radius: 6px; font-size: 14px; outline: none; box-sizing: border-box; font-family: inherit; }", ".gc-form-input:focus { border-color: #667eea; }", ".gc-checkbox-group { display: flex; flex-wrap: wrap; gap: 8px; }", ".gc-checkbox-item { display: flex; align-items: center; gap: 4px; cursor: pointer; font-size: 13px; color: #606266; }", ".gc-checkbox-item input { accent-color: #667eea; }", ".gc-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; }", ".gc-toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 10px 24px; border-radius: 6px; color: #fff; font-size: 14px; z-index: 300000; animation: gcFadeIn .3s ease; pointer-events: none; }", "@keyframes gcFadeIn { from{opacity:0;transform:translateX(-50%) translateY(-10px)} to{opacity:1;transform:translateX(-50%) translateY(0)} }", ".gc-toast-success { background: #67c23a; } .gc-toast-error { background: #f56c6c; } .gc-toast-info { background: #909399; }", ".gc-empty { text-align: center; padding: 40px 20px; color: #c0c4cc; }", ".gc-empty-icon { font-size: 48px; margin-bottom: 10px; } .gc-empty-text { font-size: 14px; }", ".gc-import-area { width:100%; height:200px; padding:10px; border:1px solid #dcdfe6;border-radius:6px;font-size:12px;font-family:monospace;resize:vertical;box-sizing:border-box;margin-top:8px; }", ".gc-shortcut-hint { position:fixed; bottom:20px; right:20px; z-index:99998; background:rgba(0,0,0,.75); color:#fff; padding:12px 18px; border-radius:8px; font-size:12px; line-height:1.8; font-family:-apple-system,'Microsoft YaHei',sans-serif; transition:opacity .5s; cursor:pointer; }", ".gc-shortcut-hint .gc-key { display:inline-block; background:#667eea; color:#fff; padding:1px 8px; border-radius:3px; font-weight:600; margin:0 2px; }", ".gc-kb-btn { position:fixed; top:10px; right:58px; z-index:99999; width:40px; height:40px; border-radius:50%; background:linear-gradient(135deg,#409eff,#337ecc); border:none; color:#fff; font-size:18px; cursor:pointer; box-shadow:0 2px 10px rgba(0,0,0,.3); display:flex; align-items:center; justify-content:center; transition:transform .2s,box-shadow .2s; }", ".gc-kb-btn:hover { transform:scale(1.1); box-shadow:0 4px 16px rgba(0,0,0,.4); }", ".gc-kb-row { display:flex; align-items:center; justify-content:space-between; padding:10px 0; border-bottom:1px solid #f0f0f0; }", ".gc-kb-row:last-child { border-bottom:none; }", ".gc-kb-action-name { font-size:14px; color:#303133; font-weight:500; }", ".gc-kb-key-box { display:inline-block; min-width:60px; padding:4px 14px; background:#f4f4f5; border:1px solid #dcdfe6; border-radius:4px; font-size:13px; color:#606266; text-align:center; cursor:pointer; transition:border-color .2s,background .2s; user-select:none; }", ".gc-kb-key-box:hover { border-color:#409eff; }", ".gc-kb-key-box.gc-recording { background:#ecf5ff; border-color:#409eff; color:#409eff; animation:gcPulse 1s infinite; }", "@keyframes gcPulse { 0%,100%{box-shadow:0 0 0 0 rgba(64,158,255,.3)} 50%{box-shadow:0 0 0 6px rgba(64,158,255,0)} }", ].join("\n"); document.head.appendChild(css); }, /** 显示Toast消息 */ showToast: function (msg, type) { type = type || "info"; var toast = document.createElement("div"); toast.className = "gc-toast gc-toast-" + type; toast.textContent = msg; document.body.appendChild(toast); setTimeout(function () { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 2200); }, /** 渲染表格 */ renderTable: function (data) { var tbody = this.panelEl.querySelector(".gc-tbody"); if (!data || data.length === 0) { tbody.innerHTML = '
🔎
暂无匹配词条
'; return; } var html = ""; for (var i = 0; i < data.length; i++) { var item = data[i]; var tagsHtml = ""; for (var j = 0; j < item.rules.length; j++) { var tagClass = RULE_TAG_CLASS_MAP[item.rules[j]] || "gc-tag-gray"; tagsHtml += '' + item.rules[j] + ''; } html += '' + '' + this._escapeHtml(item.name) + '' + '
' + tagsHtml + '
' + '' + '' + '' + ''; } tbody.innerHTML = html; this._bindTableEvents(data); }, _escapeHtml: function (str) { var div = document.createElement("div"); div.appendChild(document.createTextNode(str)); return div.innerHTML; }, _bindTableEvents: function (data) { var self = this; var editBtns = this.panelEl.querySelectorAll(".gc-edit-btn"); for (var i = 0; i < editBtns.length; i++) { editBtns[i].onclick = (function (idx) { return function () { self.showEditForm(data[idx], idx); }; })(parseInt(editBtns[i].dataset.index)); } var delBtns = this.panelEl.querySelectorAll(".gc-del-btn"); for (var j = 0; j < delBtns.length; j++) { delBtns[j].onclick = (function (idx) { return function () { self.deleteEntry(idx); }; })(parseInt(delBtns[j].dataset.index)); } }, showEditForm: function (item, index) { this.editingIndex = index !== undefined ? index : -1; this._showFormModal(item ? item.name : "", item ? item.rules : []); }, showAddForm: function () { this.editingIndex = -1; this._showFormModal("", []); }, _showFormModal: function (name, rules) { var self = this; var overlay = document.createElement("div"); overlay.className = "gc-modal-overlay"; var checkboxes = ""; for (var i = 0; i < RULE_OPTIONS.length; i++) { var checked = rules.indexOf(RULE_OPTIONS[i]) > -1 ? " checked" : ""; checkboxes += ''; } overlay.innerHTML = '
' + '

' + (this.editingIndex >= 0 ? "编辑词条" : "新增词条") + '

' + '
' + '
' + '
' + '
' + checkboxes + '
' + '
' + '' + '' + '
'; document.body.appendChild(overlay); overlay.querySelector(".gc-fm-cancel").onclick = function () { document.body.removeChild(overlay); }; overlay.onclick = function (e) { if (e.target === overlay) document.body.removeChild(overlay); }; overlay.querySelector(".gc-fm-save").onclick = function () { var nameVal = overlay.querySelector(".gc-fm-name").value.trim(); if (!nameVal) { self.showToast("请输入名称!", "error"); return; } var cbs = overlay.querySelectorAll(".gc-checkbox-group input:checked"); var selectedRules = []; for (var k = 0; k < cbs.length; k++) { selectedRules.push(cbs[k].value); } if (selectedRules.length === 0) { self.showToast("请至少选择一条规则!", "error"); return; } self.saveEntry(nameVal, selectedRules); document.body.removeChild(overlay); }; }, saveEntry: function (name, rules) { var arr = GlossaryManager.parseToArray(Glossary1); if (this.editingIndex >= 0 && this.editingIndex < arr.length) { arr[this.editingIndex].name = name; arr[this.editingIndex].rules = rules; this.showToast('词条 "' + name + '" 已更新', "success"); } else { for (var i = 0; i < arr.length; i++) { if (arr[i].name === name) { this.showToast('词条 "' + name + '" 已存在!', "error"); return; } } arr.push({ name: name, rules: rules }); this.showToast('已添加词条 "' + name + '"', "success"); } Glossary1 = GlossaryManager.arrayToString(arr); GlossaryManager.save(Glossary1); onGlossaryChanged(); this.refreshTable(); }, deleteEntry: function (index) { var arr = GlossaryManager.parseToArray(Glossary1); if (index < 0 || index >= arr.length) return; var name = arr[index].name; if (!confirm('确定删除词条 "' + name + '" 吗?')) return; arr.splice(index, 1); Glossary1 = GlossaryManager.arrayToString(arr); GlossaryManager.save(Glossary1); onGlossaryChanged(); this.showToast('已删除 "' + name + '"', "success"); this.refreshTable(); }, refreshTable: function () { if (!this.panelEl) return; var keyword = ""; var searchInput = this.panelEl.querySelector(".gc-search-input"); if (searchInput) keyword = searchInput.value.trim(); var data = GlossaryManager.search(Glossary1, keyword); this.renderTable(data); this.updateCount(); }, updateCount: function () { var countEl = this.panelEl ? this.panelEl.querySelector(".gc-count") : null; if (countEl) countEl.textContent = "共 " + glossaryIndex.size + " 条词条"; }, exportGlossary: function () { var blob = new Blob([Glossary1], { type: "text/plain;charset=utf-8" }); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = "glossary_export_" + Date.now() + ".txt"; a.click(); URL.revokeObjectURL(url); this.showToast("导出成功!", "success"); }, showImportModal: function () { var self = this; var overlay = document.createElement("div"); overlay.className = "gc-modal-overlay"; overlay.innerHTML = '
' + '

导入词库

' + '

粘贴词库数据(格式:名称\\t规则|名称\\t规则...)

' + '' + '
' + '' + '' + '
'; document.body.appendChild(overlay); overlay.querySelector(".gc-imp-cancel").onclick = function () { document.body.removeChild(overlay); }; overlay.onclick = function (e) { if (e.target === overlay) document.body.removeChild(overlay); }; overlay.querySelector(".gc-imp-confirm").onclick = function () { var val = overlay.querySelector(".gc-import-area").value.trim(); if (!val) { self.showToast("请输入内容!", "error"); return; } if (val.indexOf("\t") === -1 || val.indexOf("|") === -1) { self.showToast("数据格式不正确!", "error"); return; } Glossary1 = val; GlossaryManager.save(val); onGlossaryChanged(); self.refreshTable(); document.body.removeChild(overlay); self.showToast("导入成功!", "success"); }; }, resetToDefault: function () { if (!confirm("确定恢复为默认词库吗?当前所有自定义修改将丢失!")) return; Glossary1 = GlossaryManager.reset(); onGlossaryChanged(); this.refreshTable(); this.showToast("已恢复默认词库", "success"); }, open: function () { if (this.isOpen) return; this.isOpen = true; this.injectStyles(); var overlay = document.createElement("div"); overlay.className = "gc-overlay"; overlay.innerHTML = '
' + '

⚙ Glossary 词库配置

' + '
' + ' ' + ' ' + ' ' + ' ' + ' ' + '
' + '
' + ' ' + '
名称(关键词)匹配规则操作
' + ' ' + '
'; document.body.appendChild(overlay); this.panelEl = overlay; var self = this; overlay.querySelector(".gc-close-btn").onclick = function () { self.close(); }; overlay.onclick = function (e) { if (e.target === overlay) self.close(); }; overlay.querySelector(".gc-search-input").oninput = function () { self.renderTable(GlossaryManager.search(Glossary1, self.panelEl.querySelector(".gc-search-input").value.trim())); }; overlay.querySelector(".gc-add-btn").onclick = function () { self.showAddForm(); }; overlay.querySelector(".gc-export-btn").onclick = function () { self.exportGlossary(); }; overlay.querySelector(".gc-import-btn").onclick = function () { self.showImportModal(); }; overlay.querySelector(".gc-reset-btn").onclick = function () { self.resetToDefault(); }; this.renderTable(GlossaryManager.parseToArray(Glossary1)); this.updateCount(); }, close: function () { if (!this.isOpen) return; this.isOpen = false; if (this.panelEl && this.panelEl.parentNode) this.panelEl.parentNode.removeChild(this.panelEl); this.panelEl = null; } }; // ==================== 快捷键配置面板 ==================== var ShortcutPanel = { panelEl: null, isOpen: false, recordingAction: null, open: function () { if (this.isOpen) return; this.isOpen = true; ConfigPanel.injectStyles(); var overlay = document.createElement("div"); overlay.className = "gc-overlay"; overlay.innerHTML = '
' + '

⌨ 快捷键设置

' + '
' + ' ' + '
'; document.body.appendChild(overlay); this.panelEl = overlay; var self = this; overlay.querySelector(".gc-close-btn").onclick = function () { self.close(); }; overlay.onclick = function (e) { if (e.target === overlay) self.close(); }; overlay.querySelector(".gc-kb-reset").onclick = function () { self.resetAll(); }; this.renderRows(); }, renderRows: function () { var body = this.panelEl.querySelector(".gc-kb-body"); var shortcuts = ShortcutManager.load(); var html = ""; var actions = ["prioritySubmit", "rejectSubmit", "submitOnly"]; for (var i = 0; i < actions.length; i++) { var act = actions[i]; var keyName = keyNameFromCode(shortcuts[act]); html += '
' + '' + SHORTCUT_LABELS[act] + '' + '' + keyName + '' + '
'; } body.innerHTML = html; var self = this; var keyBoxes = body.querySelectorAll(".gc-kb-key-box"); for (var j = 0; j < keyBoxes.length; j++) { keyBoxes[j].onclick = (function (box) { return function () { self.startRecording(box); }; })(keyBoxes[j]); } }, startRecording: function (box) { // 取消之前的录制 if (this.recordingAction) { var prevBox = this.panelEl.querySelector('.gc-kb-key-box[data-action="' + this.recordingAction + '"]'); if (prevBox) prevBox.classList.remove("gc-recording"); } this.recordingAction = box.dataset.action; box.classList.add("gc-recording"); box.textContent = "按下新按键..."; var self = this; var handler = function (e) { e.preventDefault(); e.stopPropagation(); if (e.keyCode === 27) { // Esc 取消录制 self.stopRecording(); document.removeEventListener("keydown", handler, true); return; } var newCode = e.keyCode; var action = self.recordingAction; var shortcuts = ShortcutManager.load(); // 检查冲突 for (var k in shortcuts) { if (k !== action && shortcuts[k] === newCode) { ConfigPanel.showToast('按键 "' + keyNameFromCode(newCode) + '" 已被 ' + SHORTCUT_LABELS[k] + ' 使用!', "error"); self.stopRecording(); document.removeEventListener("keydown", handler, true); return; } } shortcuts[action] = newCode; ShortcutManager.save(shortcuts); self.stopRecording(); self.renderRows(); ConfigPanel.showToast(SHORTCUT_LABELS[action] + " 已绑定为 " + keyNameFromCode(newCode), "success"); document.removeEventListener("keydown", handler, true); }; document.addEventListener("keydown", handler, true); }, stopRecording: function () { if (!this.recordingAction) return; var box = this.panelEl.querySelector('.gc-kb-key-box[data-action="' + this.recordingAction + '"]'); if (box) box.classList.remove("gc-recording"); this.recordingAction = null; }, resetAll: function () { if (!confirm("确定恢复默认快捷键设置?")) return; ShortcutManager.reset(); this.renderRows(); ConfigPanel.showToast("已恢复默认快捷键", "success"); }, close: function () { if (!this.isOpen) return; this.isOpen = false; this.stopRecording(); if (this.panelEl && this.panelEl.parentNode) this.panelEl.parentNode.removeChild(this.panelEl); this.panelEl = null; } }; // ==================== 快捷键提示 ==================== function showShortcutHint() { if (GM_getValue("shortcut_hint_shown", false)) return; var sc = ShortcutManager.load(); var hint = document.createElement("div"); hint.className = "gc-shortcut-hint"; hint.innerHTML = '
' + keyNameFromCode(sc.prioritySubmit) + ' ' + SHORTCUT_LABELS.prioritySubmit + '   ' + keyNameFromCode(sc.rejectSubmit) + ' ' + SHORTCUT_LABELS.rejectSubmit + '   ' + keyNameFromCode(sc.submitOnly) + ' ' + SHORTCUT_LABELS.submitOnly + '
' + '
点击 ⚙ 配置词库 / ⌨ 配置快捷键
'; document.body.appendChild(hint); setTimeout(function () { hint.style.opacity = "0"; setTimeout(function () { if (hint.parentNode) hint.parentNode.removeChild(hint); }, 600); }, 6000); hint.onclick = function () { GM_setValue("shortcut_hint_shown", true); if (hint.parentNode) hint.parentNode.removeChild(hint); }; } // ==================== DOM辅助方法 ==================== /** 安全获取单个元素 */ function $(selector) { return document.querySelector(selector); } /** 安全获取元素列表 */ function $$(selector) { return document.querySelectorAll(selector); } // ==================== 核心匹配逻辑 ==================== /** * 解析当前任务信息,在页面上展示匹配结果 * 使用 Map 索引进行 O(1) 查找(替代原来的 split + 线性遍历) */ var refreshMatchObserver = null; function refreshMatch() { var resultEl = $(SELECTORS.matchResult); if (resultEl) resultEl.innerHTML = ""; // 使用 MutationObserver 替代固定 setTimeout,监听 .text-item 内容变化 var textItemEl = $(SELECTORS.textItem); if (!textItemEl) { // 元素尚未就绪,用短延迟重试 setTimeout(doMatch, 200); return; } doMatch(); } function doMatch() { var infoItemEl = $(SELECTORS.textItem); if (!infoItemEl) return; var infoText = infoItemEl.innerText; var parts = infoText.includes(";") ? infoText.split(";") : infoText.split("_"); if (parts.length < 2) return; // 高分值标记 var labelEls = $$(SELECTORS.label); if (labelEls[1]) { labelEls[1].innerHTML = (parseFloat(parts[1]) > 0.8) ? '高分值: ' : "标题: "; } // 使用 Map 索引 O(1) 查找 var rules = glossaryIndex.get(parts[0]); var resultEl = $(SELECTORS.matchResult); if (rules !== undefined && resultEl) { resultEl.innerHTML = rules; } } /** * 根据匹配到的规则自动勾选对应的违规程度复选框 * 使用 RULE_CHECKBOX_MAP 配置表驱动(替代硬编码 switch) * 缓存 DOM 查询结果,避免循环内重复 querySelectorAll * @returns {number} 0=成功匹配, -1=未匹配 */ function autoCheckRules() { var resultEl = $(SELECTORS.matchResult); if (!resultEl) return -1; var rulesStr = resultEl.innerHTML; if (!rulesStr) return -1; var rules = rulesStr.split(","); // 缓存 DOM 查询 var checkboxLabels = $$(SELECTORS.checkboxLabel); var offset = checkboxLabels.length === 4 ? 2 : 0; for (var i = 0; i < rules.length; i++) { var rule = rules[i].trim(); var idx = RULE_CHECKBOX_MAP[rule]; if (idx === undefined) return -1; var targetIdx = idx - offset; if (checkboxLabels[targetIdx]) { // 使用 IIFE 捕获当前 idx,避免闭包问题 (function (ti) { setTimeout(function () { checkboxLabels[ti].click(); }, 50); })(targetIdx); } } return 0; } // ==================== 主逻辑入口 ==================== setTimeout(function () { if (window.location.href.indexOf("detail?container_id=1067") > -1 || window.location.href.indexOf("detail?container_id=77") > -1 || window.location.href.indexOf("detail?container_id=120") > -1) { // 创建设置按钮 var settingsBtn = document.createElement("button"); settingsBtn.className = "gc-settings-btn"; settingsBtn.title = "词库配置"; settingsBtn.innerHTML = "⚙"; settingsBtn.onclick = function () { ConfigPanel.open(); }; document.body.appendChild(settingsBtn); // 创建快捷键设置按钮 var kbBtn = document.createElement("button"); kbBtn.className = "gc-kb-btn"; kbBtn.title = "快捷键设置"; kbBtn.innerHTML = "⌨"; kbBtn.onclick = function () { ShortcutPanel.open(); }; document.body.appendChild(kbBtn); // 创建匹配结果容器 var resultContainer = document.createElement("heis_ss"); resultContainer.appendChild(document.createTextNode("")); var taskInfoEl = $(SELECTORS.taskInfo); if (taskInfoEl) taskInfoEl.appendChild(resultContainer); // 首次匹配 refreshMatch(); // 显示快捷键提示 ConfigPanel.injectStyles(); showShortcutHint(); // ========== 键盘快捷键(使用自定义配置) ========== document.onkeydown = function (event) { var e = event || window.event || arguments.callee.caller.arguments[0]; if (!e) return; var submitBtn = $(SELECTORS.submitBtn); var radioBtns = $$(SELECTORS.radio); var sc = ShortcutManager.load(); if (e.keyCode === sc.prioritySubmit) { // 优先+提交 if (autoCheckRules() !== -1) { setTimeout(function () { if (radioBtns[0]) radioBtns[0].click(); if (submitBtn) submitBtn.click(); refreshMatch(); }, 200); } else { setTimeout(function () { if (radioBtns[0]) radioBtns[0].click(); }, 200); } } else if (e.keyCode === sc.rejectSubmit) { // 不通过+提交 if (radioBtns[1]) radioBtns[1].click(); if (submitBtn) submitBtn.click(); refreshMatch(); } else if (e.keyCode === sc.submitOnly) { // 仅提交 if (submitBtn) submitBtn.click(); refreshMatch(); } }; // ========== 按钮事件绑定 ========== var radioBtns = $$(SELECTORS.radio); if (radioBtns[1]) { radioBtns[1].onclick = function () { var allLabels = $$(SELECTORS.checkboxLabel); if (allLabels[allLabels.length - 1]) allLabels[allLabels.length - 1].click(); }; } if (radioBtns[0]) { radioBtns[0].onclick = function () { autoCheckRules(); }; } var submitBtn = $(SELECTORS.submitBtn); if (submitBtn) submitBtn.onclick = function () { refreshMatch(); }; var topBtns = $$(SELECTORS.topBtn); if (topBtns[0]) topBtns[0].onclick = function () { refreshMatch(); }; if (topBtns[1]) topBtns[1].onclick = function () { refreshMatch(); }; var skipBtn = $(SELECTORS.skipBtn); if (skipBtn) skipBtn.onclick = function () { refreshMatch(); }; // ========== 监听页面内容变化自动刷新匹配 ========== // 使用 MutationObserver 替代依赖 setTimeout 固定延迟 var taskInfoObs = $(SELECTORS.taskInfo); if (taskInfoObs && typeof MutationObserver !== "undefined") { refreshMatchObserver = new MutationObserver(function (mutations) { var shouldRefresh = false; for (var i = 0; i < mutations.length; i++) { if (mutations[i].type === "childList" || mutations[i].type === "characterData") { shouldRefresh = true; break; } } if (shouldRefresh) { // 延迟执行避免频繁触发 setTimeout(doMatch, 150); } }); refreshMatchObserver.observe(taskInfoObs, { childList: true, subtree: true, characterData: true }); } } }, 2000) })();