// ==UserScript== // @name 朝阳的工具 - 统一多功能脚本 // @namespace http://tampermonkey.net/ // @version 4.3.3 // @description 集成了验证码识别、Steam游戏入库和视频解析三大功能。所有功能及设置在统一界面中管理,支持按需启用,UI立体美观。 // @author zhaoyang // @match *://*/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_notification // @grant GM_setClipboard // @grant unsafeWindow // @connect aip.baidubce.com // @connect api.github.com // @connect raw.githubusercontent.com // @connect github.com // @connect *://*.hls.one/* // @connect *://*.jx.cn/* // @connect *://*.xmflv.com/* // @connect *://*.nnsvip.cn/* // @connect *://*.ckplayer.vip/* // @connect *://*.nnxv.cn/* // @connect *://*.yemu.xyz/* // @connect *://*.pangujiexi.com/* // @connect *://*.8090g.cn/* // @connect *://*.playm3u8.cn/* // @connect *://*.77flv.cc/* // @connect *://*.2s0.cn/* // @connect *://*.playerjy.com/* // @require https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js // @run-at document-idle // @license GPL License // ==/UserScript== /* global $ */ (function () { 'use strict'; // --- 全局配置和常量 --- const SCRIPT_PREFIX = 'chaoyang_unified_tool_'; const GM_SETTINGS_KEY = SCRIPT_PREFIX + 'global_settings'; let globalSettings = {}; // 全局功能开关设置 // 默认全局设置 const DEFAULT_GLOBAL_SETTINGS = { enableCaptchaOcr: false, enableSteamGameEntry: false, enableVideoParser: false }; // --- Tampermonkey UI 样式注入 --- GM_addStyle(` /* 通用模态框样式 */ .chaoyang-modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); z-index: 9999999; display: none; justify-content: center; align-items: center; -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); } .chaoyang-modal { background-color: #2a2a2a; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7); padding: 30px; width: 700px; max-width: 90%; max-height: 90%; overflow-y: auto; color: #e0e0e0; font-family: 'Segoe UI', Arial, sans-serif; font-size: 15px; box-sizing: border-box; position: relative; transform: scale(0.95); opacity: 0; transition: transform 0.3s ease-out, opacity 0.3s ease-out; } .chaoyang-modal.show { transform: scale(1); opacity: 1; } .chaoyang-modal h2, .chaoyang-modal h3 { color: #007bff; /* 主题蓝色 */ text-align: center; margin-top: 0; margin-bottom: 25px; font-size: 24px; font-weight: bold; text-shadow: 1px 1px 2px rgba(0,0,0,0.3); } .chaoyang-modal h3 { font-size: 18px; color: #f0f0f0; border-bottom: 1px solid #444; padding-bottom: 10px; margin-top: 25px; margin-bottom: 20px; } .chaoyang-modal .close-button { position: absolute; top: 15px; right: 20px; background: none; border: none; font-size: 32px; color: #aaa; cursor: pointer; transition: color 0.2s, transform 0.2s; } .chaoyang-modal .close-button:hover { color: white; transform: rotate(90deg); } /* 表单元素样式 */ .chaoyang-modal label { display: block; margin-bottom: 10px; font-weight: bold; color: #f0f0f0; display: flex; align-items: center; } .chaoyang-modal input[type="text"], .chaoyang-modal input[type="password"], .chaoyang-modal textarea { width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #555; border-radius: 6px; background-color: #333; color: #e0e0e0; font-size: 14px; box-shadow: inset 1px 1px 3px rgba(0,0,0,0.3); transition: border-color 0.2s, box-shadow 0.2s; } .chaoyang-modal input[type="text"]:focus, .chaoyang-modal input[type="password"]:focus, .chaoyang-modal textarea:focus { border-color: #007bff; box-shadow: inset 1px 1px 3px rgba(0,0,0,0.3), 0 0 5px rgba(0,123,255,0.5); outline: none; } /* 按钮样式 */ .chaoyang-modal button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; font-weight: bold; margin-right: 15px; background-color: #007bff; color: white; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: background-color 0.2s, transform 0.1s, box-shadow 0.2s; } .chaoyang-modal button:hover { background-color: #0056b3; transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0,0,0,0.4); } .chaoyang-modal button.secondary { background-color: #6c757d; } .chaoyang-modal button.secondary:hover { background-color: #5a6268; } .chaoyang-modal button.danger { background-color: #dc3545; } .chaoyang-modal button.danger:hover { background-color: #c82333; } .chaoyang-modal button:disabled { background-color: #4a4a4a; cursor: not-allowed; opacity: 0.7; transform: none; box-shadow: none; } /* 开关样式 */ .chaoyang-toggle-switch { position: relative; display: inline-block; width: 60px; height: 34px; margin-left: 15px; vertical-align: middle; } .chaoyang-toggle-switch input { opacity: 0; width: 0; height: 0; } .chaoyang-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 34px; } .chaoyang-slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .chaoyang-slider { background-color: #2196F3; } input:focus + .chaoyang-slider { box-shadow: 0 0 1px #2196F3; } input:checked + .chaoyang-slider:before { transform: translateX(26px); } .chaoyang-switch-label { display: flex; align-items: center; margin-bottom: 15px; font-size: 16px; color: #e0e0e0; font-weight: normal; } .chaoyang-switch-label .desc { font-size: 0.9em; color: #aaa; margin-left: 10px; } /* 选项组样式 (如下载源、文件类型) */ .chaoyang-option-group { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px; } .chaoyang-option-group .chaoyang-toggle-label { cursor: pointer; font-size: 15px; display: inline-flex; align-items: center; padding: 0; background-color: transparent; position: relative; /* For the radio button */ } .chaoyang-option-group .chaoyang-toggle-label input[type="radio"] { display: none; /* Hide native radio button */ } .chaoyang-option-group .chaoyang-toggle-label span { display: inline-block; padding: 8px 15px; border-radius: 6px; background-color: #3a3a3a; color: #e0e0e0; transition: background-color 0.2s, color 0.2s, transform 0.1s; box-shadow: 0 2px 4px rgba(0,0,0,0.2); position: relative; z-index: 1; /* Ensure span is above potential pseudo-elements */ } .chaoyang-option-group .chaoyang-toggle-label:hover span { background-color: #4a4a4a; transform: translateY(-1px); box-shadow: 0 4px 6px rgba(0,0,0,0.3); } .chaoyang-option-group .chaoyang-toggle-label input[type="radio"]:checked + span { background-color: #007bff; color: white; box-shadow: 0 3px 6px rgba(0,123,255,0.4), inset 0 0 8px rgba(255,255,255,0.2); transform: translateY(-1px); } .chaoyang-option-group .chaoyang-toggle-label:active span { transform: translateY(0); box-shadow: 0 2px 4px rgba(0,0,0,0.2); } /* 分隔线 */ .chaoyang-modal .divider { height: 1px; background-color: #444; margin: 30px 0; } /* 功能模块容器 */ .chaoyang-feature-module { margin-bottom: 25px; padding: 15px; background-color: #333; border-radius: 8px; border: 1px solid #444; box-shadow: inset 0 0 5px rgba(0,0,0,0.2); } .chaoyang-feature-module h4 { color: #007bff; margin-top: 0; margin-bottom: 15px; font-size: 16px; font-weight: bold; } .chaoyang-feature-module p.setting-desc { font-size: 0.85em; color: #aaa; margin-top: -8px; margin-bottom: 15px; } /* 自定义API列表 */ #chaoyang-custom-api-list { padding:0; margin-top: 15px; list-style: none; display: flex; flex-wrap: wrap; gap: 10px; } #chaoyang-custom-api-list li { background-color: #444; color: #eee; padding: 6px 10px; border-radius: 4px; font-size: 13px; display: flex; align-items: center; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } #chaoyang-custom-api-list li .delete-api-btn { background-color: #dc3545; color: white; border: none; border-radius: 50%; width: 20px; height: 20px; line-height: 20px; padding: 0; margin-left: 8px; cursor: pointer; font-weight: bold; font-size: 12px; display: inline-flex; justify-content: center; align-items: center; flex-shrink: 0; box-shadow: 0 1px 3px rgba(0,0,0,0.3); transition: background-color 0.2s, transform 0.1s; } #chaoyang-custom-api-list li .delete-api-btn:hover { background-color: #c82333; transform: scale(1.1); } /* Token status display */ .chaoyang-token-status { margin-top: 5px; font-size: 0.9em; color: #ccc; } /* 验证码识别 tip 样式 (沿用原Captcha Solver的样式,略作调整) */ #baidu_ocr_tip_container { position: fixed; top: -5em; /* Start off-screen */ right: 10px; background-color: rgba(0,0,0,0.85); /* 更深的背景 */ color: white; padding: 10px 15px; border-radius: 8px; /* 更圆润的边角 */ z-index: 9999999; font-size: 14px; max-width: 300px; text-align: right; box-sizing: border-box; transition: top 0.3s ease-in-out; /* Smooth transition for slide */ box-shadow: 0 4px 15px rgba(0,0,0,0.5); /* 增加阴影 */ border: 1px solid rgba(255,255,255,0.1); /* 细微边框 */ } #baidu_ocr_tip_container.show { top: 10px; /* Adjust to a visible position, not 0em for padding */ } #baidu_ocr_tip_container span.close-btn { /* 关闭按钮样式 */ color: #ccc; float: right; margin-left: 10px; font-size: 16px; font-weight: bold; cursor: pointer; } #baidu_ocr_tip_container span.close-btn:hover { color: white; } /* Steam入库 浮动按钮及伴随元素样式 */ .chaoyang-tool-storage-btn { position: fixed; width: 60px; height: 60px; border-radius: 50%; background-color: rgba(0, 0, 0, 0.8); /* 修改为黑色 */ color: white; font-size: 16px; font-weight: bold; border: none; cursor: grab; z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.4); transition: background-color 0.3s, transform 0.2s; /* 移除 top/left/right 过渡,以实现流畅拖动 */ } .chaoyang-tool-storage-btn:hover { background-color: rgba(0, 0, 0, 1); /* 修改为黑色 */ transform: scale(1.05); box-shadow: 0 6px 15px rgba(0,0,0,0.5); } .chaoyang-tool-status-box { position: fixed; width: 70px; height: 30px; border-radius: 5px; background-color: #6c757d; color: white; font-size: 14px; display: flex; justify-content: center; align-items: center; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: background-color 0.3s; /* 移除 top/left/right 过渡 */ } .chaoyang-tool-refresh-btn { position: fixed; width: 70px; height: 25px; border-radius: 5px; background-color: #007bff; color: white; font-size: 12px; border: none; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: background-color 0.3s; /* 移除 top/left/right 过渡 */ } .chaoyang-tool-refresh-btn:hover { background-color: #0056b3; transform: translateY(-1px); } .chaoyang-tool-clear-cache-btn { position: fixed; bottom: 20px; right: 20px; background-color: #dc3545; color: white; padding: 10px 15px; border-radius: 5px; border: none; cursor: pointer; z-index: 9999; box-shadow: 0 2px 5px rgba(0,0,0,0.3); font-size: 14px; transition: background-color 0.3s, transform 0.1s; } .chaoyang-tool-clear-cache-btn:hover { background-color: #c82333; transform: translateY(-1px); } /* 视频解析浮动按钮 */ #chaoyang_video_parser_entry_btn { position: fixed; width: 45px; height: 45px; border-radius: 50%; background-color: rgba(255, 165, 0, 0.8); /* 醒目的橙色 */ color: white; font-size: 18px; font-weight: bold; border: none; cursor: grab; /* Make it draggable */ z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 3px 8px rgba(0,0,0,0.4); transition: background-color 0.3s, transform 0.2s; /* 移除 top/left/right 过渡 */ right: 20px; /* 默认位置,可拖动 */ top: 200px; /* 默认位置,可拖动 */ } #chaoyang_video_parser_entry_btn:hover { background-color: rgba(255, 165, 0, 1); transform: scale(1.08); box-shadow: 0 5px 12px rgba(0,0,0,0.5); } /* 视频解析列表浮窗 */ #chaoyang_video_parser_list_modal { position: fixed; border-radius:12px; background-color: #2a2a2a; border:1px solid #444; padding:20px; width:480px; max-height: 80vh; overflow-y:auto; box-shadow: 0 8px 25px rgba(0,0,0,0.6); animation: fadeInScale 0.3s ease-out; z-index: 99999; /* Higher than normal content, lower than main settings */ top: 50%; left: 50%; transform: translate(-50%, -50%); display: none; color: #e0e0e0; } @keyframes fadeInScale { from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } #chaoyang_video_parser_list_modal h3 { color: #ffa500; /* 橙色标题 */ font-size: 20px; text-align: center; margin-bottom: 20px; border-bottom: 1px solid #555; padding-bottom: 10px; } #chaoyang_video_parser_list_modal ul { padding:0; margin:0; list-style: none; display: flex; flex-wrap: wrap; justify-content: flex-start; gap: 10px; } #chaoyang_video_parser_list_modal li { border-radius:6px; font-size:14px; color:#e0e0e0; text-align:center; width:calc(33.33% - 10px); line-height:36px; border:1px solid #555; padding:0 10px; overflow:hidden; white-space: nowrap; text-overflow: ellipsis; transition: all 0.2s ease; cursor: pointer; background-color: #3a3a3a; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #chaoyang_video_parser_list_modal li:hover { color:#fff; background-color:#555; border-color:#888; transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.3); } #chaoyang_video_parser_list_modal li.selected { color:#fff; background-color:#ffa500; border-color:#ffa500; box-shadow: 0 0 10px rgba(255,165,0,0.6); transform: translateY(-1px); } #chaoyang_video_parser_list_modal .close-button { position: absolute; top: 10px; right: 15px; background: none; border: none; font-size: 28px; color: #aaa; cursor: pointer; transition: color 0.2s, transform 0.2s; } #chaoyang_video_parser_list_modal .close-button:hover { color: white; transform: rotate(90deg); } `); // --- 全局工具函数 --- const Util = { // Tampermonkey存储键的通用前缀,避免与其他脚本冲突 STORAGE_PREFIX: SCRIPT_PREFIX, // 读取剪贴板内容 readClipboard: async function () { try { return await navigator.clipboard.readText(); } catch (err) { console.warn('Chaoyang Tool: 无法读取剪贴板内容 (可能需要用户授权或非HTTPS页面):', err); return null; } }, // 在新标签页打开URL openTab: function (url) { GM_openInTab(url, { active: true, insert: true, setParent: true }); }, // 显示通知 notify: function (title, text, timeout = 3000) { GM_notification({ title: title, text: text, image: 'https://www.tampermonkey.net/_favicon.ico', // Tampermonkey icon timeout: timeout }); }, // 获取URL的Hostname getHostname: function (url) { try { return new URL(url).hostname; } catch (e) { return ''; } }, // 睡眠函数 sleep: function (ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, // DOM查找器 (使用MutationObserver) findTargetElement: function (targetSelector, timeout = 10000) { return new Promise((resolve, reject) => { const element = document.querySelector(targetSelector); if (element) { return resolve(element); } const observer = new MutationObserver((_mutations, obs) => { const target = document.querySelector(targetSelector); if (target) { obs.disconnect(); resolve(target); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); const finalTarget = document.querySelector(targetSelector); if (finalTarget) { resolve(finalTarget); } else { reject(new Error(`Timeout finding element: ${targetSelector}`)); } }, timeout); }); }, // 统一的拖动功能 (参考Steam Game Entry的流畅实现,并统一保存格式) makeDraggable: function (element, storageKey, onDragEndCallback = null) { let isDragging = false; let offsetX, offsetY; let startX, startY; let hasMoved = false; const $element = $(element); // Temporarily disable transitions for smoother dragging const disableTransitions = () => { $element.css('transition', 'none'); if ($element.data('companionElements')) { $element.data('companionElements').forEach($comp => $comp.css('transition', 'none')); } }; const enableTransitions = () => { $element.css('transition', ''); // Restore original transition property from CSS if ($element.data('companionElements')) { $element.data('companionElements').forEach($comp => $comp.css('transition', '')); } }; $element.on('mousedown', function (e) { if (e.button !== 0) return; // Only left click isDragging = true; hasMoved = false; startX = e.clientX; startY = e.clientY; $element.css("cursor", "grabbing"); disableTransitions(); const rect = element.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; $(document).on("mousemove.draggable", function (e) { if (!isDragging) return; let newLeft = e.clientX - offsetX; let newTop = e.clientY - offsetY; // Ensure element stays within viewport const maxX = $(window).width() - element.offsetWidth; const maxY = $(window).height() - element.offsetHeight; newLeft = Math.max(0, Math.min(newLeft, maxX)); newTop = Math.max(0, Math.min(newTop, maxY)); element.style.left = `${newLeft}px`; element.style.top = `${newTop}px`; element.style.right = 'auto'; // Explicitly set right to auto when left is used if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) { hasMoved = true; } // If there are companion elements, update their positions too if ($element.data('updateCompanionPositions')) { $element.data('updateCompanionPositions')(); } e.preventDefault(); // Prevent text selection etc. }); $(document).on("mouseup.draggable", async function () { isDragging = false; $element.css("cursor", "grab"); enableTransitions(); $(document).off(".draggable"); // Save the final position (consistent format) const finalPos = { top: element.getBoundingClientRect().top, left: element.getBoundingClientRect().left, }; await GM_setValue(storageKey, finalPos); console.log(`Chaoyang Tool: Button position saved to ${storageKey}:`, finalPos); if (onDragEndCallback && !hasMoved) { onDragEndCallback(); // Only call if not dragged significantly } hasMoved = false; // Reset hasMoved for next drag }); e.preventDefault(); // Prevent text selection etc. }); }, // 统一的位置加载函数 applyButtonPosition: function (element, storedPosition, defaultTop, defaultRight = 20) { const $element = $(element); if (storedPosition && typeof storedPosition.top === 'number' && typeof storedPosition.left === 'number') { $element.css({ top: storedPosition.top + 'px', left: storedPosition.left + 'px', right: 'auto' // Ensure left takes precedence }); console.log('Chaoyang Tool: Applying stored button position:', storedPosition); } else if (storedPosition && typeof storedPosition.top === 'number' && typeof storedPosition.right === 'number') { // Fallback for older saved formats that might only have right $element.css({ top: storedPosition.top + 'px', right: storedPosition.right + 'px', left: 'auto' }); console.warn('Chaoyang Tool: Applying stored button position with old "right" format. Consider dragging to update to "left" for consistency:', storedPosition); } else { // Apply default position if no valid stored position $element.css({ top: defaultTop + 'px', right: defaultRight + 'px', left: 'auto' }); console.log('Chaoyang Tool: Applying default button position.'); } } }; // --- 统一设置模态框管理 --- const SettingsManager = { modalId: SCRIPT_PREFIX + 'settings_modal', backdropId: SCRIPT_PREFIX + 'settings_modal_backdrop', init: function () { GM_registerMenuCommand('⚙️ 朝阳的工具 - 设置', this.openModal.bind(this)); }, openModal: async function () { let $backdrop = $('#' + this.backdropId); let $modal = $('#' + this.modalId); if ($backdrop.length === 0) { // 创建模态框和背景 $backdrop = $('
').appendTo('body'); $modal = $(`
×

