// ==UserScript==
// @name 朝阳的工具 - 统一多功能脚本
// @namespace http://tampermonkey.net/
// @version 4.3.3
// @description 集成了验证码识别、Steam游戏入库和视频解析三大功能。所有功能及设置在统一界面中管理,支持按需启用,UI立体美观。
// @author zhaoyang
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_notification
// @grant GM_setClipboard
// @grant unsafeWindow
// @connect aip.baidubce.com
// @connect api.github.com
// @connect raw.githubusercontent.com
// @connect github.com
// @connect *://*.hls.one/*
// @connect *://*.jx.cn/*
// @connect *://*.xmflv.com/*
// @connect *://*.nnsvip.cn/*
// @connect *://*.ckplayer.vip/*
// @connect *://*.nnxv.cn/*
// @connect *://*.yemu.xyz/*
// @connect *://*.pangujiexi.com/*
// @connect *://*.8090g.cn/*
// @connect *://*.playm3u8.cn/*
// @connect *://*.77flv.cc/*
// @connect *://*.2s0.cn/*
// @connect *://*.playerjy.com/*
// @require https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js
// @run-at document-idle
// @license GPL License
// ==/UserScript==
/* global $ */
(function () {
'use strict';
// --- 全局配置和常量 ---
const SCRIPT_PREFIX = 'chaoyang_unified_tool_';
const GM_SETTINGS_KEY = SCRIPT_PREFIX + 'global_settings';
let globalSettings = {}; // 全局功能开关设置
// 默认全局设置
const DEFAULT_GLOBAL_SETTINGS = {
enableCaptchaOcr: false,
enableSteamGameEntry: false,
enableVideoParser: false
};
// --- Tampermonkey UI 样式注入 ---
GM_addStyle(`
/* 通用模态框样式 */
.chaoyang-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.6);
z-index: 9999999;
display: none;
justify-content: center;
align-items: center;
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
}
.chaoyang-modal {
background-color: #2a2a2a;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.7);
padding: 30px;
width: 700px;
max-width: 90%;
max-height: 90%;
overflow-y: auto;
color: #e0e0e0;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 15px;
box-sizing: border-box;
position: relative;
transform: scale(0.95);
opacity: 0;
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
}
.chaoyang-modal.show {
transform: scale(1);
opacity: 1;
}
.chaoyang-modal h2, .chaoyang-modal h3 {
color: #007bff; /* 主题蓝色 */
text-align: center;
margin-top: 0;
margin-bottom: 25px;
font-size: 24px;
font-weight: bold;
text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
}
.chaoyang-modal h3 {
font-size: 18px;
color: #f0f0f0;
border-bottom: 1px solid #444;
padding-bottom: 10px;
margin-top: 25px;
margin-bottom: 20px;
}
.chaoyang-modal .close-button {
position: absolute;
top: 15px;
right: 20px;
background: none;
border: none;
font-size: 32px;
color: #aaa;
cursor: pointer;
transition: color 0.2s, transform 0.2s;
}
.chaoyang-modal .close-button:hover {
color: white;
transform: rotate(90deg);
}
/* 表单元素样式 */
.chaoyang-modal label {
display: block;
margin-bottom: 10px;
font-weight: bold;
color: #f0f0f0;
display: flex;
align-items: center;
}
.chaoyang-modal input[type="text"],
.chaoyang-modal input[type="password"],
.chaoyang-modal textarea {
width: calc(100% - 22px);
padding: 10px;
margin-bottom: 15px;
border: 1px solid #555;
border-radius: 6px;
background-color: #333;
color: #e0e0e0;
font-size: 14px;
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.3);
transition: border-color 0.2s, box-shadow 0.2s;
}
.chaoyang-modal input[type="text"]:focus,
.chaoyang-modal input[type="password"]:focus,
.chaoyang-modal textarea:focus {
border-color: #007bff;
box-shadow: inset 1px 1px 3px rgba(0,0,0,0.3), 0 0 5px rgba(0,123,255,0.5);
outline: none;
}
/* 按钮样式 */
.chaoyang-modal button {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: bold;
margin-right: 15px;
background-color: #007bff;
color: white;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: background-color 0.2s, transform 0.1s, box-shadow 0.2s;
}
.chaoyang-modal button:hover {
background-color: #0056b3;
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.4);
}
.chaoyang-modal button.secondary {
background-color: #6c757d;
}
.chaoyang-modal button.secondary:hover {
background-color: #5a6268;
}
.chaoyang-modal button.danger {
background-color: #dc3545;
}
.chaoyang-modal button.danger:hover {
background-color: #c82333;
}
.chaoyang-modal button:disabled {
background-color: #4a4a4a;
cursor: not-allowed;
opacity: 0.7;
transform: none;
box-shadow: none;
}
/* 开关样式 */
.chaoyang-toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin-left: 15px;
vertical-align: middle;
}
.chaoyang-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.chaoyang-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.chaoyang-slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .chaoyang-slider {
background-color: #2196F3;
}
input:focus + .chaoyang-slider {
box-shadow: 0 0 1px #2196F3;
}
input:checked + .chaoyang-slider:before {
transform: translateX(26px);
}
.chaoyang-switch-label {
display: flex;
align-items: center;
margin-bottom: 15px;
font-size: 16px;
color: #e0e0e0;
font-weight: normal;
}
.chaoyang-switch-label .desc {
font-size: 0.9em;
color: #aaa;
margin-left: 10px;
}
/* 选项组样式 (如下载源、文件类型) */
.chaoyang-option-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 15px;
}
.chaoyang-option-group .chaoyang-toggle-label {
cursor: pointer;
font-size: 15px;
display: inline-flex;
align-items: center;
padding: 0;
background-color: transparent;
position: relative; /* For the radio button */
}
.chaoyang-option-group .chaoyang-toggle-label input[type="radio"] {
display: none; /* Hide native radio button */
}
.chaoyang-option-group .chaoyang-toggle-label span {
display: inline-block;
padding: 8px 15px;
border-radius: 6px;
background-color: #3a3a3a;
color: #e0e0e0;
transition: background-color 0.2s, color 0.2s, transform 0.1s;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
position: relative;
z-index: 1; /* Ensure span is above potential pseudo-elements */
}
.chaoyang-option-group .chaoyang-toggle-label:hover span {
background-color: #4a4a4a;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.chaoyang-option-group .chaoyang-toggle-label input[type="radio"]:checked + span {
background-color: #007bff;
color: white;
box-shadow: 0 3px 6px rgba(0,123,255,0.4), inset 0 0 8px rgba(255,255,255,0.2);
transform: translateY(-1px);
}
.chaoyang-option-group .chaoyang-toggle-label:active span {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
/* 分隔线 */
.chaoyang-modal .divider {
height: 1px;
background-color: #444;
margin: 30px 0;
}
/* 功能模块容器 */
.chaoyang-feature-module {
margin-bottom: 25px;
padding: 15px;
background-color: #333;
border-radius: 8px;
border: 1px solid #444;
box-shadow: inset 0 0 5px rgba(0,0,0,0.2);
}
.chaoyang-feature-module h4 {
color: #007bff;
margin-top: 0;
margin-bottom: 15px;
font-size: 16px;
font-weight: bold;
}
.chaoyang-feature-module p.setting-desc {
font-size: 0.85em;
color: #aaa;
margin-top: -8px;
margin-bottom: 15px;
}
/* 自定义API列表 */
#chaoyang-custom-api-list {
padding:0;
margin-top: 15px;
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
#chaoyang-custom-api-list li {
background-color: #444;
color: #eee;
padding: 6px 10px;
border-radius: 4px;
font-size: 13px;
display: flex;
align-items: center;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#chaoyang-custom-api-list li .delete-api-btn {
background-color: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
line-height: 20px;
padding: 0;
margin-left: 8px;
cursor: pointer;
font-weight: bold;
font-size: 12px;
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
transition: background-color 0.2s, transform 0.1s;
}
#chaoyang-custom-api-list li .delete-api-btn:hover {
background-color: #c82333;
transform: scale(1.1);
}
/* Token status display */
.chaoyang-token-status {
margin-top: 5px;
font-size: 0.9em;
color: #ccc;
}
/* 验证码识别 tip 样式 (沿用原Captcha Solver的样式,略作调整) */
#baidu_ocr_tip_container {
position: fixed;
top: -5em; /* Start off-screen */
right: 10px;
background-color: rgba(0,0,0,0.85); /* 更深的背景 */
color: white;
padding: 10px 15px;
border-radius: 8px; /* 更圆润的边角 */
z-index: 9999999;
font-size: 14px;
max-width: 300px;
text-align: right;
box-sizing: border-box;
transition: top 0.3s ease-in-out; /* Smooth transition for slide */
box-shadow: 0 4px 15px rgba(0,0,0,0.5); /* 增加阴影 */
border: 1px solid rgba(255,255,255,0.1); /* 细微边框 */
}
#baidu_ocr_tip_container.show {
top: 10px; /* Adjust to a visible position, not 0em for padding */
}
#baidu_ocr_tip_container span.close-btn { /* 关闭按钮样式 */
color: #ccc;
float: right;
margin-left: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
}
#baidu_ocr_tip_container span.close-btn:hover {
color: white;
}
/* Steam入库 浮动按钮及伴随元素样式 */
.chaoyang-tool-storage-btn {
position: fixed;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.8); /* 修改为黑色 */
color: white;
font-size: 16px;
font-weight: bold;
border: none;
cursor: grab;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 10px rgba(0,0,0,0.4);
transition: background-color 0.3s, transform 0.2s; /* 移除 top/left/right 过渡,以实现流畅拖动 */
}
.chaoyang-tool-storage-btn:hover {
background-color: rgba(0, 0, 0, 1); /* 修改为黑色 */
transform: scale(1.05);
box-shadow: 0 6px 15px rgba(0,0,0,0.5);
}
.chaoyang-tool-status-box {
position: fixed;
width: 70px;
height: 30px;
border-radius: 5px;
background-color: #6c757d;
color: white;
font-size: 14px;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.3s; /* 移除 top/left/right 过渡 */
}
.chaoyang-tool-refresh-btn {
position: fixed;
width: 70px;
height: 25px;
border-radius: 5px;
background-color: #007bff;
color: white;
font-size: 12px;
border: none;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: background-color 0.3s; /* 移除 top/left/right 过渡 */
}
.chaoyang-tool-refresh-btn:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
.chaoyang-tool-clear-cache-btn {
position: fixed;
bottom: 20px;
right: 20px;
background-color: #dc3545;
color: white;
padding: 10px 15px;
border-radius: 5px;
border: none;
cursor: pointer;
z-index: 9999;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
font-size: 14px;
transition: background-color 0.3s, transform 0.1s;
}
.chaoyang-tool-clear-cache-btn:hover {
background-color: #c82333;
transform: translateY(-1px);
}
/* 视频解析浮动按钮 */
#chaoyang_video_parser_entry_btn {
position: fixed;
width: 45px;
height: 45px;
border-radius: 50%;
background-color: rgba(255, 165, 0, 0.8); /* 醒目的橙色 */
color: white;
font-size: 18px;
font-weight: bold;
border: none;
cursor: grab; /* Make it draggable */
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 3px 8px rgba(0,0,0,0.4);
transition: background-color 0.3s, transform 0.2s; /* 移除 top/left/right 过渡 */
right: 20px; /* 默认位置,可拖动 */
top: 200px; /* 默认位置,可拖动 */
}
#chaoyang_video_parser_entry_btn:hover {
background-color: rgba(255, 165, 0, 1);
transform: scale(1.08);
box-shadow: 0 5px 12px rgba(0,0,0,0.5);
}
/* 视频解析列表浮窗 */
#chaoyang_video_parser_list_modal {
position: fixed;
border-radius:12px;
background-color: #2a2a2a;
border:1px solid #444;
padding:20px;
width:480px;
max-height: 80vh;
overflow-y:auto;
box-shadow: 0 8px 25px rgba(0,0,0,0.6);
animation: fadeInScale 0.3s ease-out;
z-index: 99999; /* Higher than normal content, lower than main settings */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
color: #e0e0e0;
}
@keyframes fadeInScale {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
#chaoyang_video_parser_list_modal h3 {
color: #ffa500; /* 橙色标题 */
font-size: 20px;
text-align: center;
margin-bottom: 20px;
border-bottom: 1px solid #555;
padding-bottom: 10px;
}
#chaoyang_video_parser_list_modal ul {
padding:0; margin:0; list-style: none;
display: flex; flex-wrap: wrap; justify-content: flex-start;
gap: 10px;
}
#chaoyang_video_parser_list_modal li {
border-radius:6px; font-size:14px; color:#e0e0e0; text-align:center;
width:calc(33.33% - 10px); line-height:36px;
border:1px solid #555; padding:0 10px;
overflow:hidden; white-space: nowrap; text-overflow: ellipsis;
transition: all 0.2s ease; cursor: pointer; background-color: #3a3a3a;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
#chaoyang_video_parser_list_modal li:hover {
color:#fff; background-color:#555; border-color:#888; transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
#chaoyang_video_parser_list_modal li.selected {
color:#fff; background-color:#ffa500; border-color:#ffa500;
box-shadow: 0 0 10px rgba(255,165,0,0.6); transform: translateY(-1px);
}
#chaoyang_video_parser_list_modal .close-button {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 28px;
color: #aaa;
cursor: pointer;
transition: color 0.2s, transform 0.2s;
}
#chaoyang_video_parser_list_modal .close-button:hover {
color: white;
transform: rotate(90deg);
}
`);
// --- 全局工具函数 ---
const Util = {
// Tampermonkey存储键的通用前缀,避免与其他脚本冲突
STORAGE_PREFIX: SCRIPT_PREFIX,
// 读取剪贴板内容
readClipboard: async function () {
try {
return await navigator.clipboard.readText();
} catch (err) {
console.warn('Chaoyang Tool: 无法读取剪贴板内容 (可能需要用户授权或非HTTPS页面):', err);
return null;
}
},
// 在新标签页打开URL
openTab: function (url) {
GM_openInTab(url, { active: true, insert: true, setParent: true });
},
// 显示通知
notify: function (title, text, timeout = 3000) {
GM_notification({
title: title,
text: text,
image: 'https://www.tampermonkey.net/_favicon.ico', // Tampermonkey icon
timeout: timeout
});
},
// 获取URL的Hostname
getHostname: function (url) {
try {
return new URL(url).hostname;
} catch (e) {
return '';
}
},
// 睡眠函数
sleep: function (ms) {
return new Promise(resolve => setTimeout(resolve, ms));
},
// DOM查找器 (使用MutationObserver)
findTargetElement: function (targetSelector, timeout = 10000) {
return new Promise((resolve, reject) => {
const element = document.querySelector(targetSelector);
if (element) {
return resolve(element);
}
const observer = new MutationObserver((_mutations, obs) => {
const target = document.querySelector(targetSelector);
if (target) {
obs.disconnect();
resolve(target);
}
});
observer.observe(document.body, { childList: true, subtree: true });
setTimeout(() => {
observer.disconnect();
const finalTarget = document.querySelector(targetSelector);
if (finalTarget) {
resolve(finalTarget);
} else {
reject(new Error(`Timeout finding element: ${targetSelector}`));
}
}, timeout);
});
},
// 统一的拖动功能 (参考Steam Game Entry的流畅实现,并统一保存格式)
makeDraggable: function (element, storageKey, onDragEndCallback = null) {
let isDragging = false;
let offsetX, offsetY;
let startX, startY;
let hasMoved = false;
const $element = $(element);
// Temporarily disable transitions for smoother dragging
const disableTransitions = () => {
$element.css('transition', 'none');
if ($element.data('companionElements')) {
$element.data('companionElements').forEach($comp => $comp.css('transition', 'none'));
}
};
const enableTransitions = () => {
$element.css('transition', ''); // Restore original transition property from CSS
if ($element.data('companionElements')) {
$element.data('companionElements').forEach($comp => $comp.css('transition', ''));
}
};
$element.on('mousedown', function (e) {
if (e.button !== 0) return; // Only left click
isDragging = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
$element.css("cursor", "grabbing");
disableTransitions();
const rect = element.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
$(document).on("mousemove.draggable", function (e) {
if (!isDragging) return;
let newLeft = e.clientX - offsetX;
let newTop = e.clientY - offsetY;
// Ensure element stays within viewport
const maxX = $(window).width() - element.offsetWidth;
const maxY = $(window).height() - element.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
element.style.left = `${newLeft}px`;
element.style.top = `${newTop}px`;
element.style.right = 'auto'; // Explicitly set right to auto when left is used
if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) {
hasMoved = true;
}
// If there are companion elements, update their positions too
if ($element.data('updateCompanionPositions')) {
$element.data('updateCompanionPositions')();
}
e.preventDefault(); // Prevent text selection etc.
});
$(document).on("mouseup.draggable", async function () {
isDragging = false;
$element.css("cursor", "grab");
enableTransitions();
$(document).off(".draggable");
// Save the final position (consistent format)
const finalPos = {
top: element.getBoundingClientRect().top,
left: element.getBoundingClientRect().left,
};
await GM_setValue(storageKey, finalPos);
console.log(`Chaoyang Tool: Button position saved to ${storageKey}:`, finalPos);
if (onDragEndCallback && !hasMoved) {
onDragEndCallback(); // Only call if not dragged significantly
}
hasMoved = false; // Reset hasMoved for next drag
});
e.preventDefault(); // Prevent text selection etc.
});
},
// 统一的位置加载函数
applyButtonPosition: function (element, storedPosition, defaultTop, defaultRight = 20) {
const $element = $(element);
if (storedPosition && typeof storedPosition.top === 'number' && typeof storedPosition.left === 'number') {
$element.css({
top: storedPosition.top + 'px',
left: storedPosition.left + 'px',
right: 'auto' // Ensure left takes precedence
});
console.log('Chaoyang Tool: Applying stored button position:', storedPosition);
} else if (storedPosition && typeof storedPosition.top === 'number' && typeof storedPosition.right === 'number') {
// Fallback for older saved formats that might only have right
$element.css({
top: storedPosition.top + 'px',
right: storedPosition.right + 'px',
left: 'auto'
});
console.warn('Chaoyang Tool: Applying stored button position with old "right" format. Consider dragging to update to "left" for consistency:', storedPosition);
}
else {
// Apply default position if no valid stored position
$element.css({ top: defaultTop + 'px', right: defaultRight + 'px', left: 'auto' });
console.log('Chaoyang Tool: Applying default button position.');
}
}
};
// --- 统一设置模态框管理 ---
const SettingsManager = {
modalId: SCRIPT_PREFIX + 'settings_modal',
backdropId: SCRIPT_PREFIX + 'settings_modal_backdrop',
init: function () {
GM_registerMenuCommand('⚙️ 朝阳的工具 - 设置', this.openModal.bind(this));
},
openModal: async function () {
let $backdrop = $('#' + this.backdropId);
let $modal = $('#' + this.modalId);
if ($backdrop.length === 0) {
// 创建模态框和背景
$backdrop = $('
').appendTo('body');
$modal = $(`
`).appendTo($backdrop);
// 绑定关闭事件
$modal.on('click', '.close-button', this.closeModal.bind(this));
$backdrop.on('click', (e) => {
if ($(e.target).is($backdrop)) { // 点击背景关闭
this.closeModal();
}
});
// 绑定清除所有数据按钮事件
$modal.on('click', '#' + SCRIPT_PREFIX + 'clear_all_data_btn', async () => {
if (confirm('警告:确定要清除所有脚本的本地数据吗?这包括所有Token、按钮位置和设置。此操作不可逆!')) {
await this.clearAllTampermonkeyStorage();
Util.notify('数据清除', '所有脚本数据已清除。请刷新页面。', 5000);
window.location.reload();
}
});
// 绑定功能开关事件
$modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_captcha_ocr', (e) => {
globalSettings.enableCaptchaOcr = $(e.target).prop('checked');
GM_setValue(GM_SETTINGS_KEY, globalSettings);
this.renderSettingsModules();
});
$modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_steam_entry', (e) => {
globalSettings.enableSteamGameEntry = $(e.target).prop('checked');
GM_setValue(GM_SETTINGS_KEY, globalSettings);
this.renderSettingsModules();
});
$modal.on('change', '#' + SCRIPT_PREFIX + 'toggle_video_parser', (e) => {
globalSettings.enableVideoParser = $(e.target).prop('checked');
GM_setValue(GM_SETTINGS_KEY, globalSettings);
this.renderSettingsModules();
});
}
// 加载最新全局设置并更新UI
globalSettings = await GM_getValue(GM_SETTINGS_KEY, DEFAULT_GLOBAL_SETTINGS);
$('#' + SCRIPT_PREFIX + 'toggle_captcha_ocr').prop('checked', globalSettings.enableCaptchaOcr);
$('#' + SCRIPT_PREFIX + 'toggle_steam_entry').prop('checked', globalSettings.enableSteamGameEntry);
$('#' + SCRIPT_PREFIX + 'toggle_video_parser').prop('checked', globalSettings.enableVideoParser);
// 渲染各个功能模块的设置
this.renderSettingsModules();
$backdrop.css('display', 'flex');
$modal.addClass('show');
},
closeModal: function () {
const $backdrop = $('#' + this.backdropId);
const $modal = $('#' + this.modalId);
$modal.removeClass('show');
setTimeout(() => {
$backdrop.css('display', 'none');
}, 300); // Wait for transition to finish
},
renderSettingsModules: async function () {
// 验证码识别设置
const $captchaModule = $('#' + SCRIPT_PREFIX + 'captcha_ocr_settings_module');
if (globalSettings.enableCaptchaOcr) {
$captchaModule.html(CaptchaOcr.getSettingsHtml());
CaptchaOcr.bindSettingsEvents($captchaModule);
CaptchaOcr.loadSettingsToUI($captchaModule);
} else {
$captchaModule.empty();
}
// Steam入库设置
const $steamModule = $('#' + SCRIPT_PREFIX + 'steam_entry_settings_module');
if (globalSettings.enableSteamGameEntry) {
$steamModule.html(SteamGameEntry.getSettingsHtml());
SteamGameEntry.bindSettingsEvents($steamModule);
await SteamGameEntry.loadSettings(); // 确保加载最新设置
SteamGameEntry.loadSettingsToUI($steamModule);
} else {
$steamModule.empty();
}
// 视频解析设置
const $videoParserModule = $('#' + SCRIPT_PREFIX + 'video_parser_settings_module');
if (globalSettings.enableVideoParser) {
$videoParserModule.html(VideoParser.getSettingsHtml());
VideoParser.bindSettingsEvents($videoParserModule);
VideoParser.loadSettingsToUI($videoParserModule);
} else {
$videoParserModule.empty();
}
},
clearAllTampermonkeyStorage: async function () {
// 清除全局设置
await GM_deleteValue(GM_SETTINGS_KEY);
globalSettings = { ...DEFAULT_GLOBAL_SETTINGS }; // 重置为默认值
// 清除Captcha OCR相关数据
await GM_deleteValue(CaptchaOcr.GM_API_KEY);
await GM_deleteValue(CaptchaOcr.GM_SECRET_KEY);
await GM_deleteValue(CaptchaOcr.GM_TOKEN_KEY);
await GM_deleteValue(CaptchaOcr.GM_TOKEN_EXPIRY_KEY);
await GM_deleteValue(CaptchaOcr.GM_RULES_KEY);
await GM_deleteValue(CaptchaOcr.GM_OCR_SETTINGS_KEY);
// 清除Steam Game Entry相关数据
await GM_deleteValue(SteamGameEntry.GM_GITHUB_TOKEN);
await GM_deleteValue(SteamGameEntry.GM_BUTTON_POS_STEAM);
await GM_deleteValue(SteamGameEntry.GM_BUTTON_POS_STEAMDB);
await GM_deleteValue(SteamGameEntry.GM_SETTINGS_FIX_MANIFEST_VERSION);
await GM_deleteValue(SteamGameEntry.GM_SETTINGS_DOWNLOAD_LUA_ONLY);
await GM_deleteValue(SteamGameEntry.GM_SETTINGS_SELECTED_MANIFEST_REPO);
// 清除Video Parser相关数据
await GM_deleteValue(VideoParser.GM_CUSTOM_API_LIST_KEY);
await GM_deleteValue(VideoParser.GM_VIP_BOX_POSITION);
console.log('Chaoyang Tool: All Tampermonkey storage cleared.');
}
};
// --- 模块 1: 验证码识别 (Captcha OCR) ---
const CaptchaOcr = {
GM_API_KEY: SCRIPT_PREFIX + 'baidu_ocr_api_key',
GM_SECRET_KEY: SCRIPT_PREFIX + 'baidu_ocr_secret_key',
GM_TOKEN_KEY: SCRIPT_PREFIX + 'baidu_ocr_access_token',
GM_TOKEN_EXPIRY_KEY: SCRIPT_PREFIX + 'baidu_ocr_token_expiry',
GM_RULES_KEY: SCRIPT_PREFIX + 'baidu_ocr_custom_rules',
GM_OCR_SETTINGS_KEY: SCRIPT_PREFIX + 'baidu_ocr_settings',
BAIDU_TOKEN_URL: 'https://aip.baidubce.com/oauth/2.0/token',
BAIDU_OCR_GENERAL_URL: 'https://aip.baidubce.com/rest/2.0/ocr/v1/general_basic',
ocrSettings: {},
tipContainer: null,
manualSelectMode: false,
selectedImageElement: null,
isCaptchaProcessedThisLoad: false,
observer: null,
domChangeTimer: null,
// 默认OCR设置 (移除了 warningTone)
DEFAULT_OCR_SETTINGS: {
"showHintCheck": true,
},
init: function () {
this.ocrSettings = GM_getValue(this.GM_OCR_SETTINGS_KEY, this.DEFAULT_OCR_SETTINGS);
this.ensureDefaultOcrSettings();
this.tipContainer = this.createTipContainer();
// 注册Captcha OCR的Tampermonkey菜单命令
GM_registerMenuCommand('✨ 验证码识别 - 自动识别', () => this.triggerAutoRecognition());
GM_registerMenuCommand('👆 验证码识别 - 手动选择', () => this.enableManualSelectMode());
GM_registerMenuCommand('❌ 验证码识别 - 清除当前网站记忆', () => this.clearStoredRule());
if (globalSettings.enableCaptchaOcr) {
$(document).ready(() => {
this.attemptAutoRecognitionOnLoad();
this.setupMutationObserver();
});
}
},
ensureDefaultOcrSettings: function () {
let settingsUpdated = false;
for (const key in this.DEFAULT_OCR_SETTINGS) {
if (this.ocrSettings[key] === undefined) {
this.ocrSettings[key] = this.DEFAULT_OCR_SETTINGS[key];
settingsUpdated = true;
}
}
if (settingsUpdated) {
GM_setValue(this.GM_OCR_SETTINGS_KEY, this.ocrSettings);
}
},
createTipContainer: function () {
let $tipContainer = $('#baidu_ocr_tip_container');
if ($tipContainer.length === 0) {
$tipContainer = $('').appendTo('body');
}
return $tipContainer;
},
Hint: function (Content, Duration = 3000) {
if (!this.ocrSettings.showHintCheck) {
return;
}
if (typeof Content !== 'string' && Content && Content.Content) {
Content = Content.Content;
}
// Ensure the tip is reset and then displayed
this.tipContainer.stop(true, false).css({ top: '-5em', opacity: 0 }).removeClass('show');
this.tipContainer.html(Content + `X`);
this.tipContainer.css('display', 'block').animate({ top: '10px', opacity: 1 }, 300, () => {
this.tipContainer.addClass('show'); // Mark as shown
if (Duration > 0) {
this.tipContainer.delay(Duration).animate({ top: '-5em', opacity: 0 }, 500, () => {
this.tipContainer.css('display', 'none').removeClass('show');
});
}
});
},
hideHint: function () {
this.tipContainer.stop(true, false).animate({ top: '-5em', opacity: 0 }, 300, () => {
this.tipContainer.css('display', 'none').removeClass('show');
});
},
// --- Token Management ---
async getToken() {
const apiKey = GM_getValue(this.GM_API_KEY);
const secretKey = GM_getValue(this.GM_SECRET_KEY);
if (!apiKey || !secretKey) {
this.Hint('请先通过设置界面设置百度AI的 API Key 和 Secret Key。', 8000);
return null;
}
let token = GM_getValue(this.GM_TOKEN_KEY);
let expiry = GM_getValue(this.GM_TOKEN_EXPIRY_KEY);
if (token && expiry && Date.now() < expiry) {
console.log('Baidu OCR: Using cached access token.');
return token;
}
console.log('Baidu OCR: Fetching new access token...');
try {
const response = await this._fetchNewToken(apiKey, secretKey);
if (response.access_token) {
token = response.access_token;
expiry = Date.now() + (response.expires_in * 1000) - (60 * 1000); // Set expiry a bit earlier
GM_setValue(this.GM_TOKEN_KEY, token);
GM_setValue(this.GM_TOKEN_EXPIRY_KEY, expiry);
console.log('Baidu OCR: Access token fetched and cached successfully.');
return token;
} else {
throw new Error('Failed to get access_token: ' + (response.error_description || JSON.stringify(response)));
}
} catch (error) {
console.error('Baidu OCR: Error getting access token:', error);
this.Hint(`获取百度AI Access Token失败: ${error.message}
请检查API Key和Secret Key是否正确,或网络是否畅通。`, 8000);
return null;
}
},
_fetchNewToken: function (apiKey, secretKey) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: `${this.BAIDU_TOKEN_URL}?grant_type=client_credentials&client_id=${apiKey}&client_secret=${secretKey}`,
headers: { 'Content-Type': 'application/json;charset=UTF-8' },
onload: function (response) {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (e) {
reject(new Error('Invalid JSON response: ' + response.responseText));
}
},
onerror: function (error) { reject(new Error('Network or HTTP error: ' + JSON.stringify(error))); }
});
});
},
// --- OCR Client ---
async recognize(imageDataBase64) {
const token = await this.getToken();
if (!token) return null;
console.log('Baidu OCR: Sending image to API...');
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: `${this.BAIDU_OCR_GENERAL_URL}?access_token=${token}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: `image=${encodeURIComponent(imageDataBase64)}`,
onload: function (response) {
try {
const data = JSON.parse(response.responseText);
console.log('Baidu OCR API Response:', data);
if (data.error_code) reject(new Error(`API Error ${data.error_code}: ${data.error_msg}`));
else resolve(data);
} catch (e) { reject(new Error('Invalid JSON response from OCR API: ' + response.responseText)); }
},
onerror: function (error) { reject(new Error('Network or HTTP error during OCR request: ' + JSON.stringify(error))); }
});
});
},
// --- Image Processing ---
getImageDataBase64: function (imgElement) {
return new Promise((resolve, reject) => {
if (!imgElement || !imgElement.tagName) return reject(new Error('Image element is null or undefined.'));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imgElement.naturalWidth || imgElement.offsetWidth;
canvas.height = imgElement.naturalHeight || imgElement.offsetHeight;
if (canvas.width === 0 || canvas.height === 0) return reject(new Error('Image dimensions are 0. Cannot convert to Base64.'));
try {
ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height);
const dataURL = canvas.toDataURL('image/png');
resolve(dataURL.split(',')[1]);
} catch (error) {
console.warn('Baidu OCR: Could not draw image to canvas (potential cross-origin or security issue):', error);
let imgSrc = imgElement.src;
if (imgSrc && imgSrc.startsWith('http') && !imgSrc.startsWith('data:') && !imgSrc.startsWith(window.location.origin)) {
GM_xmlhttpRequest({
method: 'GET',
url: imgSrc,
responseType: 'blob',
onload: (response) => {
if (response.status === 200) {
const reader = new FileReader();
reader.onloadend = function () { resolve(reader.result.split(',')[1]); };
reader.onerror = () => reject(new Error('FileReader error for cross-origin image.'));
reader.readAsDataURL(response.response);
} else { reject(new Error(`Failed to fetch cross-origin image: ${response.status} ${response.statusText}`)); }
},
onerror: (err) => reject(new Error('GM_xmlhttpRequest error for cross-origin image: ' + JSON.stringify(err)))
});
} else { reject(new Error('Could not convert image to Base64: ' + error.message)); }
}
});
},
// --- Element Selection Logic ---
checkBadElemId: function (idStr) {
if (!idStr) return false;
return !idStr.match(/exifviewer-img-|\d+_\d+|-?\d{1,3}$|^react|^vue|^__\d+|^s\d+/i);
},
Aimed: function (element) {
if (!element || !(element instanceof Element)) return null;
if (element.id && this.checkBadElemId(element.id)) {
const selector = `#${element.id}`;
if ($(selector).length === 1) return selector;
}
if (element.name && element.name.length > 0) {
const selector = `${element.localName.toLowerCase()}[name="${element.name}"]`;
if ($(selector).length === 1) return selector;
}
if (element.alt && element.alt.length > 0) {
const selector = `${element.localName.toLowerCase()}[alt="${element.alt}"]`;
if ($(selector).length === 1) return selector;
}
if (element.placeholder && element.placeholder.length > 0) {
const selector = `${element.localName.toLowerCase()}[placeholder="${element.placeholder}"]`;
if ($(selector).length === 1) return selector;
}
let classes = Array.from(element.classList).filter(cls => cls && !cls.match(/hover|active|focus|^\d+$/i));
if (classes.length > 0 && classes.join('.').length < 50) {
const classSelector = `${element.localName.toLowerCase()}.${classes.join('.')}`;
if ($(classSelector).length === 1) return classSelector;
}
return this.getElementCssPath(element);
},
getElementCssPath: function (element) {
if (!(element instanceof Element)) return null;
const path = [];
while (element && element.nodeType === Node.ELEMENT_NODE) {
let selector = element.nodeName.toLowerCase();
if (element.id && this.checkBadElemId(element.id)) {
selector += `#${element.id}`;
path.unshift(selector);
break;
}
const parent = element.parentElement;
if (parent) {
const siblings = Array.from(parent.children);
const matchingSiblings = siblings.filter(e => e.nodeName.toLowerCase() === selector);
if (matchingSiblings.length > 1) {
const index = siblings.indexOf(element);
selector += `:nth-child(${index + 1})`;
}
}
path.unshift(selector);
element = parent;
}
return path.join(' > ');
},
// --- Automatic Recognition Logic ---
findCaptchaAndInput: function () {
const storedRule = this.getStoredRule();
if (storedRule) {
const $img = $(storedRule.imgSelector);
const $input = $(storedRule.inputSelector);
if ($img.length && $input.length && $img.is(':visible') && $input.is(':visible')) {
console.log('Baidu OCR: Found captcha using stored rule.');
return { img: $img[0], input: $input[0] };
} else {
console.log('Baidu OCR: Stored rule elements not found or not visible.');
}
}
// If no stored rule, or stored rule is invalid, do not search further.
console.log('Baidu OCR: No valid stored rule found for this page. Automatic search is disabled by design.');
return { img: null, input: null };
},
async processCaptcha(imgElement, inputElement, isManual = false) {
if (this.isCaptchaProcessedThisLoad && !isManual) {
console.log('Baidu OCR: Captcha already processed, skipping automatic attempt.');
return;
}
if (!imgElement) {
if (isManual) this.Hint('未找到或未选择验证码图片。');
console.log('Baidu OCR: No image element to process.');
return;
}
const $imgElement = $(imgElement);
const $inputElement = $(inputElement);
const originalImgBorder = $imgElement.css('border');
const originalInputBorder = inputElement ? $inputElement.css('border') : '';
$imgElement.css('border', '2px solid red');
if (inputElement) $inputElement.css('border', '2px solid blue');
this.Hint('正在识别验证码...', 0);
try {
const imageDataBase64 = await this.getImageDataBase64(imgElement);
if (!imageDataBase64) throw new Error('无法获取图片Base64数据。');
const response = await this.recognize(imageDataBase64);
if (response && response.words_result && response.words_result.length > 0) {
const recognizedText = response.words_result.map(word => word.words).join('');
console.log('Baidu OCR: Recognized raw text:', recognizedText);
if (inputElement) {
this.WriteImgCodeResult(recognizedText, inputElement);
this.Hint(`验证码识别成功并已填写: ${recognizedText}`, 3000);
this.isCaptchaProcessedThisLoad = true;
if (this.observer) {
this.observer.disconnect();
console.log('Baidu OCR: Captcha processed. Observer disconnected. Script is now silent for this page load.');
}
if (isManual) {
const imgSelector = this.Aimed(imgElement);
const inputSelector = this.Aimed(inputElement);
if (imgSelector && inputSelector) {
this.storeRule(imgSelector, inputSelector);
this.Hint(`验证码识别成功并已填写: ${recognizedText}
已记住本次选择!`, 5000);
} else {
console.warn('Baidu OCR: Could not generate robust CSS selectors for memory storage. Manual rule not saved.');
}
}
} else {
this.Hint(`验证码识别成功: ${recognizedText}`, 3000);
}
} else {
this.Hint('未能识别到验证码文字。', 3000);
console.log('Baidu OCR: No words recognized.');
}
} catch (error) {
console.error('Baidu OCR: Error during recognition:', error);
this.Hint(`识别验证码时发生错误: ${error.message}
请刷新验证码或手动选择。`, 5000);
} finally {
$imgElement.css('border', originalImgBorder);
if (inputElement) $inputElement.css('border', originalInputBorder);
if (!this.isCaptchaProcessedThisLoad) { this.hideHint(); }
}
},
// --- Event Simulation ---
WriteImgCodeResult: function (ImgCodeResult, WriteInput) {
const $WriteInput = $(WriteInput);
let cleanedResult = ImgCodeResult.replace(/[\u4e00-\u9fa5\s]/g, '');
console.log(`Baidu OCR: Original recognized text: "${ImgCodeResult}", Cleaned text (no Chinese/spaces): "${cleanedResult}"`);
$WriteInput.val(cleanedResult);
const eventNames = ["input", "change", "focus", "invalid", "keypress", "keydown", "keyup", "blur"];
for (const eventName of eventNames) this.Fire($WriteInput[0], eventName);
try {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeInputValueSetter.call($WriteInput[0], cleanedResult);
$WriteInput[0].dispatchEvent(new Event('input', { bubbles: true }));
} catch (e) { /* ignore */ }
},
Fire: function (element, eventName) {
let event;
if (typeof InputEvent === 'function' && eventName === 'input') event = new InputEvent(eventName, { bubbles: true, cancelable: true });
else if (typeof Event === 'function') event = new Event(eventName, { bubbles: true, cancelable: true });
else {
event = document.createEvent("HTMLEvents");
event.initEvent(eventName, true, true);
}
element.dispatchEvent(event);
},
// --- Rule Storage Management ---
getStoredRules: function () { return GM_getValue(this.GM_RULES_KEY, {}); },
getStoredRule: function () {
const rules = this.getStoredRules();
const origin = window.location.origin;
return rules[origin];
},
storeRule: function (imgSelector, inputSelector) {
const rules = this.getStoredRules();
const origin = window.location.origin;
rules[origin] = { imgSelector, inputSelector, timestamp: Date.now() };
GM_setValue(this.GM_RULES_KEY, rules);
console.log(`Baidu OCR: Stored rule for ${origin}:`, rules[origin]);
},
clearStoredRule: function () {
if (!confirm('确定要清除当前网站的验证码识别规则吗?这将使下次访问该网站时无法自动识别,需要重新手动设置。')) return;
const rules = this.getStoredRules();
const origin = window.location.origin;
delete rules[origin];
GM_setValue(this.GM_RULES_KEY, rules);
this.Hint('已清除当前网站的记忆规则。', 3000);
console.log(`Baidu OCR: Cleared rule for ${origin}.`);
},
// --- Manual Selection Mode ---
enableManualSelectMode: function () {
if (this.manualSelectMode) {
this.Hint('手动选择模式已开启。');
return;
}
this.manualSelectMode = true;
this.selectedImageElement = null;
this.Hint('手动选择模式:请点击验证码图片,然后点击对应的输入框。
右键点击图片可直接选择图片。', 0);
const clickHandler = (e) => {
if (!this.manualSelectMode) return;
const target = e.target;
if (target.tagName.toLowerCase() === 'img') {
if (this.selectedImageElement) $(this.selectedImageElement).css('border', '');
this.selectedImageElement = target;
$(this.selectedImageElement).css('border', '2px solid orange');
this.Hint('已选择图片。现在请点击对应的输入框。');
e.preventDefault();
} else if (target.tagName.toLowerCase() === 'input' || target.tagName.toLowerCase() === 'textarea') {
if (this.selectedImageElement) {
$(this.selectedImageElement).css('border', '');
$(target).css('border', '2px solid orange');
this.Hint('已选择输入框。开始识别...');
this.processCaptcha(this.selectedImageElement, target, true);
this.disableManualSelectMode();
e.preventDefault();
} else { this.Hint('请先点击验证码图片。'); }
}
};
const contextMenuHandler = (e) => {
if (!this.manualSelectMode) return;
e.preventDefault();
const target = e.target;
if (target.tagName.toLowerCase() === 'img') {
if (this.selectedImageElement) $(this.selectedImageElement).css('border', '');
this.selectedImageElement = target;
$(this.selectedImageElement).css('border', '2px solid orange');
this.Hint('已通过右键选择图片。现在请点击对应的输入框。');
} else { this.Hint('右键点击的不是图片。请左键点击验证码图片。'); }
};
$(document).on('click.baiduocr', clickHandler);
$(document).on('contextmenu.baiduocr', contextMenuHandler);
},
disableManualSelectMode: function () {
this.manualSelectMode = false;
this.selectedImageElement = null;
$(document).off('click.baiduocr');
$(document).off('contextmenu.baiduocr');
$('img[style*="border: 2px solid orange"]').css('border', '');
$('input[style*="border: 2px solid orange"]').css('border', '');
$('textarea[style*="border: 2px solid orange"]').css('border', '');
this.hideHint();
},
// --- Auto recognition on load/DOM change ---
attemptAutoRecognitionOnLoad: async function () {
setTimeout(async () => {
if (this.isCaptchaProcessedThisLoad) return;
const { img, input } = this.findCaptchaAndInput();
if (img && input) {
console.log('Baidu OCR: Attempting automatic recognition on page load...');
await this.processCaptcha(img, input);
} else {
console.log('Baidu OCR: Automatic recognition skipped on page load (no clear captcha/input pair found).');
}
}, 1000);
},
setupMutationObserver: function () {
if (this.observer) this.observer.disconnect();
this.observer = new MutationObserver((mutations) => {
if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) return;
let needsRecheck = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE && (node.tagName === 'IMG' || node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
needsRecheck = true;
break;
}
}
}
if (mutation.type === 'attributes' && (mutation.target.tagName === 'IMG' || mutation.target.tagName === 'INPUT' || mutation.target.tagName === 'TEXTAREA')) {
if (mutation.attributeName === 'src' || mutation.attributeName === 'style' || mutation.attributeName === 'class') {
needsRecheck = true;
}
}
if (needsRecheck) break;
}
if (needsRecheck) {
clearTimeout(this.domChangeTimer);
this.domChangeTimer = setTimeout(async () => {
if (this.isCaptchaProcessedThisLoad || !globalSettings.enableCaptchaOcr) return;
const { img, input } = this.findCaptchaAndInput();
if (img && input && !$(img).data('ocr_processed')) {
$(img).data('ocr_processed', true);
await this.processCaptcha(img, input);
}
}, 500);
}
});
this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'style', 'class'] });
},
triggerAutoRecognition: async function () {
this.disableManualSelectMode();
const { img, input } = this.findCaptchaAndInput();
if (img) {
this.Hint('尝试自动识别验证码...', 3000);
await this.processCaptcha(img, input);
} else {
this.Hint('未找到已保存规则的验证码图片。请尝试手动选择以创建规则。', 5000);
}
},
// --- Settings UI for unified modal ---
getSettingsHtml: function () {
const apiKey = GM_getValue(this.GM_API_KEY, '');
const secretKey = GM_getValue(this.GM_SECRET_KEY, '');
return `
验证码识别设置
使用百度AI通用文字识别API自动识别验证码,需要填写API Key和Secret Key。
百度AI凭证
通用设置
`;
},
bindSettingsEvents: function ($container) {
$container.on('click', '#' + SCRIPT_PREFIX + 'save_baidu_credentials', async () => {
const apiKey = $('#' + SCRIPT_PREFIX + 'baidu_api_key').val().trim();
const secretKey = $('#' + SCRIPT_PREFIX + 'baidu_secret_key').val().trim();
if (apiKey && secretKey) {
GM_setValue(this.GM_API_KEY, apiKey);
GM_setValue(this.GM_SECRET_KEY, secretKey);
GM_deleteValue(this.GM_TOKEN_KEY);
GM_deleteValue(this.GM_TOKEN_EXPIRY_KEY);
Util.notify('验证码识别', 'API Key 和 Secret Key 已保存。Access Token 已失效,下次将自动获取新Token。', 5000);
} else {
Util.notify('验证码识别', 'API Key 和 Secret Key 不能为空!', 3000);
}
});
$container.on('click', '#' + SCRIPT_PREFIX + 'clear_baidu_credentials', async () => {
if (confirm('确定要清除所有已保存的百度AI凭证吗?这将需要您重新输入凭证才能使用识别功能。')) {
GM_deleteValue(this.GM_API_KEY);
GM_deleteValue(this.GM_SECRET_KEY);
GM_deleteValue(this.GM_TOKEN_KEY);
GM_deleteValue(this.GM_TOKEN_EXPIRY_KEY);
$('#' + SCRIPT_PREFIX + 'baidu_api_key').val('');
$('#' + SCRIPT_PREFIX + 'baidu_secret_key').val('');
Util.notify('验证码识别', '所有百度AI凭证已清除。', 5000);
}
});
$container.on('click', '#' + SCRIPT_PREFIX + 'save_ocr_general_settings', () => {
this.ocrSettings.showHintCheck = $('#' + SCRIPT_PREFIX + 'setting_showHintCheck').prop('checked');
GM_setValue(this.GM_OCR_SETTINGS_KEY, this.ocrSettings);
Util.notify('验证码识别', '通用设置已保存。', 3000);
});
},
loadSettingsToUI: function ($container) {
this.ocrSettings = GM_getValue(this.GM_OCR_SETTINGS_KEY, this.DEFAULT_OCR_SETTINGS);
$container.find('#' + SCRIPT_PREFIX + 'setting_showHintCheck').prop('checked', this.ocrSettings.showHintCheck);
}
};
// --- 模块 2: Steam游戏入库 (Steam Game Entry) ---
const SteamGameEntry = {
GM_GITHUB_TOKEN: SCRIPT_PREFIX + 'github_token',
GM_BUTTON_POS_STEAM: SCRIPT_PREFIX + 'button_pos_steam',
GM_BUTTON_POS_STEAMDB: SCRIPT_PREFIX + 'button_pos_steamdb',
GM_SETTINGS_FIX_MANIFEST_VERSION: SCRIPT_PREFIX + 'setting_fix_manifest_version',
GM_SETTINGS_DOWNLOAD_LUA_ONLY: SCRIPT_PREFIX + 'setting_download_lua_only',
GM_SETTINGS_SELECTED_MANIFEST_REPO: SCRIPT_PREFIX + 'setting_selected_manifest_repo',
CORE_GITHUB_MANIFEST_REPOS: [
{ "name": "清单网站1", "url": "SteamAutoCracks/ManifestHub", "type": "branches" },
{ "name": "清单网站2", "url": "hansaes/ManifestAutoUpdate", "type": "branches" }
],
RETRY_LIMIT: 3,
RETRY_DELAY_MS: 2000,
DRAG_THRESHOLD: 5,
APP_ID_REGEX_STEAM: /\/app\/(\d+)/,
APP_ID_REGEX_STEAMDB: /\/app\/(\d+)/,
APP_ID_REGEX_CLIPBOARD: /^\d{1,10}$/,
githubToken: '',
storedButtonPosition: null,
setting_fixManifestVersion: true,
setting_downloadLuaOnly: true,
setting_selectedManifestRepo: '',
currentAppId: null,
clipboardAppId: null,
activeAppId: null,
// UI elements
storageBtn: null,
statusBox: null,
refreshStatusBtn: null,
// 【修复】将 init 函数改为 async,以正确处理异步的设置加载
init: async function () {
if (!globalSettings.enableSteamGameEntry) return;
const isSteamPage = window.location.href.match(/https:\/\/(store\.steampowered\.com\/app\/|steamdb\.info\/app\/|steamui\.com\/)/);
if (!isSteamPage) return;
// 【修复】使用 await 等待设置加载完成,确保后续操作使用正确的设置值
await this.loadSettings();
this.createUI();
this.currentAppId = this.getAppIdFromUrl();
document.addEventListener('paste', this.handlePasteEvent.bind(this));
// Initial update active App ID
if (this.currentAppId) {
await this.updateActiveAppId(this.currentAppId, 'url');
} else {
Util.readClipboard().then(async text => {
if (text && this.APP_ID_REGEX_CLIPBOARD.test(text.trim())) {
const id = text.trim();
if (id !== this.activeAppId) {
if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${id},是否以此App ID进行操作?`)) {
this.clipboardAppId = id;
await this.updateActiveAppId(this.clipboardAppId, 'clipboard');
}
}
}
});
}
},
async loadSettings() {
this.githubToken = await GM_getValue(this.GM_GITHUB_TOKEN, '');
this.setting_fixManifestVersion = await GM_getValue(this.GM_SETTINGS_FIX_MANIFEST_VERSION, true);
this.setting_downloadLuaOnly = await GM_getValue(this.GM_SETTINGS_DOWNLOAD_LUA_ONLY, true);
this.setting_selectedManifestRepo = await GM_getValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.CORE_GITHUB_MANIFEST_REPOS[0].name);
// 确保所选仓库在列表中,如果不存在则重置为第一个
if (!this.CORE_GITHUB_MANIFEST_REPOS.some(repo => repo.name === this.setting_selectedManifestRepo)) {
this.setting_selectedManifestRepo = this.CORE_GITHUB_MANIFEST_REPOS[0].name;
await GM_setValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.setting_selectedManifestRepo);
}
const positionKey = window.location.hostname === 'steamdb.info' ? this.GM_BUTTON_POS_STEAMDB : this.GM_BUTTON_POS_STEAM;
this.storedButtonPosition = await GM_getValue(positionKey, null);
console.log('Steam Entry: Settings loaded.', {
githubToken: this.githubToken ? 'Set' : 'Not Set',
fixManifestVersion: this.setting_fixManifestVersion,
downloadLuaOnly: this.setting_downloadLuaOnly,
selectedManifestRepo: this.setting_selectedManifestRepo,
storedButtonPosition: this.storedButtonPosition
});
},
createUI: function () {
this.storageBtn = $('').appendTo('body')[0];
this.statusBox = $('加载中
').appendTo('body')[0];
this.refreshStatusBtn = $('').appendTo('body')[0];
// Attach companion elements to the main button's jQuery data for easier management by makeDraggable
$(this.storageBtn).data('companionElements', [$(this.statusBox), $(this.refreshStatusBtn)]);
$(this.storageBtn).data('updateCompanionPositions', this.updateCompanionButtonPositions.bind(this));
const positionKey = window.location.hostname === 'steamdb.info' ? this.GM_BUTTON_POS_STEAMDB : this.GM_BUTTON_POS_STEAM;
const defaultButtonTop = window.location.hostname === 'steamdb.info' ? 120 : 20; // SteamDB often has header elements
Util.applyButtonPosition(this.storageBtn, this.storedButtonPosition, defaultButtonTop);
this.updateCompanionButtonPositions(); // Position companion elements relative to the main button
// Event Listeners
// Use the unified makeDraggable
Util.makeDraggable(this.storageBtn, positionKey, () => {
this.downloadManifest(this.activeAppId, this.setting_selectedManifestRepo); // Click action
});
$(this.refreshStatusBtn).on('click', () => this.updateStatusBox());
},
// Helper to update positions of companion elements
updateCompanionButtonPositions: function () {
if (!this.storageBtn || !this.statusBox || !this.refreshStatusBtn) return;
const btnRect = this.storageBtn.getBoundingClientRect();
const spacing = 10;
// Position status box to the left of the main button
this.statusBox.style.left = `${btnRect.left - this.statusBox.offsetWidth - spacing}px`;
this.statusBox.style.top = `${btnRect.top}px`;
this.statusBox.style.right = 'auto'; // Ensure right is auto
// Position refresh button below the status box, also to the left of main button
this.refreshStatusBtn.style.left = `${btnRect.left - this.refreshStatusBtn.offsetWidth - spacing}px`;
this.refreshStatusBtn.style.top = `${btnRect.top + this.statusBox.offsetHeight + 5}px`;
this.refreshStatusBtn.style.right = 'auto'; // Ensure right is auto
},
// --- Core Functions ---
getAppIdFromUrl: function () {
const url = window.location.href;
let match;
if (url.startsWith('https://store.steampowered.com/app/')) {
match = url.match(this.APP_ID_REGEX_STEAM);
} else if (url.startsWith('https://steamdb.info/app/')) {
match = url.match(this.APP_ID_REGEX_STEAMDB);
} else if (url.startsWith('https://steamui.com/')) { // SteamUI match
match = url.match(this.APP_ID_REGEX_STEAM); // Reuse Steam Regex
}
return match ? String(match[1]) : null;
},
updateActiveAppId: async function (newAppId, source) {
if (this.activeAppId === newAppId && source !== 'url') return;
this.activeAppId = newAppId;
console.log(`Steam Entry: Active App ID updated to ${this.activeAppId || 'N/A'} (Source: ${source})`);
if (this.storageBtn) {
// Main button text simplified as it's a direct download button
this.storageBtn.textContent = '入库'; // 总是显示“入库”
this.storageBtn.disabled = !this.activeAppId;
}
await this.updateStatusBox();
},
handlePasteEvent: async function (event) {
const pastedText = event.clipboardData ? event.clipboardData.getData('text') : null;
if (pastedText && this.APP_ID_REGEX_CLIPBOARD.test(pastedText.trim())) {
const newClipboardAppId = pastedText.trim();
if (newClipboardAppId !== this.activeAppId) {
if (confirm(`朝阳的工具: 检测到剪贴板App ID: ${newClipboardAppId},是否以此App ID进行操作?`)) {
this.clipboardAppId = newClipboardAppId;
await this.updateActiveAppId(this.clipboardAppId, 'clipboard');
Util.notify('朝阳的工具', `已识别剪贴板App ID: ${this.clipboardAppId}`, 3000);
}
} else {
console.log(`Steam Entry: Clipboard App ID ${newClipboardAppId} is same as active, skipping.`);
}
}
},
// --- GitHub API Calls ---
async makeGitHubApiRequestWithRetry(url, repoName, method = 'GET', retries = 0) {
const headers = {
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28'
};
if (this.githubToken) {
headers['Authorization'] = `token ${this.githubToken}`;
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
headers: headers,
timeout: 10000,
onload: async (res) => {
const remaining = res.responseHeaders.match(/x-ratelimit-remaining:\s*(\d+)/i);
const reset = res.responseHeaders.match(/x-ratelimit-reset:\s*(\d+)/i);
if (remaining && reset) {
const resetTime = new Date(parseInt(reset[1]) * 1000);
if (parseInt(remaining[1]) < 100) {
console.warn(`Steam Entry: [${repoName}] Rate limit: ${remaining[1]} remaining, resets ${resetTime.toLocaleTimeString()}`);
if (parseInt(remaining[1]) === 0) Util.notify('朝阳的工具: 速率限制警告', `您的GitHub API请求已达到速率限制,将在 ${resetTime.toLocaleTimeString()} 后重置。请检查您的Token设置。`, 8000);
}
}
if (res.status === 200) resolve({ data: (method === 'HEAD' ? null : res.responseText), headers: res.responseHeaders, status: res.status });
else if (res.status === 403 || res.status === 429 || res.status >= 500) {
if (retries < this.RETRY_LIMIT) {
console.warn(`Steam Entry: [${repoName}] Request failed, status ${res.status} (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`);
await Util.sleep(this.RETRY_DELAY_MS * (retries + 1));
this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject);
} else {
console.error(`Steam Entry: [${repoName}] Request failed, status ${res.status} (max retries). URL: ${url}`);
Util.notify('朝阳的工具: 请求失败', `[${repoName}] GitHub API请求失败,状态码 ${res.status} (已达最大重试次数)。请检查网络或Token。`, 5000);
reject(new Error(`Failed to fetch: ${res.status} (Max retries exceeded)`));
}
} else if (res.status === 404) resolve({ data: null, headers: res.responseHeaders, status: res.status });
else {
console.error(`Steam Entry: [${repoName}] Request failed, status: ${res.status}, URL: ${url}`);
Util.notify('朝阳的工具: 请求失败', `[${repoName}] GitHub API请求失败,状态码: ${res.status}。`, 5000);
reject(new Error(`Failed to fetch: ${res.status}`));
}
},
onerror: async (err) => {
if (retries < this.RETRY_LIMIT) {
console.warn(`Steam Entry: [${repoName}] Request error (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`, err);
await Util.sleep(this.RETRY_DELAY_MS * (retries + 1));
this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject);
} else {
console.error(`Steam Entry: [${repoName}] Request error (max retries). URL: ${url}`, err);
Util.notify('朝阳的工具: 网络错误', `[${repoName}] GitHub API请求发生网络错误。`, 5000);
reject(new Error('Request error (Max retries exceeded): ' + JSON.stringify(err)));
}
},
ontimeout: async () => {
if (retries < this.RETRY_LIMIT) {
console.warn(`Steam Entry: [${repoName}] Request timeout (retry ${retries + 1}/${this.RETRY_LIMIT}). URL: ${url}`);
await Util.sleep(this.RETRY_DELAY_MS * (retries + 1));
this.makeGitHubApiRequestWithRetry(url, repoName, method, retries + 1).then(resolve).catch(reject);
} else {
console.error(`Steam Entry: [${repoName}] Request timeout (max retries). URL: ${url}`);
Util.notify('朝阳的工具: 请求超时', `[${repoName}] GitHub API请求超时。`, 5000);
reject(new Error('Request Timeout (Max retries exceeded)'));
}
}
});
});
},
async getAppIdManifestInfo(appId) {
const targetAppId = String(appId);
const results = {};
let latestSource = null;
const promises = this.CORE_GITHUB_MANIFEST_REPOS.map(async (repoInfo) => {
const ownerRepoParts = repoInfo.url.split('/');
const owner = ownerRepoParts[0];
const repo = ownerRepoParts[1];
const repoName = repoInfo.name;
let repoStatus = -1;
try {
const branchCheckUrl = `https://api.github.com/repos/${owner}/${repo}/branches/${targetAppId}`;
const response = await this.makeGitHubApiRequestWithRetry(branchCheckUrl, repoName, 'HEAD');
if (response.status === 200) {
repoStatus = 1;
if (!latestSource) latestSource = repoName;
console.log(`Steam Entry: App ID ${targetAppId} in ${repoName} exists.`);
} else if (response.status === 404) {
repoStatus = 0;
console.log(`Steam Entry: App ID ${targetAppId} in ${repoName} does not exist (404).`);
} else {
console.warn(`Steam Entry: App ID ${targetAppId} in ${repoName} check returned unexpected status: ${response.status}`);
repoStatus = -1;
}
} catch (error) {
console.error(`Steam Entry: Failed to check App ID ${targetAppId} in ${repoName}:`, error);
repoStatus = -1;
}
return { repoName, status: repoStatus };
});
const allResults = await Promise.all(promises);
for (const res of allResults) {
results[res.repoName] = res.status;
}
return { manifests: results, latestSource: latestSource };
},
async downloadManifest(appId, sourceOption) {
if (!appId) {
Util.notify('朝阳的工具', '未指定App ID。', 3000);
return;
}
const repoToDownload = this.CORE_GITHUB_MANIFEST_REPOS.find(r => r.name === sourceOption);
if (!repoToDownload) {
Util.notify('朝阳的工具', `无法找到 App ID ${appId} 的下载链接或指定来源: ${sourceOption}。`, 5000);
console.error(`Steam Entry: Cannot find download link for App ID ${appId} or source: ${sourceOption}`);
return;
}
const ownerRepo = repoToDownload.url.split('/');
let downloadUrl = '';
let fileName = '';
let responseType = '';
let processContent = null;
if (this.setting_downloadLuaOnly) {
downloadUrl = `https://raw.githubusercontent.com/${ownerRepo[0]}/${ownerRepo[1]}/${String(appId)}/${String(appId)}.lua`;
fileName = `${appId}.lua`;
responseType = 'text';
processContent = (content) => {
// 【修正】使用 `this.setting_fixManifestVersion` 来判断,而不是取反
if (!this.setting_fixManifestVersion) {
console.log(`Steam Entry: Commenting out setManifestid.`);
return content.replace(/setManifestid/g, '--setManifestid');
}
console.log(`Steam Entry: Not commenting out setManifestid.`);
return content;
};
} else {
downloadUrl = `https://github.com/${ownerRepo[0]}/${ownerRepo[1]}/archive/refs/heads/${String(appId)}.zip`;
fileName = `${appId}.zip`;
responseType = 'arraybuffer';
processContent = (content) => content;
}
console.log(`Steam Entry: Preparing to download App ID ${appId} from ${sourceOption}. URL: ${downloadUrl}`);
Util.notify('朝阳的工具', `正在从 ${sourceOption} 下载 ${fileName}...`, 3000);
GM_xmlhttpRequest({
method: 'GET',
url: downloadUrl,
responseType: responseType,
onload: (response) => {
if (response.status === 200) {
let fileContent = response.response;
if (this.setting_downloadLuaOnly && responseType === 'text') {
fileContent = processContent(fileContent);
}
console.log(`Steam Entry: File content fetched, preparing to download.`);
const blob = new Blob([fileContent], { type: this.setting_downloadLuaOnly ? 'text/plain;charset=utf-8' : 'application/zip' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
Util.notify('朝阳的工具', `App ID ${appId} 的${this.setting_downloadLuaOnly ? 'Lua文件' : 'ZIP文件'}下载成功!`, 5000);
} else if (response.status === 404) {
console.error(`Steam Entry: Download App ID ${appId} failed, file not found (404 Not Found). URL: ${downloadUrl}`);
Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件失败,指定清单中不存在该App ID的${this.setting_downloadLuaOnly ? 'Lua文件' : 'ZIP分支'}。`, 5000);
} else {
console.error(`Steam Entry: Download App ID ${appId} failed, status: ${response.status}. URL: ${downloadUrl}`);
Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件失败,请检查网络或文件是否存在 (HTTP ${response.status})。`, 5000);
}
},
onerror: (error) => {
console.error(`Steam Entry: Error downloading App ID ${appId}:`, error);
Util.notify('朝阳的工具', `下载 App ID ${appId} 的文件时发生网络错误。`, 5000);
}
});
},
async updateStatusBox() {
if (!this.statusBox) return;
if (!this.activeAppId) {
this.statusBox.textContent = 'N/A';
this.statusBox.style.backgroundColor = '#6c757d';
return;
}
this.statusBox.textContent = '检查中';
this.statusBox.style.backgroundColor = '#ffc107';
const { latestSource } = await this.getAppIdManifestInfo(this.activeAppId);
if (latestSource) {
this.statusBox.textContent = '存在';
this.statusBox.style.backgroundColor = '#4CAF50';
} else {
this.statusBox.textContent = '没有';
this.statusBox.style.backgroundColor = '#f44336';
}
},
async fetchAndDownloadDlcIds() {
if (!this.activeAppId) {
Util.notify('朝阳的工具', '无法获取当前游戏App ID。请在Steam或SteamDB游戏详情页使用此功能,或通过剪贴板识别。', 5000);
return;
}
console.log('Steam Entry: Fetching DLC IDs...');
const dlcIds = new Set();
const hostname = window.location.hostname;
if (hostname === 'steamdb.info') {
document.querySelectorAll('#dlc tr[data-appid]').forEach(row => {
const dlcId = row.getAttribute('data-appid');
if (dlcId) dlcIds.add(dlcId.trim());
});
} else if (hostname === 'store.steampowered.com') {
document.querySelectorAll('.game_area_dlc_row').forEach(row => {
const dlcId = row.getAttribute('data-ds-appid');
if (dlcId) dlcIds.add(dlcId.trim());
else {
const link = row.querySelector('a');
if (link && link.href) {
const match = link.href.match(/\/app\/(\d+)/);
if (match && match[1]) dlcIds.add(match[1]);
}
}
});
} else if (hostname === 'steamui.com') {
const potentialDlcElements = document.querySelectorAll('[data-appid-dlc], .dlc-list-item a, .dlc-id-text');
potentialDlcElements.forEach(el => {
let dlcId = el.getAttribute('data-appid-dlc') || el.textContent.trim();
const linkMatch = el.href ? el.href.match(/\/app\/(\d+)/) : null;
if (linkMatch && linkMatch[1]) dlcId = linkMatch[1];
if (dlcId && this.APP_ID_REGEX_CLIPBOARD.test(dlcId)) dlcIds.add(dlcId);
});
if (dlcIds.size === 0) {
Util.notify('朝阳的工具', '在 steamui.com 页面未找到DLC信息。请确保您当前查看的是有DLC列表的页面。该站点的DLC识别功能依赖于页面结构,可能不够完善。', 8000);
return;
}
} else {
Util.notify('朝阳的工具', '当前页面不支持自动获取DLC ID。请在Steam商店、SteamDB或SteamUI页面使用此功能。', 5000);
return;
}
const dlcIdArray = Array.from(dlcIds);
console.log(`Steam Entry: Found ${dlcIdArray.length} unique DLC IDs.`);
if (dlcIdArray.length === 0) {
Util.notify('朝阳的工具', '未在此页面上找到任何DLC。', 3000);
return;
}
const fileContent = dlcIdArray.join('\n');
const blob = new Blob([fileContent], { type: 'text/plain;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `dlc_${this.activeAppId}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
Util.notify('朝阳的工具', `成功获取并下载了 ${dlcIdArray.length} 个DLC ID。`, 5000);
},
// --- Settings UI for unified modal ---
getSettingsHtml: function () {
let repoOptionsHtml = this.CORE_GITHUB_MANIFEST_REPOS.map(repo => `
`).join('');
return `
Steam游戏入库设置
提供清单文件下载,需要GitHub Personal Access Token以获得更高的API请求额度,避免下载失败。
清单下载选项
选择下载源
${repoOptionsHtml}
`;
},
bindSettingsEvents: function ($container) {
$container.on('click', '#' + SCRIPT_PREFIX + 'steam_save_github_token', async () => {
const tokenValue = $('#' + SCRIPT_PREFIX + 'steam_github_token_input').val().trim();
this.githubToken = tokenValue;
await GM_setValue(this.GM_GITHUB_TOKEN, this.githubToken);
this.loadSettingsToUI($container);
Util.notify('Steam入库', 'GitHub Token已保存。', 3000);
});
$container.on('click', '#' + SCRIPT_PREFIX + 'steam_clear_github_token', async () => {
if (confirm('确定要清除您的GitHub Token吗?这将可能导致API速率限制。')) {
await GM_deleteValue(this.GM_GITHUB_TOKEN);
this.githubToken = '';
this.loadSettingsToUI($container);
Util.notify('Steam入库', 'GitHub Token已清除。', 3000);
}
});
$container.on('change', '#' + SCRIPT_PREFIX + 'steam_setting_fixManifestVersion', async (e) => {
this.setting_fixManifestVersion = $(e.target).prop('checked');
await GM_setValue(this.GM_SETTINGS_FIX_MANIFEST_VERSION, this.setting_fixManifestVersion);
Util.notify('Steam入库', '固定清单版本设置已保存。', 2000);
});
$container.on('change', '#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly', async (e) => {
this.setting_downloadLuaOnly = $(e.target).prop('checked');
await GM_setValue(this.GM_SETTINGS_DOWNLOAD_LUA_ONLY, this.setting_downloadLuaOnly);
Util.notify('Steam入库', '下载文件类型设置已保存。', 2000);
});
$container.on('change', `input[name="${SCRIPT_PREFIX}steam_download_option"]`, async (e) => {
this.setting_selectedManifestRepo = $(e.target).val();
await GM_setValue(this.GM_SETTINGS_SELECTED_MANIFEST_REPO, this.setting_selectedManifestRepo);
Util.notify('Steam入库', `默认下载源已更新为: ${this.setting_selectedManifestRepo}`, 2000);
});
$container.on('click', '#' + SCRIPT_PREFIX + 'steam_fetch_dlc_btn', this.fetchAndDownloadDlcIds.bind(this));
},
loadSettingsToUI: function ($container) {
$container.find('#' + SCRIPT_PREFIX + 'steam_github_token_input').val(this.githubToken);
$container.find('#' + SCRIPT_PREFIX + 'steam_current_token_display').text(this.githubToken ? '已设置' : '未设置').css('color', this.githubToken ? '#4CAF50' : '#f44336');
$container.find('#' + SCRIPT_PREFIX + 'steam_setting_fixManifestVersion').prop('checked', this.setting_fixManifestVersion);
$container.find('#' + SCRIPT_PREFIX + 'steam_setting_downloadLuaOnly').prop('checked', this.setting_downloadLuaOnly);
$container.find(`input[name="${SCRIPT_PREFIX}steam_download_option"][value="${this.setting_selectedManifestRepo}"]`).prop('checked', true);
}
};
// --- 模块 3: 视频解析接口 (Video Parser) ---
const VideoParser = {
GM_CUSTOM_API_LIST_KEY: SCRIPT_PREFIX + 'video_parser_custom_api_list',
GM_VIP_BOX_POSITION: SCRIPT_PREFIX + 'vip_box_position',
DEFAULT_VIP_BOX_POSITION: { "left": null, "top": 200, "right": 20 }, // 默认位置
VIDEO_PARSE_LIST: [
{ "name": "HLS解析", "url": "https://jx.hls.one/?url=" },
{ "name": "冰豆解析", "url": "https://bd.jx.cn/?url=" },
{ "name": "虾米视频", "url": "https://jx.xmflv.com/?url=" },
{ "name": "UTO解析", "url": "https://jx.nnsvip.cn/?url=" },
{ "name": "CK解析", "url": "https://www.ckplayer.vip/jiexi/?url=" },
{ "name": "七哥解析", "url": "https://jx.nnxv.cn/tv.php?url=" },
{ "name": "夜幕解析", "url": "https://www.yemu.xyz/?url=" },
{ "name": "盘古解析", "url": "https://www.pangujiexi.com/jiexi/?url=" },
{ "name": "8090解析", "url": "https://www.8090g.cn/?url=" },
{ "name": "playm3u8", "url": "https://www.playm3u8.cn/jiexi.php?url=" },
{ "name": "七七云解析", "url": "https://jx.77flv.cc/?url=" },
{ "name": "极速解析", "url": "https://jx.2s0.cn/player/?url=" },
{ "name": "Player-JY", "url": "https://jx.playerjy.com/?url=" },
],
customApiList: [],
mergedApiList: [],
// 浮动按钮和列表的DOM元素
entryBtn: null,
listModal: null,
// 匹配的视频网站列表 (从原脚本复制,并统一为字符串匹配)
MATCHED_VIDEO_HOSTS: [
'v.qq.com', 'm.v.qq.com', 'iqiyi.com', 'm.iqiyi.com', 'iq.com',
'youku.com', 'm.youku.com', 'mgtv.com', 'm.mgtv.com',
'tudou.com', 'film.sohu.com', 'tv.sohu.com', 'm.tv.sohu.com', 'm.film.sohu.com',
'bilibili.com', 'm.bilibili.com', 'v.pptv.com', 'vip.pptv.com', 'm.pptv.com',
'wasu.cn', 'le.com', 'acfun.cn', '1905.com'
],
init: function () {
if (!globalSettings.enableVideoParser) return;
// 检查当前页面是否匹配视频解析的规则
const currentHostname = Util.getHostname(window.location.href);
const isMatchedVideoPage = this.MATCHED_VIDEO_HOSTS.some(host => currentHostname.includes(host));
if (!isMatchedVideoPage) {
console.log("Video Parser: Current page is not a matched video page, skipping UI creation.");
return;
}
this.loadCustomApis();
this.mergedApiList = [...this.VIDEO_PARSE_LIST, ...this.customApiList.map(api => ({ ...api, custom: true }))];
// 创建浮动入口按钮和解析列表模态框
this.createEntryButton();
this.createParserListModal();
},
loadCustomApis: function () {
this.customApiList = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []);
},
createEntryButton: function () {
this.entryBtn = $(`▶
`).appendTo('body')[0];
const defaultTop = 200;
const defaultRight = 20;
const savedPos = GM_getValue(this.GM_VIP_BOX_POSITION, this.DEFAULT_VIP_BOX_POSITION);
Util.applyButtonPosition(this.entryBtn, savedPos, defaultTop, defaultRight);
// Use the unified makeDraggable
Util.makeDraggable(this.entryBtn, this.GM_VIP_BOX_POSITION, this.toggleListModal.bind(this));
},
createParserListModal: function () {
this.listModal = $(`
`).appendTo('body')[0];
this.updateParserListUI();
$(this.listModal).on('click', '.close-button', this.closeListModal.bind(this));
},
toggleListModal: function () {
if ($(this.listModal).css('display') === 'none') {
this.updateParserListUI(); // 每次打开都刷新列表,确保自定义接口最新
$(this.listModal).css('display', 'block');
} else {
$(this.listModal).css('display', 'none');
}
},
closeListModal: function () {
$(this.listModal).css('display', 'none');
},
updateParserListUI: function () {
this.loadCustomApis(); // Ensure custom APIs are fresh
this.mergedApiList = [...this.VIDEO_PARSE_LIST, ...this.customApiList.map(api => ({ ...api, custom: true }))];
const $ul = $(`#${SCRIPT_PREFIX}parser_list_ul`);
$ul.empty();
this.mergedApiList.forEach((item, index) => {
const $li = $(`${item.name}`);
$li.on('click', () => this.openParserUrl(item));
$ul.append($li);
});
},
openParserUrl: function (videoObj) {
let url = videoObj.url + encodeURIComponent(window.location.href);
Util.openTab(url);
this.closeListModal();
},
// --- Settings UI for unified modal ---
getSettingsHtml: function () {
this.loadCustomApis();
let customApiListHtml = this.customApiList.map(api => `
${api.name} X
`).join('');
return `
视频解析接口设置
管理视频解析的自定义接口,并提供一个测试解析的入口。
添加自定义接口
`;
},
bindSettingsEvents: function ($container) {
$container.on('click', '#' + SCRIPT_PREFIX + 'add_custom_api_btn', () => {
const name = $('#' + SCRIPT_PREFIX + 'custom_api_name').val().trim();
const url = $('#' + SCRIPT_PREFIX + 'custom_api_url').val().trim();
if (!name || !url) {
Util.notify('视频解析', '接口名称和地址均不能为空!', 3000);
return;
}
if (!url.includes('?url=')) {
if (!confirm('接口地址通常包含 "?url=",您确定要添加吗?')) {
return;
}
}
const newApi = { name: name, url: url };
let customApis = GM_getValue(this.GM_CUSTOM_API_LIST_KEY, []);
if (customApis.some(api => api.url === url)) {
Util.notify('视频解析', '该接口地址已存在!', 3000);
return;
}
customApis.push(newApi);
GM_setValue(this.GM_CUSTOM_API_LIST_KEY, customApis);
Util.notify('视频解析', '接口添加成功!', 3000);
SettingsManager.renderSettingsModules(); // 刷新设置模块UI
});
$container.on('click', '#' + SCRIPT_PREFIX + 'custom_api_list .delete-api-btn', function () {
if (!confirm('您确定要删除这个自定义接口吗?')) return;
const urlToDelete = $(this).data('url');
let customApis = GM_getValue(VideoParser.GM_CUSTOM_API_LIST_KEY, []);
customApis = customApis.filter(api => api.url !== urlToDelete);
GM_setValue(VideoParser.GM_CUSTOM_API_LIST_KEY, customApis);
Util.notify('视频解析', '接口删除成功!', 3000);
SettingsManager.renderSettingsModules(); // 刷新设置模块UI
});
$container.on('click', '#' + SCRIPT_PREFIX + 'open_parser_entry_btn', () => {
SettingsManager.closeModal(); // 关闭设置模态框
if (this.entryBtn) {
this.toggleListModal(); // 打开或关闭解析列表
} else {
Util.notify('视频解析', '视频解析入口按钮未显示,请确认当前页面为支持的视频网站。', 5000);
}
});
},
loadSettingsToUI: function ($container) {
this.loadCustomApis();
let customApiListHtml = this.customApiList.map(api => `
${api.name} X
`).join('');
$container.find('#' + SCRIPT_PREFIX + 'custom_api_list').html(customApiListHtml);
$container.find('#' + SCRIPT_PREFIX + 'custom_api_name').val('');
$container.find('#' + SCRIPT_PREFIX + 'custom_api_url').val('');
}
};
// --- 主入口:脚本初始化 ---
$(document).ready(async function () {
// 1. 加载全局设置
globalSettings = await GM_getValue(GM_SETTINGS_KEY, DEFAULT_GLOBAL_SETTINGS);
console.log('朝阳的工具: Global settings loaded:', globalSettings);
// 2. 初始化统一设置管理器 (注册Tampermonkey菜单命令)
SettingsManager.init();
// 3. 根据全局设置初始化各个模块
if (globalSettings.enableCaptchaOcr) {
CaptchaOcr.init();
console.log('朝阳的工具: Captcha OCR module initialized.');
} else {
console.log('朝阳的工具: Captcha OCR module disabled by settings.');
}
if (globalSettings.enableSteamGameEntry) {
// 【修复】使用 await 等待 Steam 模块初始化完成
await SteamGameEntry.init();
console.log('朝阳的工具: Steam Game Entry module initialized.');
} else {
console.log('朝阳的工具: Steam Game Entry module disabled by settings.');
}
if (globalSettings.enableVideoParser) {
VideoParser.init();
console.log('朝阳的工具: Video Parser module initialized.');
} else {
console.log('朝阳的工具: Video Parser module disabled by settings.');
}
console.log('朝阳的工具: All modules initialized based on settings.');
});
})();