// ==UserScript== // @name 朝阳的工具 - 统一多功能脚本 // @version 5.1 // @description 简化 // @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 cdn.jsdelivr.net // @connect jx.hls.one // @connect bd.jx.cn // @connect jx.xmflv.com // @connect jx.nnsvip.cn // @connect www.ckplayer.vip // @connect jx.nnxv.cn // @connect www.yemu.xyz // @connect www.pangujiexi.com // @connect www.8090g.cn // @connect www.playm3u8.cn // @connect jx.77flv.cc // @connect jx.2s0.cn // @connect jx.playerjy.com // @require https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js // @run-at document-start // @license GPL License // ==/UserScript== (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 }; GM_addStyle(` :root { --cy-bg-glass: rgba(20, 20, 20, 0.88); --cy-border: rgba(255, 255, 255, 0.1); --cy-text-main: #FAF9F6; --cy-text-sub: #aaa; --cy-primary: #3B6B85; --cy-primary-hover: #2F576B; --cy-danger: #A35C4C; --cy-danger-hover: #8C4F41; --cy-module-bg: rgba(255, 255, 255, 0.05); --cy-shadow: 0 20px 50px rgba(0,0,0,0.8); } .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; backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px); } .chaoyang-modal { background-color: var(--cy-bg-glass); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px); border: 1px solid var(--cy-border); border-radius: 12px; box-shadow: var(--cy-shadow); padding: 30px; width: 700px; max-width: 90%; max-height: 90%; overflow-y: auto; color: var(--cy-text-main); 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 { color: var(--cy-primary); 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: #FAF9F6; border-bottom: 1px solid #555; padding-bottom: 10px; margin: 25px 0 20px 0; text-align: center; font-weight: bold; width: 100%; display: block; } .chaoyang-modal h3.cy-settings-header { font-size: 15px !important; } .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; line-height: 1; } .chaoyang-modal .close-button:hover { color: white; transform: rotate(90deg); } .chaoyang-feature-module { margin-bottom: 20px; padding: 20px; background-color: var(--cy-module-bg); border-radius: 8px; border: 1px solid #444; box-shadow: inset 0 0 5px rgba(0,0,0,0.2); } .chaoyang-feature-module h4 { color: var(--cy-primary); margin: 0 0 15px 0; font-size: 16px; font-weight: bold; } .chaoyang-switch-label { display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px; font-size: 16px; color: #FAF9F6; font-weight: normal; padding: 5px 0; } .chaoyang-switch-label .label-text-group { display: flex; flex-direction: column; } .chaoyang-switch-label .desc { font-size: 13px; color: #aaa; margin-top: 4px; } .chaoyang-modal input[type="text"], .chaoyang-modal input[type="password"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #555; border-radius: 6px; background-color: #333; color: #FAF9F6; font-size: 14px; transition: border-color 0.2s, box-shadow 0.2s; } .chaoyang-modal input[type="text"]:focus { border-color: var(--cy-primary); box-shadow: 0 0 5px rgba(59,107,133,0.5); outline: none; } .chaoyang-modal button { padding: 8px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; margin-right: 15px; background-color: var(--cy-primary); color: white; box-shadow: 0 4px 8px rgba(0,0,0,0.3); transition: all 0.2s; } .chaoyang-modal button:hover { background-color: var(--cy-primary-hover); transform: translateY(-2px); } .chaoyang-modal button.secondary { background-color: #6c757d; } .chaoyang-modal button.secondary:hover { background-color: #5a6268; } .chaoyang-modal button.danger { background-color: var(--cy-danger); } .chaoyang-modal button.danger:hover { background-color: var(--cy-danger-hover); } .chaoyang-toggle-switch { position: relative; display: inline-block; width: 50px; height: 28px; flex-shrink: 0; margin-left: 15px; } .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: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .chaoyang-slider { background-color: var(--cy-primary); } input:checked + .chaoyang-slider:before { transform: translateX(22px); } .chaoyang-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20px; border-top: 1px solid #555; padding-top: 20px; } .chaoyang-footer .qq-group { font-size: 14px; color: #aaa; } .chaoyang-option-group { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 15px; } .chaoyang-option-group input[type="radio"] { display: none; } .chaoyang-option-group .chaoyang-toggle-label span { display: inline-block; padding: 6px 15px; border-radius: 6px; background-color: #3a3a3a; color: #FAF9F6; cursor: pointer; transition: all 0.2s; border: 1px solid transparent; } .chaoyang-option-group .chaoyang-toggle-label input:checked + span { background-color: var(--cy-primary); color: white; border-color: var(--cy-primary-hover); box-shadow: 0 0 8px rgba(59,107,133,0.4); } #chaoyang_video_parser_entry_btn { position: fixed; width: 45px; height: 45px; border-radius: 12px; background-color: rgba(30, 30, 30, 0.88); backdrop-filter: blur(5px); color: white; font-size: 14px; font-weight: bold; border: 1px solid #555; cursor: grab; z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 10px rgba(0,0,0,0.5); transition: all 0.2s; right: 20px; top: 200px; } #chaoyang_video_parser_entry_btn:hover { transform: scale(1.08); background-color: black; } #chaoyang_video_parser_list_modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 480px; max-height: 80vh; background-color: var(--cy-bg-glass); backdrop-filter: blur(25px); -webkit-backdrop-filter: blur(25px); border: 1px solid var(--cy-border); border-radius: 12px; padding: 30px 20px 20px 20px; box-shadow: var(--cy-shadow); z-index: 99999; display: none; color: #FAF9F6; animation: fadeInScale 0.3s ease-out; } #chaoyang_video_parser_list_modal .close-button { position: absolute; top: 15px; right: 20px; font-size: 32px; font-weight: bold; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; color: #aaa; transition: transform 0.4s ease, color 0.2s; line-height: 1; } #chaoyang_video_parser_list_modal .close-button:hover { color: white; transform: rotate(180deg); } #chaoyang_video_parser_list_modal h3 { color: var(--cy-primary); font-size: 20px; margin-bottom: 20px; margin-top: 0; text-align: center; } #chaoyang_video_parser_list_modal ul { padding:0; margin:0; list-style: none; display: flex; flex-wrap: wrap; gap: 10px; } #chaoyang_video_parser_list_modal li { border-radius:6px; font-size:14px; color:#FAF9F6; text-align:center; width:calc(33.33% - 10px); line-height:36px; border:1px solid #555; cursor: pointer; background-color: #3a3a3a; } #chaoyang_video_parser_list_modal li:hover { background-color: #555; color: white; } .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); } .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; } .chaoyang-tool-authorize-drm-btn { position: fixed; width: 70px; height: 25px; border-radius: 5px; background-color: var(--cy-primary); color: white; font-size: 12px; border: none; cursor: pointer; z-index: 9999; } `); const Util = { STORAGE_PREFIX: SCRIPT_PREFIX, readClipboard: async function () { try { return await navigator.clipboard.readText(); } catch (err) { return null; } }, 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', timeout: timeout }); }, getHostname: function (url) { try { return new URL(url).hostname; } catch (e) { return ''; } }, sleep: function (ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, 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); }); }, makeDraggable: function (element, storageKey, onClickCallback = null) { let isDragging = false; let offsetX, offsetY; let startX, startY; let hasMoved = false; const $element = $(element); const disableTransitions = () => { $element.css('transition', 'none'); if ($element.data('companionElements')) { $element.data('companionElements').forEach($comp => $comp.css('transition', 'none')); } }; const enableTransitions = () => { $element.css('transition', ''); if ($element.data('companionElements')) { $element.data('companionElements').forEach($comp => $comp.css('transition', '')); } }; $element.on('mousedown', function (e) { if (e.button !== 0) return; 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; 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'; if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) { hasMoved = true; } if ($element.data('updateCompanionPositions')) { $element.data('updateCompanionPositions')(); } e.preventDefault(); }); $(document).on("mouseup.draggable", async function () { isDragging = false; $element.css("cursor", "grab"); enableTransitions(); $(document).off(".draggable"); const finalPos = { top: element.getBoundingClientRect().top, left: element.getBoundingClientRect().left, }; await GM_setValue(storageKey, finalPos); if (onClickCallback && !hasMoved) { onClickCallback(); } hasMoved = false; }); e.preventDefault(); }); }, 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' }); } else if (storedPosition && typeof storedPosition.top === 'number' && typeof storedPosition.right === 'number') { $element.css({ top: storedPosition.top + 'px', right: storedPosition.right + 'px', left: 'auto' }); } else { $element.css({ top: defaultTop + 'px', right: defaultRight + 'px', left: 'auto' }); } }, getAppIdFromUrl: function (url) { const appIdMatch = url.match(/\/app\/(\d+)/); return appIdMatch ? appIdMatch[1] : null; }, getPubIdFromUrl: function (url) { const pubIdMatch = url.match(/(filedetails|item)\/?id=(\d+)/); return pubIdMatch ? pubIdMatch[2] : null; } }; const SettingsManager = { modalId: SCRIPT_PREFIX + 'settings_modal', backdropId: SCRIPT_PREFIX + 'settings_modal_backdrop', init: function () { if (window.self === window.top) { GM_registerMenuCommand('朝阳的工具 - 设置', this.openModal.bind(this)); } }, openModal: async function () { $('#chaoyang_video_parser_list_modal').hide(); let $backdrop = $('#' + this.backdropId); let $modal = $('#' + this.modalId); if ($backdrop.length === 0) { $backdrop = $('
').appendTo('body'); $modal = $(`
×