朝阳的工具 - 统一设置

功能开关

反馈QQ群:1035993374

`).appendTo($backdrop); // 绑定关闭事件 $modal.on('click', '.close-button', this.closeModal.bind(this)); $backdrop.on('click', (e) => { if ($(e.target).is($backdrop)) { // 点击背景关闭 this.closeModal(); } }); // 绑定清除所有数据按钮事件 $modal.on('click', '#' + SCRIPT_PREFIX + 'clear_all_data_btn', async () => { if (confirm('警告:确定要清除所有脚本的本地数据吗?这包括所有Token、按钮位置和设置。此操作不可逆!')) { await this.clearAllTampermonkeyStorage(); Util.notify('数据清除', '所有脚本数据已清除。请刷新页面。', 5000); window.location.reload(); } }); // 绑定功能开关事件 $modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_captcha_ocr', (e) => { globalSettings.enableCaptchaOcr = $(e.target).prop('checked'); GM_setValue(GM_SETTINGS_KEY, globalSettings); this.renderSettingsModules(); }); $modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_steam_entry', (e) => { globalSettings.enableSteamGameEntry = $(e.target).prop('checked'); GM_setValue(GM_SETTINGS_KEY, globalSettings); this.renderSettingsModules(); }); $modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_video_parser', (e) => { globalSettings.enableVideoParser = $(e.target).prop('checked'); GM_setValue(GM_SETTINGS_KEY, globalSettings); this.renderSettingsModules(); }); } // 加载最新全局设置并更新UI globalSettings = await GM_getValue(GM_SETTINGS_KEY, DEFAULT_GLOBAL_SETTINGS); $('#' + SCRIPT_PREFIX + 'toggle_captcha_ocr').prop('checked', globalSettings.enableCaptchaOcr); $('#' + SCRIPT_PREFIX + 'toggle_steam_entry').prop('checked', globalSettings.enableSteamGameEntry); $('#' + SCRIPT_PREFIX + 'toggle_video_parser').prop('checked', globalSettings.enableVideoParser); // 渲染各个功能模块的设置 this.renderSettingsModules(); $backdrop.css('display', 'flex'); $modal.addClass('show'); }, closeModal: function () { const $backdrop = $('#' + this.backdropId); const $modal = $('#' + this.modalId); $modal.removeClass('show'); setTimeout(() => { $backdrop.css('display', 'none'); }, 300); // Wait for transition to finish }, renderSettingsModules: async function () { // 验证码识别设置 const $captchaModule = $('#' + SCRIPT_PREFIX + 'captcha_ocr_settings_module'); if (globalSettings.enableCaptchaOcr) { $captchaModule.html(CaptchaOcr.getSettingsHtml()); CaptchaOcr.bindSettingsEvents($captchaModule); CaptchaOcr.loadSettingsToUI($captchaModule); } else { $captchaModule.empty(); } // Steam入库设置 const $steamModule = $('#' + SCRIPT_PREFIX + 'steam_entry_settings_module'); if (globalSettings.enableSteamGameEntry) { $steamModule.html(SteamGameEntry.getSettingsHtml()); SteamGameEntry.bindSettingsEvents($steamModule); await SteamGameEntry.loadSettings(); // 确保加载最新设置 SteamGameEntry.loadSettingsToUI($steamModule); } else { $steamModule.empty(); } // 视频解析设置 const $videoParserModule = $('#' + SCRIPT_PREFIX + 'video_parser_settings_module'); if (globalSettings.enableVideoParser) { $videoParserModule.html(VideoParser.getSettingsHtml()); VideoParser.bindSettingsEvents($videoParserModule); VideoParser.loadSettingsToUI($videoParserModule); } else { $videoParserModule.empty(); } }, clearAllTampermonkeyStorage: async function () { // 清除全局设置 await GM_deleteValue(GM_SETTINGS_KEY); globalSettings = { ...DEFAULT_GLOBAL_SETTINGS }; // 重置为默认值 // 清除Captcha OCR相关数据 await GM_deleteValue(CaptchaOcr.GM_API_KEY); await GM_deleteValue(CaptchaOcr.GM_SECRET_KEY); await GM_deleteValue(CaptchaOcr.GM_TOKEN_KEY); await GM_deleteValue(CaptchaOcr.GM_TOKEN_EXPIRY_KEY); await GM_deleteValue(CaptchaOcr.GM_RULES_KEY); await GM_deleteValue(CaptchaOcr.GM_OCR_SETTINGS_KEY); // 清除Steam Game Entry相关数据 await GM_deleteValue(SteamGameEntry.GM_GITHUB_TOKEN); await GM_deleteValue(SteamGameEntry.GM_BUTTON_POS_STEAM); await GM_deleteValue(SteamGameEntry.GM_BUTTON_POS_STEAMDB); await GM_deleteValue(SteamGameEntry.GM_SETTINGS_FIX_MANIFEST_VERSION); await GM_deleteValue(SteamGameEntry.GM_SETTINGS_DOWNLOAD_LUA_ONLY); await GM_deleteValue(SteamGameEntry.GM_SETTINGS_SELECTED_MANIFEST_REPO); // 清除Video Parser相关数据 await GM_deleteValue(VideoParser.GM_CUSTOM_API_LIST_KEY); await GM_deleteValue(VideoParser.GM_VIP_BOX_POSITION); console.log('Chaoyang Tool: All Tampermonkey storage cleared.'); } }; // --- 模块 1: 验证码识别 (Captcha OCR) --- const CaptchaOcr = { GM_API_KEY: SCRIPT_PREFIX + 'baidu_ocr_api_key', GM_SECRET_KEY: SCRIPT_PREFIX + 'baidu_ocr_secret_key', GM_TOKEN_KEY: SCRIPT_PREFIX + 'baidu_ocr_access_token', GM_TOKEN_EXPIRY_KEY: SCRIPT_PREFIX + 'baidu_ocr_token_expiry', GM_RULES_KEY: SCRIPT_PREFIX + 'baidu_ocr_custom_rules', GM_OCR_SETTINGS_KEY: SCRIPT_PREFIX + 'baidu_ocr_settings', BAIDU_TOKEN_URL: 'https://aip.baidubce.com/oauth/2.0/token', BAIDU_OCR_GENERAL_URL: 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic', ocrSettings: {}, tipContainer: null, manualSelectMode: false, selectedImageElement: null, isCaptchaProcessedThisLoad: false, observer: null, domChangeTimer: null, // 默认OCR设置 (移除了 warningTone) DEFAULT_OCR_SETTINGS: { "showHintCheck": true, }, init: function () { this.ocrSettings = GM_getValue(this.GM_OCR_SETTINGS_KEY, this.DEFAULT_OCR_SETTINGS); this.ensureDefaultOcrSettings(); this.tipContainer = this.createTipContainer(); // 注册Captcha OCR的Tampermonkey菜单命令 GM_registerMenuCommand('✨ 验证码识别 - 自动识别', () => this.triggerAutoRecognition()); GM_registerMenuCommand('👆 验证码识别 - 手动选择', () => this.enableManualSelectMode()); GM_registerMenuCommand('❌ 验证码识别 - 清除当前网站记忆', () => this.clearStoredRule()); if (globalSettings.enableCaptchaOcr) { $(document).ready(() => { this.attemptAutoRecognitionOnLoad(); this.setupMutationObserver(); }); } }, ensureDefaultOcrSettings: function () { let settingsUpdated = false; for (const key in this.DEFAULT_OCR_SETTINGS) { if (this.ocrSettings[key] === undefined) { this.ocrSettings[key] = this.DEFAULT_OCR_SETTINGS[key]; settingsUpdated = true; } } if (settingsUpdated) { GM_setValue(this.GM_OCR_SETTINGS_KEY, this.ocrSettings); } }, createTipContainer: function () { let $tipContainer = $('#baidu_ocr_tip_container'); if ($tipContainer.length === 0) { $tipContainer = $('
').appendTo('body'); } return $tipContainer; }, Hint: function (Content, Duration = 3000) { if (!this.ocrSettings.showHintCheck) { return; } if (typeof Content !== 'string' && Content && Content.Content) { Content = Content.Content; } // Ensure the tip is reset and then displayed this.tipContainer.stop(true, false).css({ top: '-5em', opacity: 0 }).removeClass('show'); this.tipContainer.html(Content + `X`); this.tipContainer.css('display', 'block').animate({ top: '10px', opacity: 1 }, 300, () => { this.tipContainer.addClass('show'); // Mark as shown if (Duration > 0) { this.tipContainer.delay(Duration).animate({ top: '-5em', opacity: 0 }, 500, () => { this.tipContainer.css('display', 'none').removeClass('show'); }); } }); }, hideHint: function () { this.tipContainer.stop(true, false).animate({ top: '-5em', opacity: 0 }, 300, () => { this.tipContainer.css('display', 'none').removeClass('show'); }); }, // --- Token Management --- async getToken() { const apiKey = GM_getValue(this.GM_API_KEY); const secretKey = GM_getValue(this.GM_SECRET_KEY); if (!apiKey || !secretKey) { this.Hint('请先通过设置界面设置百度AI的 API Key 和 Secret Key。', 8000); return null; } let token = GM_getValue(this.GM_TOKEN_KEY); let expiry = GM_getValue(this.GM_TOKEN_EXPIRY_KEY); if (token && expiry && Date.now() < expiry) { console.log('Baidu OCR: Using cached access token.'); return token; } console.log('Baidu OCR: Fetching new access token...'); try { const response = await this._fetchNewToken(apiKey, secretKey); if (response.access_token) { token = response.access_token; expiry = Date.now() + (response.expires_in * 1000) - (60 * 1000); // Set expiry a bit earlier GM_setValue(this.GM_TOKEN_KEY, token); GM_setValue(this.GM_TOKEN_EXPIRY_KEY, expiry); console.log('Baidu OCR: Access token fetched and cached successfully.'); return token; } else { throw new Error('Failed to get access_token: ' + (response.error_description || JSON.stringify(response))); } } catch (error) { console.error('Baidu OCR: Error getting access token:', error); this.Hint(`获取百度AI Access Token失败: ${error.message}
请检查API Key和Secret Key是否正确,或网络是否畅通。`, 8000); return null; } }, _fetchNewToken: function (apiKey, secretKey) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `${this.BAIDU_TOKEN_URL}?grant_type=client_credentials&client_id=${apiKey}&client_secret=${secretKey}`, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, onload: function (response) { try { const data = JSON.parse(response.responseText); resolve(data); } catch (e) { reject(new Error('Invalid JSON response: ' + response.responseText)); } }, onerror: function (error) { reject(new Error('Network or HTTP error: ' + JSON.stringify(error))); } }); }); }, // --- OCR Client --- async recognize(imageDataBase64) { const token = await this.getToken(); if (!token) return null; console.log('Baidu OCR: Sending image to API...'); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: `${this.BAIDU_OCR_GENERAL_URL}?access_token=${token}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: `image=${encodeURIComponent(imageDataBase64)}`, onload: function (response) { try { const data = JSON.parse(response.responseText); console.log('Baidu OCR API Response:', data); if (data.error_code) reject(new Error(`API Error ${data.error_code}: ${data.error_msg}`)); else resolve(data); } catch (e) { reject(new Error('Invalid JSON response from OCR API: ' + response.responseText)); } }, onerror: function (error) { reject(new Error('Network or HTTP error during OCR request: ' + JSON.stringify(error))); } }); }); }, // --- Image Processing --- getImageDataBase64: function (imgElement) { return new Promise((resolve, reject) => { if (!imgElement || !imgElement.tagName) return reject(new Error('Image element is null or undefined.')); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = imgElement.naturalWidth || imgElement.offsetWidth; canvas.height = imgElement.naturalHeight || imgElement.offsetHeight; if (canvas.width === 0 || canvas.height === 0) return reject(new Error('Image dimensions are 0. Cannot convert to Base64.')); try { ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL('image/png'); resolve(dataURL.split(',')[1]); } catch (error) { console.warn('Baidu OCR: Could not draw image to canvas (potential cross-origin or security issue):', error); let imgSrc = imgElement.src; if (imgSrc && imgSrc.startsWith('http') && !imgSrc.startsWith('data:') && !imgSrc.startsWith(window.location.origin)) { GM_xmlhttpRequest({ method: 'GET', url: imgSrc, responseType: 'blob', onload: (response) => { if (response.status === 200) { const reader = new FileReader(); reader.onloadend = function () { resolve(reader.result.split(',')[1]); }; reader.onerror = () => reject(new Error('FileReader error for cross-origin image.')); reader.readAsDataURL(response.response); } else { reject(new Error(`Failed to fetch cross-origin image: ${response.status} ${response.statusText}`)); } }, onerror: (err) => reject(new Error('GM_xmlhttpRequest error for cross-origin image: ' + JSON.stringify(err))) }); } else { reject(new Error('Could not convert image to Base64: ' + error.message)); } } }); }, // --- Element Selection Logic --- checkBadElemId: function (idStr) { if (!idStr) return false; return !idStr.match(/exifviewer-img-|\d+_\d+|-?\d{1,3}$|^react|^vue|^__\d+|^s\d+/i); }, Aimed: function (element) { if (!element || !(element instanceof Element)) return null; if (element.id && this.checkBadElemId(element.id)) { const selector = `#${element.id}`; if ($(selector).length === 1) return selector; } if (element.name && element.name.length > 0) { const selector = `${element.localName.toLowerCase()}[name="${element.name}"]`; if ($(selector).length === 1) return selector; } if (element.alt && element.alt.length > 0) { const selector = `${element.localName.toLowerCase()}[alt="${element.alt}"]`; if ($(selector).length === 1) return selector; } if (element.placeholder && element.placeholder.length > 0) { const selector = `${element.localName.toLowerCase()}[placeholder="${element.placeholder}"]`; if ($(selector).length === 1) return selector; } let classes = Array.from(element.classList).filter(cls => cls && !cls.match(/hover|active|focus|^\d+$/i)); if (classes.length > 0 && classes.join('.').length < 50) { const classSelector = `${element.localName.toLowerCase()}.${classes.join('.')}`; if ($(classSelector).length === 1) return classSelector; } return this.getElementCssPath(element); }, getElementCssPath: function (element) { if (!(element instanceof Element)) return null; const path = []; while (element && element.nodeType === Node.ELEMENT_NODE) { let selector = element.nodeName.toLowerCase(); if (element.id && this.checkBadElemId(element.id)) { selector += `#${element.id}`; path.unshift(selector); break; } const parent = element.parentElement; if (parent) { const siblings = Array.from(parent.children); const matchingSiblings = siblings.filter(e => e.nodeName.toLowerCase() === selector); if (matchingSiblings.length > 1) { const index = siblings.indexOf(element); selector += `:nth-child(${index + 1})`; } } path.unshift(selector); element = parent; } return path.join(' > '); }, // --- Automatic Recognition Logic --- findCaptchaAndInput: function () { const storedRule = this.getStoredRule(); if (storedRule) { const $img = $(storedRule.imgSelector); const $input = $(storedRule.inputSelector); if ($img.length && $input.length && $img.is(':visible') && $input.is(':visible')) { console.log('Baidu OCR: Found captcha using stored rule.'); return { img: $img[0], input: $input[0] }; } else { console.log('Baidu OCR: Stored rule elements not found or not visible.'); } } // If no stored rule, or stored rule is invalid, do not search further. console.log('Baidu OCR: No valid stored rule found for this page. Automatic search is disabled by design.'); return { img: null, input: null }; }, async processCaptcha(imgElement, inputElement, isManual = false) { if (this.isCaptchaProcessedThisLoad && !isManual) { console.log('Baidu OCR: Captcha already processed, skipping automatic attempt.'); return; } if (!imgElement) { if (isManual) this.Hint('未找到或未选择验证码图片。'); console.log('Baidu OCR: No image element to process.'); return; } const $imgElement = $(imgElement); const $inputElement = $(inputElement); const originalImgBorder = $imgElement.css('border'); const originalInputBorder = inputElement ? $inputElement.css('border') : ''; $imgElement.css('border', '2px solid red'); if (inputElement) $inputElement.css('border', '2px solid blue'); this.Hint('正在识别验证码...', 0); try { const imageDataBase64 = await this.getImageDataBase64(imgElement); if (!imageDataBase64) throw new Error('无法获取图片Base64数据。'); const response = await this.recognize(imageDataBase64); if (response && response.words_result && response.words_result.length > 0) { const recognizedText = response.words_result.map(word => word.words).join(''); console.log('Baidu OCR: Recognized raw text:', recognizedText); if (inputElement) { this.WriteImgCodeResult(recognizedText, inputElement); this.Hint(`验证码识别成功并已填写: ${recognizedText}`, 3000); this.isCaptchaProcessedThisLoad = true; if (this.observer) { this.observer.disconnect(); console.log('Baidu OCR: Captcha processed. Observer disconnected. Script is now silent for this page load.'); } if (isManual) { const imgSelector = this.Aimed(imgElement); const inputSelector = this.Aimed(inputElement); if (imgSelector && inputSelector) { this.storeRule(imgSelector, inputSelector); this.Hint(`验证码识别成功并已填写: ${recognizedText}
已记住本次选择!`, 5000); } else { console.warn('Baidu OCR: Could not generate robust CSS selectors for memory storage. Manual rule not saved.'); } } } else { this.Hint(`验证码识别成功: ${recognizedText}`, 3000); } } else { this.Hint('未能识别到验证码文字。', 3000); console.log('Baidu OCR: No words recognized.'); } } catch (error) { console.error('Baidu OCR: Error during recognition:', error); this.Hint(`识别验证码时发生错误: ${error.message}
请刷新验证码或手动选择。`, 5000); } finally { $imgElement.css('border', originalImgBorder); if (inputElement) $inputElement.css('border', originalInputBorder); if (!this.isCaptchaProcessedThisLoad) { this.hideHint(); } } }, // --- Event Simulation --- WriteImgCodeResult: function (ImgCodeResult, WriteInput) { const $WriteInput = $(WriteInput); let cleanedResult = ImgCodeResult.replace(/[\u4e00-\u9fa5\s]/g, ''); console.log(`Baidu OCR: Original recognized text: "${ImgCodeResult}", Cleaned text (no Chinese/spaces): "${cleanedResult}"`); $WriteInput.val(cleanedResult); const eventNames = ["input", "change", "focus", "invalid", "keypress", "keydown", "keyup", "blur"]; for (const eventName of eventNames) this.Fire($WriteInput[0], eventName); try { const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeInputValueSetter.call($WriteInput[0], cleanedResult); $WriteInput[0].dispatchEvent(new Event('input', { bubbles: true })); } catch (e) { /* ignore */ } }, Fire: function (element, eventName) { let event; if (typeof InputEvent === 'function' && eventName === 'input') event = new InputEvent(eventName, { bubbles: true, cancelable: true }); else if (typeof Event === 'function') event = new Event(eventName, { bubbles: true, cancelable: true }); else { event = document.createEvent("HTMLEvents"); event.initEvent(eventName, true, true); } element.dispatchEvent(event); }, // --- Rule Storage Management --- getStoredRules: function () { return GM_getValue(this.GM_RULES_KEY, {}); }, getStoredRule: function () { const rules = this.getStoredRules(); const origin = window.location.origin; return rules[origin]; }, storeRule: function (imgSelector, inputSelector) { const rules = this.getStoredRules(); const origin = window.location.origin; rules[origin] = { imgSelector, inputSelector, timestamp: Date.now() }; GM_setValue(this.GM_RULES_KEY, rules); console.log(`Baidu OCR: Stored rule for ${origin}:`, rules[origin]); }, clearStoredRule: function () { if (!confirm('确定要清除当前网站的验证码识别规则吗?这将使下次访问该网站时无法自动识别,需要重新手动设置。')) return; const rules = this.getStoredRules(); const origin = window.location.origin; delete rules[origin]; GM_setValue(this.GM_RULES_KEY, rules); this.Hint('已清除当前网站的记忆规则。', 3000); console.log(`Baidu OCR: Cleared rule for ${origin}.`); }, // --- Manual Selection Mode --- enableManualSelectMode: function () { if (this.manualSelectMode) { this.Hint('手动选择模式已开启。'); return; } this.manualSelectMode = true; this.selectedImageElement = null; this.Hint('手动选择模式:请点击验证码图片,然后点击对应的输入框。
右键点击图片可直接选择图片。', 0); const clickHandler = (e) => { if (!this.manualSelectMode) return; const target = e.target; if (target.tagName.toLowerCase() === 'img') { if (this.selectedImageElement) $(this.selectedImageElement).css('border', ''); this.selectedImageElement = target; $(this.selectedImageElement).css('border', '2px solid orange'); this.Hint('已选择图片。现在请点击对应的输入框。'); e.preventDefault(); } else if (target.tagName.toLowerCase() === 'input' || target.tagName.toLowerCase() === 'textarea') { if (this.selectedImageElement) { $(this.selectedImageElement).css('border', ''); $(target).css('border', '2px solid orange'); this.Hint('已选择输入框。开始识别...'); this.processCaptcha(this.selectedImageElement, target, true); this.disableManualSelectMode(); e.preventDefault(); } else { this.Hint('请先点击验证码图片。'); } } }; const contextMenuHandler = (e) => { if (!this.manualSelectMode) return; e.preventDefault(); const target = e.target; if (target.tagName.toLowerCase() === 'img') { if (this.selectedImageElement) $(this.selectedImageElement).css('border', ''); this.selectedImageElement = target; $(this.selectedImageElement).css('border', '2px solid orange'); this.Hint('已通过右键选择图片。现在请点击对应的输入框。'); } else { this.Hint('右键点击的不是图片。请左键点击验证码图片。'); } }; $(document).on('click.baiduocr', clickHandler); $(document).on('contextmenu.baiduocr', contextMenuHandler); }, disableManualSelectMode: function () { this.manualSelectMode = false; this.selectedImageElement = null; $(document).off('click.baiduocr'); $(document).off('contextmenu.baiduocr'); $('img[style*="border: 2px solid orange"]').css('border', ''); $('input[style*="border: 2px solid orange"]').css('border', ''); $('textarea[style*="border: 2px solid orange"]').css('border', ''); this.hideHint(); }, // --- Auto recognition on load/DOM change --- attemptAutoRecognitionOnLoad: async function () { setTimeout(async () => { if (this.isCaptchaProcessedThisLoad) return; const { img, input } = this.findCaptchaAndInput(); if (img && input) { console.log('Baidu OCR: Attempting automatic recognition on page load...'); await this.processCaptcha(img, input); } else { console.log('Baidu OCR: Automatic recognition skipped on page load (no clear captcha/input pair found).'); } }, 1000); }, setupMutationObserver: function () { if (this.observer) this.observer.disconnect(); this.observer = new MutationObserver((mutations) => { if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) return; let needsRecheck = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'IMG' || node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) { needsRecheck = true; break; } } } if (mutation.type === 'attributes' && (mutation.target.tagName === 'IMG' || mutation.target.tagName === 'INPUT' || mutation.target.tagName === 'TEXTAREA')) { if (mutation.attributeName === 'src' || mutation.attributeName === 'style' || mutation.attributeName === 'class') { needsRecheck = true; } } if (needsRecheck) break; } if (needsRecheck) { clearTimeout(this.domChangeTimer); this.domChangeTimer = setTimeout(async () => { if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) return; const { img, input } = this.findCaptchaAndInput(); if (img && input && !$(img).data('ocr_processed')) { $(img).data('ocr_processed', true); await this.processCaptcha(img, input); } }, 500); } }); this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'style', 'class'] }); }, triggerAutoRecognition: async function () { this.disableManualSelectMode(); const { img, input } = this.findCaptchaAndInput(); if (img) { this.Hint('尝试自动识别验证码...', 3000); await this.processCaptcha(img, input); } else { this.Hint('未找到已保存规则的验证码图片。请尝试手动选择以创建规则。', 5000); } }, // --- Settings UI for unified modal --- getSettingsHtml: function () { const apiKey = GM_getValue(this.GM_API_KEY, ''); const secretKey = GM_getValue(this.GM_SECRET_KEY, ''); return `

