Extend VPS Expiration

Automatically renews the expiration date of free Xserver VPS.

目前為 2025-11-03 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Extend VPS Expiration
// @name:zh-CN   Xserver VPS 自动续期脚本
// @namespace    http://greasyfork.org/
// @version      2025-11-03
// @description  Automatically renews the expiration date of free Xserver VPS.
// @description:zh-CN 自动为 Xserver 的免费 VPS 续期。
// @author       You
// @match        https://secure.xserver.ne.jp/xapanel*/xvps*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=xserver.ne.jp
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license MIT
// ==/UserScript==
/*
 * =================================================================================================
 * 使用说明 (Usage Instructions)
 * =================================================================================================
 * 1. 请将登录页面设为浏览器书签: https://secure.xserver.ne.jp/xapanel/login/xvps/
 * (Bookmark the login page)
 *
 * 2. 每天访问一次该书签。
 * (Visit the bookmark once every day.)
 *
 * 3. (可选) 首次访问时,在登录页面输入您的邮箱和密码,脚本会自动保存。之后访问将自动填充和登录。
 * (Optional) On your first visit, enter your email and password on the login page.
 * The script will save them automatically for future auto-login.
 *
 * =================================================================================================
 * 工作流程 (Workflow)
 * =================================================================================================
 * 1. 登录页面: 自动填充已保存的凭据并提交。
 * (Login Page: Auto-fills saved credentials and submits.)
 *
 * 2. VPS管理主页: 检查免费VPS的到期日期。如果明天到期,则跳转到续期页面。
 * (VPS Dashboard: Checks the expiration date. If it expires tomorrow, it navigates to the renewal page.)
 *
 * 3. 续期申请页: 自动点击“确认”按钮,进入验证码页面。
 * (Renewal Page: Clicks the confirmation button to proceed to the CAPTCHA page.)
 *
 * 4. 验证码页:
 * a. 提取验证码图片。
 * b. 发送到外部API服务进行识别。
 * c. 自动填充识别结果。
 * d. 监听 Cloudflare Turnstile (一种人机验证) 的令牌生成,一旦生成,立即提交表单。
 * (CAPTCHA Page: Extracts the CAPTCHA image, sends it to a recognition service,
 * fills the result, and submits the form once the Cloudflare Turnstile token is ready.)
 * =================================================================================================
 */

// 翻译
function t(text) {
    const translations = {
        '正在处理登录...': { en: 'Processing login...', ja: 'ログインを処理しています...', },
        '已检测到保存凭据,正在自动登录...': { en: 'Saved credentials detected, automatically logging in...', ja: '保存された認証情報を検出しました。自動ログイン中...', },
        '警告:登录函数异常,请手动登录。': { en: 'Warning: login function error, please log in manually.', ja: '警告:ログイン機能に異常が発生しました。手動でログインしてください。', },
        '自动登录失败,请手动登录。': { en: 'Automatic login failed, please log in manually.', ja: '自動ログインに失敗しました。手動でログインしてください。', },
        '正在检查续期状态...': { en: 'Checking renewal status...', ja: '更新状況を確認しています...', },
        '未找到免费VPS。': { en: 'No free VPS found.', ja: '無料VPSが見つかりませんでした。', },
        '检测到即将过期,正在续期...': { en: 'Detected imminent expiration, renewing...', ja: '期限切れが間近であることを検出しました。更新中...', },
        '当前VPS无需续期。': { en: 'Current VPS does not require renewal.', ja: '現在のVPSは更新不要です。', },
        '检查续期状态出错,请刷新页面重试。': { en: 'Error checking renewal status, please refresh the page and try again.', ja: '更新状況の確認中にエラーが発生しました。ページをリロードして再試行してください。', },
        '正在准备续期申请...': { en: 'Preparing renewal request...', ja: '更新リクエストを準備しています...', },
        '正在确认续期协议...': { en: 'Confirming renewal agreement...', ja: '更新契約を確認しています...', },
        '续期申请页面交互失败。': { en: 'Failed to interact with the renewal request page.', ja: '更新申請ページの操作に失敗しました。', },
        '正在识别并输入验证码...': { en: 'Recognizing and entering CAPTCHA...', ja: 'CAPTCHAを認識して入力しています...', },
        '正在识别验证码,请稍候...': { en: 'Recognizing CAPTCHA, please wait...', ja: 'CAPTCHAを認識しています。しばらくお待ちください...', },
        '验证码识别完成,准备提交表单...': { en: 'CAPTCHA recognition complete, preparing to submit form...', ja: 'CAPTCHAの認識が完了しました。フォームを送信する準備をしています...', },
        '已完成验证码填写,正在处理人机验证...': { en: 'CAPTCHA entry complete, processing human verification...', ja: 'CAPTCHAの入力が完了しました。人間認証を処理中...', },
        '等待人机验证令牌生成...': { en: 'Waiting for human verification token generation...', ja: '人間認証トークンの生成を待っています...', },
        '人机验证响应超时,强制提交...': { en: 'Human verification response timed out, forcing submission...', ja: '人間認証の応答がタイムアウトしました。強制送信中...', },
        '验证码处理异常,请刷新页面重试。': { en: 'CAPTCHA processing error, please refresh the page and try again.', ja: 'CAPTCHA処理でエラーが発生しました。ページをリロードして再試行してください。', },
        '所有验证已完成,准备提交...': { en: 'All verifications completed, preparing to submit...', ja: 'すべての認証が完了しました。送信準備中...', },
        '找不到提交按钮,请手动提交表单': { en: 'Submit button not found, please submit the form manually.', ja: '送信ボタンが見つかりません。手動でフォームを送信してください。', },
    }
    if (!navigator?.language) return text
    return translations[text]?.[navigator.language.slice(0, 2)] ?? text
}