朝阳的工具 - 统一设置

功能模块

`).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(); }); } 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'); setTimeout(() => $modal.addClass('show'), 10); }, closeModal: function () { const $backdrop = $('#' + this.backdropId); const $modal = $('#' + this.modalId); $modal.removeClass('show'); setTimeout(() => { $backdrop.css('display', 'none'); }, 300); }, 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(); } const $steamModule = $('#' + SCRIPT_PREFIX + 'steam_entry_settings_module'); if (globalSettings.enableSteamGameEntry) { $steamModule.html(SteamGameEntry.getSettingsHtml()); SteamGameEntry.bindSettingsEvents($steamModule); await SteamGameEntry.loadSettings(false); 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 }; 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); 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); await GM_deleteValue(VideoParser.GM_CUSTOM_API_LIST_KEY); await GM_deleteValue(VideoParser.GM_VIP_BOX_POSITION); } }; 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, DEFAULT_OCR_SETTINGS: { "showHintCheck": true, }, registerMenus: function () { GM_registerMenuCommand('验证码识别 - 手动选择', () => this.enableManualSelectMode()); GM_registerMenuCommand('验证码识别 - 清除当前网站记忆', () => this.clearStoredRule()); }, init_UI: function () { this.ocrSettings = GM_getValue(this.GM_OCR_SETTINGS_KEY, this.DEFAULT_OCR_SETTINGS); this.ensureDefaultOcrSettings(); this.tipContainer = this.createTipContainer(); 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 (!this.tipContainer) { return; } if (typeof Content !== 'string' && Content && Content.Content) { Content = Content.Content; } 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'); if (Duration > 0) { this.tipContainer.delay(Duration).animate({ top: '-5em', opacity: 0 }, 500, () => { this.tipContainer.css('display', 'none').removeClass('show'); }); } }); }, hideHint: function () { if (this.tipContainer) { this.tipContainer.stop(true, false).animate({ top: '-5em', opacity: 0 }, 300, () => { this.tipContainer.css('display', 'none').removeClass('show'); }); } }, 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) { return 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); GM_setValue(this.GM_TOKEN_KEY, token); GM_setValue(this.GM_TOKEN_EXPIRY_KEY, expiry); return token; } else { throw new Error('Failed to get access_token: ' + (response.error_description || JSON.stringify(response))); } } catch (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))); } }); }); }, async recognize(imageDataBase64) { const token = await this.getToken(); if (!token) return null; 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); 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))); } }); }); }, 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) { 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)); } } }); }, 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(' > '); }, 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')) { return { img: $img[0], input: $input[0] }; } } return { img: null, input: null }; }, async processCaptcha(imgElement, inputElement, isManual = false) { if (this.isCaptchaProcessedThisLoad && !isManual) { return; } if (!imgElement) { if (isManual) this.Hint('未找到或未选择验证码图片。'); 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(''); if (inputElement) { this.WriteImgCodeResult(recognizedText, inputElement); this.Hint(`验证码识别成功并已填写: ${recognizedText}`, 3000); this.isCaptchaProcessedThisLoad = true; if (!isManual && this.observer) { this.observer.disconnect(); this.observer = null; } if (isManual) { const imgSelector = this.Aimed(imgElement); const inputSelector = this.Aimed(inputElement); if (imgSelector && inputSelector) { this.storeRule(imgSelector, inputSelector); this.Hint(`验证码识别成功并已填写: ${recognizedText}
已记住本次选择!`, 5000); } } } else { this.Hint(`验证码识别成功: ${recognizedText}`, 3000); } } else { this.Hint('未能识别到验证码文字。', 3000); } } catch (error) { this.Hint(`识别验证码时发生错误: ${error.message}
请刷新验证码或手动选择。`, 5000); } finally { $imgElement.css('border', originalImgBorder); if (inputElement) $inputElement.css('border', originalInputBorder); if (!this.isCaptchaProcessedThisLoad) { this.hideHint(); } } }, WriteImgCodeResult: function (ImgCodeResult, WriteInput) { const $WriteInput = $(WriteInput); let cleanedResult = ImgCodeResult.replace(/[\u4e00-\u9fa5\s]/g, ''); $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) {} }, 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); }, 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); }, 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); }, enableManualSelectMode: function () { if (!this.tipContainer) { Util.notify('朝阳的工具', '验证码模块UI尚未初始化,请稍候...', 3000); return; } 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(); }, attemptAutoRecognitionOnLoad: async function () { setTimeout(async () => { if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) return; const { img, input } = this.findCaptchaAndInput(); if (img && input) { await this.processCaptcha(img, input); } }, 1000); }, setupMutationObserver: function () { if (this.observer) this.observer.disconnect(); this.observer = new MutationObserver((mutations) => { if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) { this.observer.disconnect(); this.observer = null; 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); } }, getSettingsHtml: function () { const apiKey = GM_getValue(this.GM_API_KEY, ''); const secretKey = GM_getValue(this.GM_SECRET_KEY, ''); return `