验证码识别设置

使用百度AI通用文字识别API自动识别验证码,需要填写API Key和Secret Key。

百度AI凭证

通用设置

`; }, bindSettingsEvents: function ($container) { $container.on('click', '#' + SCRIPT_PREFIX + 'save_baidu_credentials', async () => { const apiKey = $('#' + SCRIPT_PREFIX + 'baidu_api_key').val().trim(); const secretKey = $('#' + SCRIPT_PREFIX + 'baidu_secret_key').val().trim(); if (apiKey && secretKey) { GM_setValue(this.GM_API_KEY, apiKey); GM_setValue(this.GM_SECRET_KEY, secretKey); GM_deleteValue(this.GM_TOKEN_KEY); GM_deleteValue(this.GM_TOKEN_EXPIRY_KEY); Util.notify('验证码识别', 'API Key 和 Secret Key 已保存。Access Token 已失效,下次将自动获取新Token。', 5000); } else { Util.notify('验证码识别', 'API Key 和 Secret Key 不能为空!', 3000); } }); $container.on('click', '#' + SCRIPT_PREFIX + 'clear_baidu_credentials', async () => { if (confirm('确定要清除所有已保存的百度AI凭证吗?这将需要您重新输入凭证才能使用识别功能。')) { GM_deleteValue(this.GM_API_KEY); GM_deleteValue(this.GM_SECRET_KEY); GM_deleteValue(this.GM_TOKEN_KEY); GM_deleteValue(this.GM_TOKEN_EXPIRY_KEY); $('#' + SCRIPT_PREFIX + 'baidu_api_key').val(''); $('#' + SCRIPT_PREFIX + 'baidu_secret_key').val(''); Util.notify('验证码识别', '所有百度AI凭证已清除。', 5000); } }); $container.on('click', '#' + SCRIPT_PREFIX + 'save_ocr_general_settings', () => { this.ocrSettings.showHintCheck = $('#' + SCRIPT_PREFIX + 'setting_showHintCheck').prop('checked'); GM_setValue(this.GM_OCR_SETTINGS_KEY, this.ocrSettings); Util.notify('验证码识别', '通用设置已保存。', 3000); }); }, loadSettingsToUI: function ($container) { this.ocrSettings = GM_getValue(this.GM_OCR_SETTINGS_KEY, this.DEFAULT_OCR_SETTINGS); $container.find('#' + SCRIPT_PREFIX + 'setting_showHintCheck').prop('checked', this.ocrSettings.showHintCheck); } }; // --- 模块 2: Steam游戏入库 (Steam Game Entry) --- const SteamGameEntry = { GM_GITHUB_TOKEN: SCRIPT_PREFIX + 'github_token', GM_BUTTON_POS_STEAM: SCRIPT_PREFIX + 'button_pos_steam', GM_BUTTON_POS_STEAMDB: SCRIPT_PREFIX + 'button_pos_steamdb', GM_SETTINGS_FIX_MANIFEST_VERSION: SCRIPT_PREFIX + 'setting_fix_manifest_version', GM_SETTINGS_DOWNLOAD_LUA_ONLY: SCRIPT_PREFIX + 'setting_download_lua_only', GM_SETTINGS_SELECTED_MANIFEST_REPO: SCRIPT_PREFIX + 'setting_selected_manifest_repo', CORE_GITHUB_MANIFEST_REPOS: [ { "name": "清单网站1", "url": "SteamAutoCracks/ManifestHub", "type": "branches" }, { "name": "清单网站2", "url": "hansaes/ManifestAutoUpdate", "type": "branches" } ], RETRY_LIMIT: 3, RETRY_DELAY_MS: 2000, DRAG_THRESHOLD: 5, APP_ID_REGEX_STEAM: /\/app\/(\d+)/, APP_ID_REGEX_STEAMDB: /\/app\/(\d+)/, APP_ID_REGEX_CLIPBOARD: /^\d{1,10}$/, githubToken: '', storedButtonPosition: null, setting_fixManifestVersion: true, setting_downloadLuaOnly: true, setting_selectedManifestRepo: '', currentAppId: null, clipboardAppId: null, activeAppId: null, // UI elements storageBtn: null, statusBox: null, refreshStatusBtn: null, // 【修复】将 init 函数改为 async,以正确处理异步的设置加载 init: async function () { if (!globalSettings.enableSteamGameEntry) return; const isSteamPage = window.location.href.match(/https:\/\/(store\.steampowered\.com\/app\/|steamdb\.info\/app\/|steamui\.com\/)/); if (!isSteamPage) return; // 【修复】使用 await 等待设置加载完成,确保后续操作使用正确的设置值 await this.loadSettings(); this.createUI(); this.currentAppId = this.getAppIdFromUrl(); document.addEventListener('paste', this.handlePasteEvent.bind(this)); // Initial update active App ID if (this.currentAppId) { await this.updateActiveAppId(this.currentAppId, 'url'); } else { Util.readClipboard().then(async text => { if (text && this.APP_ID_REGEX_CLIPBOARD.test(text.trim())) { const id = text.trim(); if (id !== this.activeAppId) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${id},是否以此App ID进行操作?`)) { this.clipboardAppId = id; await this.updateActiveAppId(this.clipboardAppId, 'clipboard'); } } } }); } }, async loadSettings() { this.githubToken = await GM_getValue(this.GM_GITHUB_TOKEN, ''); this.setting_fixManifestVersion = await GM_getValue(this.GM_SETTINGS_FIX_MANIFEST_VERSION, true); this.setting_downloadLuaOnly = await GM_getValue(this.GM_SETTINGS_DOWNLOAD_LUA_ONLY, true); this.setting_selectedManifestRepo = await GM_getValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.CORE_GITHUB_MANIFEST_REPOS[0].name); // 确保所选仓库在列表中,如果不存在则重置为第一个 if (!this.CORE_GITHUB_MANIFEST_REPOS.some(repo => repo.name === this.setting_selectedManifestRepo)) { this.setting_selectedManifestRepo = this.CORE_GITHUB_MANIFEST_REPOS[0].name; await GM_setValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.setting_selectedManifestRepo); } const positionKey = window.location.hostname === 'steamdb.info' ? this.GM_BUTTON_POS_STEAMDB : this.GM_BUTTON_POS_STEAM; this.storedButtonPosition = await GM_getValue(positionKey, null); console.log('Steam Entry: Settings loaded.', { githubToken: this.githubToken ? 'Set' : 'Not Set', fixManifestVersion: this.setting_fixManifestVersion, downloadLuaOnly: this.setting_downloadLuaOnly, selectedManifestRepo: this.setting_selectedManifestRepo, storedButtonPosition: this.storedButtonPosition }); }, createUI: function () { this.storageBtn = $('').appendTo('body')[0]; this.statusBox = $('
加载中
').appendTo('body')[0]; this.refreshStatusBtn = $('').appendTo('body')[0]; // Attach companion elements to the main button's jQuery data for easier management by makeDraggable $(this.storageBtn).data('companionElements', [$(this.statusBox), $(this.refreshStatusBtn)]); $(this.storageBtn).data('updateCompanionPositions', this.updateCompanionButtonPositions.bind(this)); const positionKey = window.location.hostname === 'steamdb.info' ? this.GM_BUTTON_POS_STEAMDB : this.GM_BUTTON_POS_STEAM; const defaultButtonTop = window.location.hostname === 'steamdb.info' ? 120 : 20; // SteamDB often has header elements Util.applyButtonPosition(this.storageBtn, this.storedButtonPosition, defaultButtonTop); this.updateCompanionButtonPositions(); // Position companion elements relative to the main button // Event Listeners // Use the unified makeDraggable Util.makeDraggable(this.storageBtn, positionKey, () => { this.downloadManifest(this.activeAppId, this.setting_selectedManifestRepo); // Click action }); $(this.refreshStatusBtn).on('click', () => this.updateStatusBox()); }, // Helper to update positions of companion elements updateCompanionButtonPositions: function () { if (!this.storageBtn || !this.statusBox || !this.refreshStatusBtn) return; const btnRect = this.storageBtn.getBoundingClientRect(); const spacing = 10; // Position status box to the left of the main button this.statusBox.style.left = `${btnRect.left - this.statusBox.offsetWidth - spacing}px`; this.statusBox.style.top = `${btnRect.top}px`; this.statusBox.style.right = 'auto'; // Ensure right is auto // Position refresh button below the status box, also to the left of main button this.refreshStatusBtn.style.left = `${btnRect.left - this.refreshStatusBtn.offsetWidth - spacing}px`; this.refreshStatusBtn.style.top = `${btnRect.top + this.statusBox.offsetHeight + 5}px`; this.refreshStatusBtn.style.right = 'auto'; // Ensure right is auto }, // --- Core Functions --- getAppIdFromUrl: function () { const url = window.location.href; let match; if (url.startsWith('https://store.steampowered.com/app/')) { match = url.match(this.APP_ID_REGEX_STEAM); } else if (url.startsWith('https://steamdb.info/app/')) { match = url.match(this.APP_ID_REGEX_STEAMDB); } else if (url.startsWith('https://steamui.com/')) { // SteamUI match match = url.match(this.APP_ID_REGEX_STEAM); // Reuse Steam Regex } return match ? String(match[1]) : null; }, updateActiveAppId: async function (newAppId, source) { if (this.activeAppId === newAppId && source !== 'url') return; this.activeAppId = newAppId; console.log(`Steam Entry: Active App ID updated to ${this.activeAppId || 'N/A'} (Source: ${source})`); if (this.storageBtn) { // Main button text simplified as it's a direct download button this.storageBtn.textContent = '入库'; // 总是显示“入库” this.storageBtn.disabled = !this.activeAppId; } await this.updateStatusBox(); }, handlePasteEvent: async function (event) { const pastedText = event.clipboardData ? event.clipboardData.getData('text') : null; if (pastedText && this.APP_ID_REGEX_CLIPBOARD.test(pastedText.trim())) { const newClipboardAppId = pastedText.trim(); if (newClipboardAppId !== this.activeAppId) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${newClipboardAppId},是否以此App ID进行操作?`)) { this.clipboardAppId = newClipboardAppId; await this.updateActiveAppId(this.clipboardAppId, 'clipboard'); Util.notify('朝阳的工具', `已识别剪贴板App ID: ${this.clipboardAppId}`, 3000); } } else { console.log(`Steam Entry: Clipboard App ID ${newClipboardAppId} is same as active, skipping.`); } } }, // --- GitHub API Calls --- async makeGitHubApiRequestWithRetry(url, repoName, method = 'GET', retries = 0) { const headers = { 'Accept': 'application/vnd.github.v3+json', 'X-GitHub-Api-Version': '2022-11-28' }; if (this.githubToken) { headers['Authorization'] = `token ${this.githubToken}`; } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: headers, timeout: 10000, onload: async (res) => { const remaining = res.responseHeaders.match(/x-ratelimit-remaining:\s*(\d+)/i); const reset = res.responseHeaders.match(/x-ratelimit-reset:\s*(\d+)/i); if (remaining && reset) { const resetTime = new Date(parseInt(reset[1]) * 1000); if (parseInt(remaining[1]) < 100) { console.warn(`Steam Entry: [${repoName}] Rate limit: ${remaining[1]} remaining, resets ${resetTime.toLocaleTimeString()}`); if (parseInt(remaining[1]) === 0) Util.notify('朝阳的工具: 速率限制警告', `您的GitHub API请求已达到速率限制,将在 ${resetTime.toLocaleTimeString()} 后重置。请检查您的Token设置。`, 8000); } } if (res.status === 200) resolve({ data: (method === 'HEAD' ? null : res.responseText), headers: res.responseHeaders, status: res.status }); else if (res.status === 403 || res.status === 429 || res.status >= 500) { if (retries < this.RETRY_LIMIT) { console.warn(`Steam Entry: [${repoName}] Request failed, status ${res.status} (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`); await Util.sleep(this.RETRY_DELAY_MS * (retries + 1)); this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`Steam Entry: [${repoName}] Request failed, status ${res.status} (max retries). URL: ${url}`); Util.notify('朝阳的工具: 请求失败', `[${repoName}] GitHub API请求失败,状态码 ${res.status} (已达最大重试次数)。请检查网络或Token。`, 5000); reject(new Error(`Failed to fetch: ${res.status} (Max retries exceeded)`)); } } else if (res.status === 404) resolve({ data: null, headers: res.responseHeaders, status: res.status }); else { console.error(`Steam Entry: [${repoName}] Request failed, status: ${res.status}, URL: ${url}`); Util.notify('朝阳的工具: 请求失败', `[${repoName}] GitHub API请求失败,状态码: ${res.status}。`, 5000); reject(new Error(`Failed to fetch: ${res.status}`)); } }, onerror: async (err) => { if (retries < this.RETRY_LIMIT) { console.warn(`Steam Entry: [${repoName}] Request error (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`, err); await Util.sleep(this.RETRY_DELAY_MS * (retries + 1)); this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`Steam Entry: [${repoName}] Request error (max retries). URL: ${url}`, err); Util.notify('朝阳的工具: 网络错误', `[${repoName}] GitHub API请求发生网络错误。`, 5000); reject(new Error('Request error (Max retries exceeded): ' + JSON.stringify(err))); } }, ontimeout: async () => { if (retries < this.RETRY_LIMIT) { console.warn(`Steam Entry: [${repoName}] Request timeout (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`); await Util.sleep(this.RETRY_DELAY_MS * (retries + 1)); this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject); } else { console.error(`Steam Entry: [${repoName}] Request timeout (max retries). URL: ${url}`); Util.notify('朝阳的工具: 请求超时', `[${repoName}] GitHub API请求超时。`, 5000); reject(new Error('Request Timeout (Max retries exceeded)')); } } }); }); }, async getAppIdManifestInfo(appId) { const targetAppId = String(appId); const results = {}; let latestSource = null; const promises = this.CORE_GITHUB_MANIFEST_REPOS.map(async (repoInfo) => { const ownerRepoParts = repoInfo.url.split('/'); const owner = ownerRepoParts[0]; const repo = ownerRepoParts[1]; const repoName = repoInfo.name; let repoStatus = -1; try { const branchCheckUrl = `https://api.github.com/repos/${owner}/${repo}/branches/${targetAppId}`; const response = await this.makeGitHubApiRequestWithRetry(branchCheckUrl, repoName, 'HEAD'); if (response.status === 200) { repoStatus = 1; if (!latestSource) latestSource = repoName; console.log(`Steam Entry: App ID ${targetAppId} in ${repoName} exists.`); } else if (response.status === 404) { repoStatus = 0; console.log(`Steam Entry: App ID ${targetAppId} in ${repoName} does not exist (404).`); } else { console.warn(`Steam Entry: App ID ${targetAppId} in ${repoName} check returned unexpected status: ${response.status}`); repoStatus = -1; } } catch (error) { console.error(`Steam Entry: Failed to check App ID ${targetAppId} in ${repoName}:`, error); repoStatus = -1; } return { repoName, status: repoStatus }; }); const allResults = await Promise.all(promises); for (const res of allResults) { results[res.repoName] = res.status; } return { manifests: results, latestSource: latestSource }; }, async downloadManifest(appId, sourceOption) { if (!appId) { Util.notify('朝阳的工具', '未指定App ID。', 3000); return; } const repoToDownload = this.CORE_GITHUB_MANIFEST_REPOS.find(r => r.name === sourceOption); if (!repoToDownload) { Util.notify('朝阳的工具', `无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}。`, 5000); console.error(`Steam Entry: Cannot find download link for App ID ${appId} or source: ${sourceOption}`); return; } const ownerRepo = repoToDownload.url.split('/'); let downloadUrl = ''; let fileName = ''; let responseType = ''; let processContent = null; if (this.setting_downloadLuaOnly) { downloadUrl = `https://raw.githubusercontent.com/${ownerRepo[0]}/${ownerRepo[1]}/${String(appId)}/${String(appId)}.lua`; fileName = `${appId}.lua`; responseType = 'text'; processContent = (content) => { // 【修正】使用 `this.setting_fixManifestVersion` 来判断,而不是取反 if (!this.setting_fixManifestVersion) { console.log(`Steam Entry: Commenting out setManifestid.`); return content.replace(/setManifestid/g, '--setManifestid'); } console.log(`Steam Entry: Not commenting out setManifestid.`); return content; }; } else { downloadUrl = `https://github.com/${ownerRepo[0]}/${ownerRepo[1]}/archive/refs/heads/${String(appId)}.zip`; fileName = `${appId}.zip`; responseType = 'arraybuffer'; processContent = (content) => content; } console.log(`Steam Entry: Preparing to download App ID ${appId} from ${sourceOption}. URL: ${downloadUrl}`); Util.notify('朝阳的工具', `正在从 ${sourceOption} 下载 ${fileName}...`, 3000); GM_xmlhttpRequest({ method: 'GET', url: downloadUrl, responseType: responseType, onload: (response) => { if (response.status === 200) { let fileContent = response.response; if (this.setting_downloadLuaOnly && responseType === 'text') { fileContent = processContent(fileContent); } console.log(`Steam Entry: File content fetched, preparing to download.`); const blob = new Blob([fileContent], { type: this.setting_downloadLuaOnly ? 'text/plain;charset=utf-8' : 'application/zip' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); Util.notify('朝阳的工具', `App ID ${appId} 的${this.setting_downloadLuaOnly ? 'Lua文件' : 'ZIP文件'}下载成功!`, 5000); } else if (response.status === 404) { console.error(`Steam Entry: Download App ID ${appId} failed, file not found (404 Not Found). URL: ${downloadUrl}`); Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件失败,指定清单中不存在该App ID的${this.setting_downloadLuaOnly ? 'Lua文件' : 'ZIP分支'}。`, 5000); } else { console.error(`Steam Entry: Download App ID ${appId} failed, status: ${response.status}. URL: ${downloadUrl}`); Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件失败,请检查网络或文件是否存在 (HTTP ${response.status})。`, 5000); } }, onerror: (error) => { console.error(`Steam Entry: Error downloading App ID ${appId}:`, error); Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件时发生网络错误。`, 5000); } }); }, async updateStatusBox() { if (!this.statusBox) return; if (!this.activeAppId) { this.statusBox.textContent = 'N/A'; this.statusBox.style.backgroundColor = '#6c757d'; return; } this.statusBox.textContent = '检查中'; this.statusBox.style.backgroundColor = '#ffc107'; const { latestSource } = await this.getAppIdManifestInfo(this.activeAppId); if (latestSource) { this.statusBox.textContent = '存在'; this.statusBox.style.backgroundColor = '#4CAF50'; } else { this.statusBox.textContent = '没有'; this.statusBox.style.backgroundColor = '#f44336'; } }, async fetchAndDownloadDlcIds() { if (!this.activeAppId) { Util.notify('朝阳的工具', '无法获取当前游戏App ID。请在Steam或SteamDB游戏详情页使用此功能,或通过剪贴板识别。', 5000); return; } console.log('Steam Entry: Fetching DLC IDs...'); const dlcIds = new Set(); const hostname = window.location.hostname; if (hostname === 'steamdb.info') { document.querySelectorAll('#dlc tr[data-appid]').forEach(row => { const dlcId = row.getAttribute('data-appid'); if (dlcId) dlcIds.add(dlcId.trim()); }); } else if (hostname === 'store.steampowered.com') { document.querySelectorAll('.game_area_dlc_row').forEach(row => { const dlcId = row.getAttribute('data-ds-appid'); if (dlcId) dlcIds.add(dlcId.trim()); else { const link = row.querySelector('a'); if (link && link.href) { const match = link.href.match(/\/app\/(\d+)/); if (match && match[1]) dlcIds.add(match[1]); } } }); } else if (hostname === 'steamui.com') { const potentialDlcElements = document.querySelectorAll('[data-appid-dlc], .dlc-list-item a, .dlc-id-text'); potentialDlcElements.forEach(el => { let dlcId = el.getAttribute('data-appid-dlc') || el.textContent.trim(); const linkMatch = el.href ? el.href.match(/\/app\/(\d+)/) : null; if (linkMatch && linkMatch[1]) dlcId = linkMatch[1]; if (dlcId && this.APP_ID_REGEX_CLIPBOARD.test(dlcId)) dlcIds.add(dlcId); }); if (dlcIds.size === 0) { Util.notify('朝阳的工具', '在 steamui.com 页面未找到DLC信息。请确保您当前查看的是有DLC列表的页面。该站点的DLC识别功能依赖于页面结构,可能不够完善。', 8000); return; } } else { Util.notify('朝阳的工具', '当前页面不支持自动获取DLC ID。请在Steam商店、SteamDB或SteamUI页面使用此功能。', 5000); return; } const dlcIdArray = Array.from(dlcIds); console.log(`Steam Entry: Found ${dlcIdArray.length} unique DLC IDs.`); if (dlcIdArray.length === 0) { Util.notify('朝阳的工具', '未在此页面上找到任何DLC。', 3000); return; } const fileContent = dlcIdArray.join('\n'); const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `dlc_${this.activeAppId}.txt`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); Util.notify('朝阳的工具', `成功获取并下载了 ${dlcIdArray.length} 个DLC ID。`, 5000); }, // --- Settings UI for unified modal --- getSettingsHtml: function () { let repoOptionsHtml = this.CORE_GITHUB_MANIFEST_REPOS.map(repo => ` `).join(''); return `

