// ==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 `
下载配置
`;
},
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);
}
});
})();
})();