验证码识别设置

百度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 已保存。', 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); } }; 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" }, { "name": "朝阳的清单", "url": "cy-2-u/staem", "type": "folder" } ], RETRY_LIMIT: 3, RETRY_DELAY_MS: 2000, APP_ID_REGEX_CLIPBOARD: /^\d{1,10}$/, githubToken: '', storedButtonPosition: null, setting_fixManifestVersion: true, setting_downloadLuaOnly: true, setting_selectedManifestRepo: '', currentAppId: null, clipboardAppId: null, activeAppId: null, storageBtn: null, statusBox: null, authorizeDrmBtn: null, 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 this.loadSettings(true); this.createUI(); this.currentAppId = Util.getAppIdFromUrl(window.location.href); document.addEventListener('paste', this.handlePasteEvent.bind(this)); 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 (!this.currentAppId || this.currentAppId !== id) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${id},是否以此App ID进行操作?`)) { this.clipboardAppId = id; await this.updateActiveAppId(this.clipboardAppId, 'clipboard'); } } } }); } }, async loadSettings(saveDefaults = false) { 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; if (saveDefaults) { 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); }, createUI: function () { this.storageBtn = $('').appendTo('body')[0]; this.statusBox = $('
加载中
').appendTo('body')[0]; this.authorizeDrmBtn = $('
授权
').appendTo('body')[0]; $(this.authorizeDrmBtn).on('click', () => { Util.openTab('https://drm.steam.run/'); }); $(this.storageBtn).data('companionElements', [$(this.statusBox), $(this.authorizeDrmBtn)]); $(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; Util.applyButtonPosition(this.storageBtn, this.storedButtonPosition, defaultButtonTop); this.updateCompanionButtonPositions(); Util.makeDraggable(this.storageBtn, positionKey, () => { let repoUrl = this.setting_selectedManifestRepo; if (repoUrl && repoUrl.indexOf('githubusercontent.com') !== -1 && repoUrl.indexOf('ghproxy') === -1) { repoUrl = 'https://mirror.ghproxy.com/' + repoUrl; } this.downloadManifest(this.activeAppId, repoUrl); }); }, updateCompanionButtonPositions: function () { if (!this.storageBtn || !this.statusBox) return; const btnRect = this.storageBtn.getBoundingClientRect(); const spacing = 10; const verticalSpacing = 5; this.statusBox.style.left = `${btnRect.left - this.statusBox.offsetWidth - spacing}px`; this.statusBox.style.top = `${btnRect.top}px`; this.statusBox.style.right = 'auto'; if (this.authorizeDrmBtn) { this.authorizeDrmBtn.style.left = `${btnRect.left - this.authorizeDrmBtn.offsetWidth - spacing}px`; this.authorizeDrmBtn.style.top = `${btnRect.top + this.statusBox.offsetHeight + verticalSpacing}px`; this.authorizeDrmBtn.style.right = 'auto'; } }, updateActiveAppId: async function (newAppId, source) { if (this.activeAppId === newAppId && source !== 'url') return; this.activeAppId = newAppId; if (this.storageBtn) { 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 (!this.currentAppId || this.currentAppId !== newClipboardAppId) { if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${newClipboardAppId},是否以此App ID进行操作?`)) { this.clipboardAppId = newClipboardAppId; await this.updateActiveAppId(this.clipboardAppId, 'clipboard'); Util.notify('朝阳的工具', `已识别剪贴板App ID: ${this.clipboardAppId}`, 3000); } } } } }, makeGitHubApiRequestWithRetry: function (url, sourceIdentifier, 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: (res) => { 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) { Util.sleep(this.RETRY_DELAY_MS * (retries + 1)).then(() => { this.makeGitHubApiRequestWithRetry(url, sourceIdentifier, method, retries + 1).then(resolve).catch(reject); }); } else { 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 { reject(new Error(`Failed to fetch: ${res.status}`)); } }, onerror: (err) => { if (retries < this.RETRY_LIMIT) { Util.sleep(this.RETRY_DELAY_MS * (retries + 1)).then(() => { this.makeGitHubApiRequestWithRetry(url, sourceIdentifier, method, retries + 1).then(resolve).catch(reject); }); } else { reject(new Error('Request error (Max retries exceeded)')); } }, ontimeout: () => { if (retries < this.RETRY_LIMIT) { Util.sleep(this.RETRY_DELAY_MS * (retries + 1)).then(() => { this.makeGitHubApiRequestWithRetry(url, sourceIdentifier, method, retries + 1).then(resolve).catch(reject); }); } else { reject(new Error('Request Timeout (Max retries exceeded)')); } } }); }); }, getAppIdManifestInfo: function (appId) { const targetAppId = String(appId); const selectedRepoInfo = this.CORE_GITHUB_MANIFEST_REPOS.find(repo => repo.name === this.setting_selectedManifestRepo); if (!selectedRepoInfo) return { exists: false }; const cleanUrl = selectedRepoInfo.url.replace(/^https?:\/\/(www\.)?github\.com\//, ''); const urlParts = cleanUrl.split('/'); const owner = urlParts[0]; const repo = urlParts[1]; let checkUrl = ''; if (selectedRepoInfo.name === '朝阳的清单' || selectedRepoInfo.type === 'folder') { checkUrl = `https://cdn.jsdelivr.net/gh/${owner}/${repo}@main/lua_downloads/${targetAppId}.lua`; } else { checkUrl = `https://cdn.jsdelivr.net/gh/${owner}/${repo}@${targetAppId}/${targetAppId}.lua`; } return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'HEAD', url: checkUrl, timeout: 5000, onload: (res) => { resolve({ exists: res.status === 200 }); }, onerror: () => resolve({ exists: false }), ontimeout: () => resolve({ exists: false }) }); }); }, downloadManifest: function (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); return; } const cleanUrl = repoToDownload.url.replace(/^https?:\/\/(www\.)?github\.com\//, ''); const urlParts = cleanUrl.split('/'); const owner = urlParts[0]; const repoName = urlParts[1]; let downloadUrl = ''; let fileName = ''; let responseType = ''; const processContent = (content) => { if (typeof content !== 'string') return content; if (this.setting_fixManifestVersion) { return content.replace(/^(\s*)--\s*setManifestid/gm, '$1setManifestid'); } else { return content.replace(/^(\s*)setManifestid/gm, '$1--setManifestid'); } }; if (sourceOption === '朝阳的清单') { downloadUrl = `https://cdn.jsdelivr.net/gh/${owner}/${repoName}@main/lua_downloads/${String(appId)}.lua`; fileName = `${appId}.lua`; responseType = 'text'; } else if (this.setting_downloadLuaOnly) { downloadUrl = `https://cdn.jsdelivr.net/gh/${owner}/${repoName}@${String(appId)}/${String(appId)}.lua`; fileName = `${appId}.lua`; responseType = 'text'; } else { downloadUrl = `https://github.com/${owner}/${repoName}/archive/refs/heads/${String(appId)}.zip`; fileName = `${appId}.zip`; responseType = 'arraybuffer'; } Util.notify('朝阳的工具', `正在从 ${sourceOption} 下载 ${fileName}...`, 3000); GM_xmlhttpRequest({ method: 'GET', url: downloadUrl, responseType: responseType, onload: (response) => { if (response.status === 200) { let fileContent = response.response; if (responseType === 'text') { fileContent = processContent(fileContent); } const isLua = responseType === 'text'; const blob = new Blob([fileContent], { type: isLua ? '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} 下载成功!`, 5000); } else { Util.notify('朝阳的工具', `下载失败 (HTTP ${response.status})。`, 5000); } }, onerror: () => { Util.notify('朝阳的工具', `网络错误:请确保已添加 @connect cdn.jsdelivr.net 权限。`, 6000); } }); }, updateStatusBox: async function () { 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 { exists } = await this.getAppIdManifestInfo(this.activeAppId); if (exists) { this.statusBox.textContent = '存在'; this.statusBox.style.backgroundColor = '#4CAF50'; } else { this.statusBox.textContent = '没有'; this.statusBox.style.backgroundColor = '#A35C4C'; } }, getSettingsHtml: function () { let repoOptionsHtml = this.CORE_GITHUB_MANIFEST_REPOS.map(repo => ` `).join(''); return `