Steam游戏入库设置

提供清单文件下载,需要GitHub Personal Access Token以获得更高的API请求额度,避免下载失败。

GitHub Token 管理

清单下载选项

选择下载源

${repoOptionsHtml}
`; }, bindSettingsEvents: function ($container) { $container.on('click', '#' + SCRIPT_PREFIX + 'steam_save_github_token', async () => { const tokenValue = $('#' + SCRIPT_PREFIX + 'steam_github_token_input').val().trim(); this.githubToken = tokenValue; await GM_setValue(this.GM_GITHUB_TOKEN, this.githubToken); this.loadSettingsToUI($container); Util.notify('Steam入库', 'GitHub Token已保存。', 3000); }); $container.on('click', '#' + SCRIPT_PREFIX + 'steam_clear_github_token', async () => { if (confirm('确定要清除您的GitHub Token吗?这将可能导致API速率限制。')) { await GM_deleteValue(this.GM_GITHUB_TOKEN); this.githubToken = ''; this.loadSettingsToUI($container); Util.notify('Steam入库', 'GitHub Token已清除。', 3000); } }); $container.on('change', '#' + SCRIPT_PREFIX + 'steam_setting_fixManifestVersion', async (e) => { this.setting_fixManifestVersion = $(e.target).prop('checked'); await GM_setValue(this.GM_SETTINGS_FIX_MANIFEST_VERSION, this.setting_fixManifestVersion); Util.notify('Steam入库', '固定清单版本设置已保存。', 2000); }); $container.on('change', '#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly', async (e) => { this.setting_downloadLuaOnly = $(e.target).prop('checked'); await GM_setValue(this.GM_SETTINGS_DOWNLOAD_LUA_ONLY, this.setting_downloadLuaOnly); Util.notify('Steam入库', '下载文件类型设置已保存。', 2000); }); $container.on('change', `input[name="${SCRIPT_PREFIX}steam_download_option"]`, async (e) => { this.setting_selectedManifestRepo = $(e.target).val(); await GM_setValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.setting_selectedManifestRepo); Util.notify('Steam入库', `默认下载源已更新为: ${this.setting_selectedManifestRepo}`, 2000); }); $container.on('click', '#' + SCRIPT_PREFIX + 'steam_fetch_dlc_btn', this.fetchAndDownloadDlcIds.bind(this)); }, loadSettingsToUI: function ($container) { $container.find('#' + SCRIPT_PREFIX + 'steam_github_token_input').val(this.githubToken); $container.find('#' + SCRIPT_PREFIX + 'steam_current_token_display').text(this.githubToken ? '已设置' : '未设置').css('color', this.githubToken ? '#4CAF50' : '#f44336'); $container.find('#' + SCRIPT_PREFIX + 'steam_setting_fixManifestVersion').prop('checked', this.setting_fixManifestVersion); $container.find('#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly').prop('checked', this.setting_downloadLuaOnly); $container.find(`input[name="${SCRIPT_PREFIX}steam_download_option"][value="${this.setting_selectedManifestRepo}"]`).prop('checked', true); } }; // --- 模块 3: 视频解析接口 (Video Parser) --- const VideoParser = { GM_CUSTOM_API_LIST_KEY: SCRIPT_PREFIX + 'video_parser_custom_api_list', GM_VIP_BOX_POSITION: SCRIPT_PREFIX + 'vip_box_position', DEFAULT_VIP_BOX_POSITION: { "left": null, "top": 200, "right": 20 }, // 默认位置 VIDEO_PARSE_LIST: [ { "name": "HLS解析", "url": "https://jx.hls.one/?url=" }, { "name": "冰豆解析", "url": "https://bd.jx.cn/?url=" }, { "name": "虾米视频", "url": "https://jx.xmflv.com/?url=" }, { "name": "UTO解析", "url": "https://jx.nnsvip.cn/?url=" }, { "name": "CK解析", "url": "https://www.ckplayer.vip/jiexi/?url=" }, { "name": "七哥解析", "url": "https://jx.nnxv.cn/tv.php?url=" }, { "name": "夜幕解析", "url": "https://www.yemu.xyz/?url=" }, { "name": "盘古解析", "url": "https://www.pangujiexi.com/jiexi/?url=" }, { "name": "8090解析", "url": "https://www.8090g.cn/?url=" }, { "name": "playm3u8", "url": "https://www.playm3u8.cn/jiexi.php?url=" }, { "name": "七七云解析", "url": "https://jx.77flv.cc/?url=" }, { "name": "极速解析", "url": "https://jx.2s0.cn/player/?url=" }, { "name": "Player-JY", "url": "https://jx.playerjy.com/?url=" }, ], customApiList: [], mergedApiList: [], // 浮动按钮和列表的DOM元素 entryBtn: null, listModal: null, // 匹配的视频网站列表 (从原脚本复制,并统一为字符串匹配) MATCHED_VIDEO_HOSTS: [ 'v.qq.com', 'm.v.qq.com', 'iqiyi.com', 'm.iqiyi.com', 'iq.com', 'youku.com', 'm.youku.com', 'mgtv.com', 'm.mgtv.com', 'tudou.com', 'film.sohu.com', 'tv.sohu.com', 'm.tv.sohu.com', 'm.film.sohu.com', 'bilibili.com', 'm.bilibili.com', 'v.pptv.com', 'vip.pptv.com', 'm.pptv.com', 'wasu.cn', 'le.com', 'acfun.cn', '1905.com' ], init: function () { if (!globalSettings.enableVideoParser) return; // 检查当前页面是否匹配视频解析的规则 const currentHostname = Util.getHostname(window.location.href); const isMatchedVideoPage = this.MATCHED_VIDEO_HOSTS.some(host => currentHostname.includes(host)); if (!isMatchedVideoPage) { console.log("Video Parser: Current page is not a matched video page, skipping UI creation."); return; } this.loadCustomApis(); this.mergedApiList = [...this.VIDEO_PARSE_LIST, ...this.customApiList.map(api => ({ ...api, custom: true }))]; // 创建浮动入口按钮和解析列表模态框 this.createEntryButton(); this.createParserListModal(); }, loadCustomApis: function () { this.customApiList = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []); }, createEntryButton: function () { this.entryBtn = $(`
`).appendTo('body')[0]; const defaultTop = 200; const defaultRight = 20; const savedPos = GM_getValue(this.GM_VIP_BOX_POSITION, this.DEFAULT_VIP_BOX_POSITION); Util.applyButtonPosition(this.entryBtn, savedPos, defaultTop, defaultRight); // Use the unified makeDraggable Util.makeDraggable(this.entryBtn, this.GM_VIP_BOX_POSITION, this.toggleListModal.bind(this)); }, createParserListModal: function () { this.listModal = $(`
×