(function () {
    'use strict';

    // 给脚本日志添加统一前缀,便于识别
    const LOG_PREFIX = "[VPS续期脚本]";

    let isRunning = false;

    GM_addStyle(`
        #vps-renewal-progress {
            position: fixed;
            top: 10px;
            right: 10px;
            z-index: 10000;
            background: #333;
            color: white;
            padding: 10px;
            border-radius: 5px;
            font-size: 12px;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
        }
    `);

    // 等待DOM加载完成
    function waitForDOMReady() {
        return new Promise(resolve => {
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', resolve);
            } else {
                resolve();
            }
        });
    }

    // 等待jQuery加载完成
    function waitForjQuery() {
        return new Promise(resolve => {
            if (typeof $ !== 'undefined') {
                resolve();
            } else {
                const checkjQuery = setInterval(() => {
                    if (typeof $ !== 'undefined') {
                        clearInterval(checkjQuery);
                        resolve();
                    }
                }, 50);
            }
        });
    }

    /**
     * 创建一个状态提示元素并显示消息
     */
    function createStatusElement(message) {
        removeStatusElement(); // 先移除已有的元素
        const statusEl = document.createElement('div');
        statusEl.id = 'vps-renewal-progress';
        statusEl.textContent = t(message);
        document.body.appendChild(statusEl);
    }

    /**
     * 更新或移除状态提示元素
     */
    function updateStatusElement(message) {
        const statusEl = document.getElementById('vps-renewal-progress');
        if (statusEl) {
            statusEl.textContent = t(message);
        } else {
            createStatusElement(message);
        }
    }

    function removeStatusElement() {
        const statusEl = document.getElementById('vps-renewal-progress');
        if (statusEl) {
            statusEl.remove();
        }
    }

    /**
     * 登录页面逻辑:自动填充并保存用户凭据
     */
    async function handleLogin() {
        console.log(`${LOG_PREFIX} 当前在登录页面。`);
        updateStatusElement("正在处理登录...");

        const memberid = GM_getValue('memberid');
        const user_password = GM_getValue('user_password');

        // 判断是否可以进行自动登录(存在保存的凭据并且没有错误)
        if (memberid && user_password && !document.querySelector('.errorMessage')) {
            console.log(`${LOG_PREFIX} 发现已保存的凭据,正在尝试自动登录...`);
            try {
                // 确保表单元素存在再进行赋值
                if (unsafeWindow.memberid && unsafeWindow.user_password) {
                    unsafeWindow.memberid.value = memberid;
                    unsafeWindow.user_password.value = user_password;
                    updateStatusElement("已检测到保存凭据,正在自动登录...");
                    // 延迟调用避免页面未完全渲染的问题
                    setTimeout(() => {
                        if (typeof unsafeWindow.loginFunc === 'function') {
                            unsafeWindow.loginFunc();
                        } else {
                            console.warn(`${LOG_PREFIX} 页面登录函数 loginFunc 不存在或不是函数。`);
                            updateStatusElement("警告:登录函数异常,请手动登录。");
                        }
                    }, 500);
                } else {
                    throw new Error('登录表单元素不存在');
                }
            } catch (e) {
                console.error(`${LOG_PREFIX} 自动登录失败: `, e);
                updateStatusElement("自动登录失败,请手动登录。");
            }
        } else {
            console.log(`${LOG_PREFIX} 未发现凭据或页面有错误信息,等待用户手动操作。`);
            // 监听用户提交登录表单以保存数据
            await waitForjQuery();
            if (typeof $ !== 'undefined') {
                $('#login_area').on('submit', function () {
                    try {
                        // 防止重复保存
                        if (unsafeWindow.memberid && unsafeWindow.user_password) {
                            GM_setValue('memberid', unsafeWindow.memberid.value);
                            GM_setValue('user_password', unsafeWindow.user_password.value);
                            console.log(`${LOG_PREFIX} 已保存新的用户凭据。`);
                        }
                    } catch (e) {
                        console.error(`${LOG_PREFIX} 保存凭据时出错:`, e);
                    }
                });
            }
        }
    }

    /**
     * VPS管理主页逻辑:检查到期时间和跳转
     */
    function handleVPSDashboard() {
        console.log(`${LOG_PREFIX} 当前在VPS管理主页。`);
        updateStatusElement("正在检查续期状态...");

        try {
            // 计算明天的日期,格式为 yyyy-mm-dd (瑞典时区格式更稳定)
            const tomorrow = new Date(Date.now() + 86400000).toLocaleDateString('sv', { timeZone: 'Asia/Tokyo' });
            const row = document.querySelector('tr:has(.freeServerIco)');

            if (!row) {
                console.log(`${LOG_PREFIX} 未找到免费VPS条目。`);
                updateStatusElement("未找到免费VPS。");
                return;
            }

            const expireSpan = row.querySelector('.contract__term');
            const expireDate = expireSpan ? expireSpan.textContent.trim() : null;

            console.log(`${LOG_PREFIX} 页面上的到期日: ${expireDate || '未找到'}`);
            console.log(`${LOG_PREFIX} 明天的日期: ${tomorrow}`);

            if (expireDate === tomorrow) {
                console.log(`${LOG_PREFIX} 条件满足:到期日为明天。正在跳转到续期页面...`);
                const detailLink = row.querySelector('a[href^="/xapanel/xvps/server/detail?id="]');
                if (detailLink && detailLink.href) {
                    updateStatusElement("检测到即将过期,正在续期...");
                    setTimeout(() => {
                        location.href = detailLink.href.replace('detail?id', 'freevps/extend/index?id_vps');
                    }, 1000);
                } else {
                    throw new Error('无法定位续期链接');
                }
            } else {
                console.log(`${LOG_PREFIX} 条件不满足:无需执行续期操作。`);
                updateStatusElement("当前VPS无需续期。");
                setTimeout(removeStatusElement, 3000);
            }
        } catch (e) {
            console.error(`${LOG_PREFIX} 在VPS管理主页处理出现错误:`, e);
            updateStatusElement("检查续期状态出错,请刷新页面重试。");
        }
    }

    /**
     * 续期申请页面逻辑:自动点击确认按钮
     */
    function handleRenewalPage() {
        console.log(`${LOG_PREFIX} 当前在续期申请页面。`);
        updateStatusElement("正在准备续期申请...");

        try {
            // 延迟一下确保页面内容稳定
            setTimeout(() => {
                const extendButton = document.querySelector('[formaction="/xapanel/xvps/server/freevps/extend/conf"]');
                if (extendButton) {
                    console.log(`${LOG_PREFIX} 找到续期按钮,正在点击...`);
                    updateStatusElement("正在确认续期协议...");
                    setTimeout(() => {
                        extendButton.click();
                    }, 800);
                } else {
                    throw new Error('未找到续期按钮');
                }
            }, 1000);
        } catch (e) {
            console.error(`${LOG_PREFIX} 续期确认按钮处理异常:`, e);
            updateStatusElement("续期申请页面交互失败。");
        }
    }

    /**
     * 验证码页面逻辑:识别并提交验证码
     */
    async function handleCaptchaPage() {
        console.log(`${LOG_PREFIX} 当前在验证码页面,开始处理验证码...`);
        updateStatusElement("正在识别并输入验证码...");

        try {
            // 等待DOM加载完成
            await waitForDOMReady();

            // 查找验证码图片(确保是base64编码)
            const img = document.querySelector('img[src^="data:image"]') || document.querySelector('img[src^="data:"]');
            if (!img || !img.src) {
                throw new Error('未找到验证码图片');
            }

            console.log(`${LOG_PREFIX} 已找到验证码图片,正在发送到API进行识别...`);
            updateStatusElement("正在识别验证码,请稍候...");

            // 调用外部API识别验证码
            let codeResponse;
            const maxRetries = 3;
            let retryCount = 0;

            while (retryCount < maxRetries) {
                try {
                    const response = await fetch('https://captcha-120546510085.asia-northeast1.run.app', {
                        method: 'POST',
                        body: img.src,
                        headers: {
                            'Content-Type': 'text/plain'
                        }
                    });

                    if (!response.ok) {
                        throw new Error(`API请求失败: ${response.status}`);
                    }

                    codeResponse = await response.text();
                    if (codeResponse && codeResponse.length >= 4) break;

                    throw new Error('API返回无效验证码');
                } catch (err) {
                    retryCount++;
                    if (retryCount >= maxRetries) {
                        throw err;
                    }
                    console.log(`${LOG_PREFIX} 验证码识别失败,正在进行第${retryCount}次重试...`);
                }
            }

            const code = codeResponse.trim();
            if (!code || code.length < 4) {
                throw new Error('未接收到有效验证码或验证码太短');
            }

            console.log(`${LOG_PREFIX} API返回验证码: ${code}`);
            updateStatusElement("验证码识别完成,准备提交表单...");

            // 将验证码填入输入框
            const input = document.querySelector('[placeholder*="上の画像"]');
            if (!input) {
                throw new Error('未找到验证码输入框');
            }

            input.value = code;
            input.dispatchEvent(new Event('input', { bubbles: true }));
            console.log(`${LOG_PREFIX} 已将验证码填入输入框。`);
            updateStatusElement("已完成验证码填写,正在处理人机验证...");

            // 处理 Cloudflare Turnstile 人机验证
            const cfContainer = document.querySelector('.cf-turnstile');
            if (!cfContainer) {
                console.warn(`${LOG_PREFIX} 未检测到Cloudflare组件,可能页面结构变化。`);
                submitForm();
                return;
            }

            const cf = cfContainer.querySelector('[name=cf-turnstile-response]');
            if (cf && cf.value) {
                console.log(`${LOG_PREFIX} Cloudflare 令牌已存在,直接提交表单。`);
                submitForm();
                return;
            }

            console.log(`${LOG_PREFIX} Cloudflare 令牌不存在,设置监听器等待生成...`);
            updateStatusElement("等待人机验证令牌生成...");

            // 设置超时机制防止无限等待
            const timeoutId = setTimeout(() => {
                console.error(`${LOG_PREFIX} Cloudflare Turnstile令牌生成超时,强制提交表单。`);
                updateStatusElement("人机验证响应超时,强制提交...");
                submitForm();
            }, 15000);

            // 监听cf-turnstile-response字段的value属性变化
            const observer = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (
                        mutation.type === 'attributes' &&
                        mutation.attributeName === 'value' &&
                        cf.value
                    ) {
                        console.log(`${LOG_PREFIX} Cloudflare 令牌已生成,正在提交表单...`);
                        clearTimeout(timeoutId);
                        observer.disconnect();
                        submitForm();
                        return;
                    }
                }
            });

            observer.observe(cf, { attributes: true, attributeFilter: ['value'] });

        } catch (error) {
            console.error(`${LOG_PREFIX} 处理验证码时发生错误:`, error);
            updateStatusElement("验证码处理异常,请刷新页面重试。");
        }

        // 提交表单逻辑
        function submitForm() {
            updateStatusElement("所有验证已完成,准备提交...");
            setTimeout(() => {
                if (typeof unsafeWindow.submit_button !== 'undefined' &&
                    unsafeWindow.submit_button &&
                    typeof unsafeWindow.submit_button.click === 'function') {
                    unsafeWindow.submit_button.click();
                } else {
                    const submitBtn = document.querySelector('input[type="submit"], button[type="submit"]');
                    if (submitBtn) {
                        submitBtn.click();
                    } else {
                        console.error(`${LOG_PREFIX} 未找到可点击的提交按钮`);
                        updateStatusElement("找不到提交按钮,请手动提交表单");
                    }
                }
            }, 1000);
        }
    }

    /**
     * 主流程分发
     */
    function main() {
        if (isRunning) return; // 防止多重运行
        isRunning = true;

        const path = window.location.pathname;

        if (path.startsWith('/xapanel/login/xvps')) {
            handleLogin();
        } else if (path.includes('/xapanel/xvps/index')) {
            handleVPSDashboard();
        } else if (path.includes('/xapanel/xvps/server/freevps/extend/index')) {
            handleRenewalPage();
        } else if (
            path.includes('/xapanel/xvps/server/freevps/extend/conf') ||
            path.includes('/xapanel/xvps/server/freevps/extend/do')
        ) {
            handleCaptchaPage();
        } else {
            console.log(`${LOG_PREFIX} 当前不在已支持的路径中,脚本不会执行任何操作。`);
            isRunning = false;
        }
    }

    // 入口调用
    main();

})();