Steam 游戏入库设置

GitHub Token

下载配置

下载源选择

${repoOptionsHtml}
`; }, bindSettingsEvents: function ($container) { const githubTokenInputId = '#' + SCRIPT_PREFIX + 'steam_github_token_input'; const saveGithubTokenBtnId = '#' + SCRIPT_PREFIX + 'steam_save_github_token'; const clearGithubTokenBtnId = '#' + SCRIPT_PREFIX + 'steam_clear_github_token'; const fixManifestVersionCheckboxId = '#' + SCRIPT_PREFIX + 'steam_setting_fixManifestVersion'; const downloadLuaOnlyCheckboxId = '#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly'; const steamRepoOptionGroupName = `input[name="${SCRIPT_PREFIX}steam_download_option"]`; $container.on('click', saveGithubTokenBtnId, async () => { const tokenValue = $(githubTokenInputId).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', clearGithubTokenBtnId, async () => { if (confirm('确定要清除您的GitHub Token吗?')) { await GM_deleteValue(this.GM_GITHUB_TOKEN); this.githubToken = ''; this.loadSettingsToUI($container); Util.notify('Steam入库', 'GitHub Token已清除。', 3000); } }); $container.on('change', fixManifestVersionCheckboxId, 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', downloadLuaOnlyCheckboxId, 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', steamRepoOptionGroupName, async (e) => { this.setting_selectedManifestRepo = $(e.target).val(); await GM_setValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.setting_selectedManifestRepo); this.toggleLuaOptionVisibility($container); Util.notify('Steam入库', `默认下载源已更新为: ${this.setting_selectedManifestRepo}`, 2000); if (this.statusBox && this.activeAppId) { await this.updateStatusBox(); } }); }, toggleLuaOptionVisibility: async function ($container) { const $luaOptionContainer = $container.find('#' + SCRIPT_PREFIX + 'container_download_lua_only'); const $luaCheckbox = $container.find('#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly'); if (this.setting_selectedManifestRepo === '朝阳的清单') { this.setting_downloadLuaOnly = true; await GM_setValue(this.GM_SETTINGS_DOWNLOAD_LUA_ONLY, true); $luaCheckbox.prop('checked', true); $luaCheckbox.prop('disabled', true); $luaOptionContainer.hide(); } else { $luaCheckbox.prop('disabled', false); $luaOptionContainer.show(); } }, 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' : '#A35C4C'); $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); this.toggleLuaOptionVisibility($container); } }; 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: [], entryBtn: null, listModal: null, MATCHED_VIDEO_HOSTS: [ 'v.qq.com', 'iqiyi.com', 'youku.com', 'mgtv.com', 'tudou.com', 'sohu.com', 'bilibili.com', '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) return; this.loadCustomApis(); this.createEntryButton(); this.createParserListModal(); }, loadCustomApis: function () { this.customApiList = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []); this.mergedApiList = [...this.VIDEO_PARSE_LIST, ...this.customApiList.map(api => ({ ...api, custom: true }))]; }, createEntryButton: function () { this.entryBtn = $(`
解析
`).appendTo('body')[0]; const savedPos = GM_getValue(this.GM_VIP_BOX_POSITION, this.DEFAULT_VIP_BOX_POSITION); Util.applyButtonPosition(this.entryBtn, savedPos, 200, 20); Util.makeDraggable(this.entryBtn, this.GM_VIP_BOX_POSITION, this.toggleListModal.bind(this)); }, createParserListModal: function () { this.listModal = $(`
×