视频解析接口

`).appendTo('body')[0]; this.updateParserListUI(); $(this.listModal).on('click', '.close-button', this.closeListModal.bind(this)); }, toggleListModal: function () { if ($(this.listModal).css('display') === 'none') { this.updateParserListUI(); // 每次打开都刷新列表,确保自定义接口最新 $(this.listModal).css('display', 'block'); } else { $(this.listModal).css('display', 'none'); } }, closeListModal: function () { $(this.listModal).css('display', 'none'); }, updateParserListUI: function () { this.loadCustomApis(); // Ensure custom APIs are fresh this.mergedApiList = [...this.VIDEO_PARSE_LIST, ...this.customApiList.map(api => ({ ...api, custom: true }))]; const $ul = $(`#${SCRIPT_PREFIX}parser_list_ul`); $ul.empty(); this.mergedApiList.forEach((item, index) => { const $li = $(`
  • ${item.name}
  • `); $li.on('click', () => this.openParserUrl(item)); $ul.append($li); }); }, openParserUrl: function (videoObj) { let url = videoObj.url + encodeURIComponent(window.location.href); Util.openTab(url); this.closeListModal(); }, // --- Settings UI for unified modal --- getSettingsHtml: function () { this.loadCustomApis(); let customApiListHtml = this.customApiList.map(api => `
  • ${api.name} X
  • `).join(''); return `

    视频解析接口设置

    管理视频解析的自定义接口,并提供一个测试解析的入口。

    添加自定义接口

    我的自定义接口

    `; }, bindSettingsEvents: function ($container) { $container.on('click', '#' + SCRIPT_PREFIX + 'add_custom_api_btn', () => { const name = $('#' + SCRIPT_PREFIX + 'custom_api_name').val().trim(); const url = $('#' + SCRIPT_PREFIX + 'custom_api_url').val().trim(); if (!name || !url) { Util.notify('视频解析', '接口名称和地址均不能为空!', 3000); return; } if (!url.includes('?url=')) { if (!confirm('接口地址通常包含 "?url=",您确定要添加吗?')) { return; } } const newApi = { name: name, url: url }; let customApis = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []); if (customApis.some(api => api.url === url)) { Util.notify('视频解析', '该接口地址已存在!', 3000); return; } customApis.push(newApi); GM_setValue(this.GM_CUSTOM_API_LIST_KEY, customApis); Util.notify('视频解析', '接口添加成功!', 3000); SettingsManager.renderSettingsModules(); // 刷新设置模块UI }); $container.on('click', '#' + SCRIPT_PREFIX + 'custom_api_list .delete-api-btn', function () { if (!confirm('您确定要删除这个自定义接口吗?')) return; const urlToDelete = $(this).data('url'); let customApis = GM_getValue(VideoParser.GM_CUSTOM_API_LIST_KEY, []); customApis = customApis.filter(api => api.url !== urlToDelete); GM_setValue(VideoParser.GM_CUSTOM_API_LIST_KEY, customApis); Util.notify('视频解析', '接口删除成功!', 3000); SettingsManager.renderSettingsModules(); // 刷新设置模块UI }); $container.on('click', '#' + SCRIPT_PREFIX + 'open_parser_entry_btn', () => { SettingsManager.closeModal(); // 关闭设置模态框 if (this.entryBtn) { this.toggleListModal(); // 打开或关闭解析列表 } else { Util.notify('视频解析', '视频解析入口按钮未显示,请确认当前页面为支持的视频网站。', 5000); } }); }, loadSettingsToUI: function ($container) { this.loadCustomApis(); let customApiListHtml = this.customApiList.map(api => `
  • ${api.name} X
  • `).join(''); $container.find('#' + SCRIPT_PREFIX + 'custom_api_list').html(customApiListHtml); $container.find('#' + SCRIPT_PREFIX + 'custom_api_name').val(''); $container.find('#' + SCRIPT_PREFIX + 'custom_api_url').val(''); } }; // --- 主入口:脚本初始化 --- $(document).ready(async function () { // 1. 加载全局设置 globalSettings = await GM_getValue(GM_SETTINGS_KEY, DEFAULT_GLOBAL_SETTINGS); console.log('朝阳的工具: Global settings loaded:', globalSettings); // 2. 初始化统一设置管理器 (注册Tampermonkey菜单命令) SettingsManager.init(); // 3. 根据全局设置初始化各个模块 if (globalSettings.enableCaptchaOcr) { CaptchaOcr.init(); console.log('朝阳的工具: Captcha OCR module initialized.'); } else { console.log('朝阳的工具: Captcha OCR module disabled by settings.'); } if (globalSettings.enableSteamGameEntry) { // 【修复】使用 await 等待 Steam 模块初始化完成 await SteamGameEntry.init(); console.log('朝阳的工具: Steam Game Entry module initialized.'); } else { console.log('朝阳的工具: Steam Game Entry module disabled by settings.'); } if (globalSettings.enableVideoParser) { VideoParser.init(); console.log('朝阳的工具: Video Parser module initialized.'); } else { console.log('朝阳的工具: Video Parser module disabled by settings.'); } console.log('朝阳的工具: All modules initialized based on settings.'); }); })();