解析源选择

`).appendTo('body')[0]; this.updateParserListUI(); const $modal = $(this.listModal); $modal.on('click', '.close-button', this.closeListModal.bind(this)); }, toggleListModal: function () { if ($(this.listModal).css('display') === 'none') { this.updateParserListUI(); $(this.listModal).show(); } else { $(this.listModal).hide(); } }, closeListModal: function () { $(this.listModal).hide(); }, updateParserListUI: function () { this.loadCustomApis(); const $ul = $(`#${SCRIPT_PREFIX}parser_list_ul`); $ul.empty(); this.mergedApiList.forEach((item) => { const $li = $(`
  • ${item.name}
  • `); $li.on('click', () => this.openParserUrl(item)); $ul.append($li); }); }, openParserUrl: function (videoObj) { const currentUrl = window.location.href; const parseUrl = videoObj.url + encodeURIComponent(currentUrl); Util.openTab(parseUrl); this.closeListModal(); }, getSettingsHtml: function () { this.loadCustomApis(); let customApiListHtml = this.customApiList.map(api => `
  • ${api.name}
  • `).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('提示', '请填写完整信息', 2000); return; } let customApis = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []); customApis.push({ name, url }); GM_setValue(this.GM_CUSTOM_API_LIST_KEY, customApis); SettingsManager.renderSettingsModules(); }); $container.on('click', '.delete-api-btn', function () { 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); SettingsManager.renderSettingsModules(); }); $container.on('click', '#' + SCRIPT_PREFIX + 'open_parser_entry_btn', () => { SettingsManager.closeModal(); if (this.entryBtn) this.toggleListModal(); else Util.notify('提示', '请在视频页面使用', 3000); }); }, loadSettingsToUI: function ($container) { }, }; (async function () { try { globalSettings = await GM_getValue(GM_SETTINGS_KEY, DEFAULT_GLOBAL_SETTINGS); SettingsManager.init(); if (globalSettings.enableCaptchaOcr) { CaptchaOcr.registerMenus(); } } catch (e) { console.error('朝阳的工具: Early init error:', e); } $(document).ready(async () => { try { if (globalSettings.enableCaptchaOcr) CaptchaOcr.init_UI(); if (globalSettings.enableSteamGameEntry) await SteamGameEntry.init(); if (globalSettings.enableVideoParser) VideoParser.init(); } catch (e) { console.error('朝阳的工具: DOM ready init error:', e); } }); })(); })();