-
Notifications
You must be signed in to change notification settings - Fork 202
Expand file tree
/
Copy pathrenew.user.js
More file actions
483 lines (433 loc) · 21.9 KB
/
Copy pathrenew.user.js
File metadata and controls
483 lines (433 loc) · 21.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
// ==UserScript==
// @name Extend VPS Expiration
// @name:zh-CN Xserver VPS 自动续期脚本
// @namespace http://tampermonkey.net/
// @version 2026-04-09
// @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
// @require https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.22.0/dist/tf.min.js
// @updateURL https://raw.githubusercontent.com/GitHub30/extend-vps-exp/refs/heads/main/renew.user.js
// @downloadURL https://raw.githubusercontent.com/GitHub30/extend-vps-exp/refs/heads/main/renew.user.js
// @supportURL https://github.com/GitHub30/extend-vps-exp
// ==/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.)
* =================================================================================================
*/
/* global tf */
// 翻译
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 decodeCaptcha(predictionTensor) {
const blankIndex = predictionTensor.shape[2] - 1;
const bestPath = predictionTensor.argMax(-1).dataSync();
const digits = [];
let previous = blankIndex;
for (const index of bestPath) {
if (index !== blankIndex && index !== previous) {
digits.push(String(index));
}
previous = index;
}
return digits.join('');
}
async function imageToCode(img) {
await tf.ready()
const model = await tf.loadLayersModel('https://github30.github.io/captcha-cloudrun/web_model/model.json?v=20260407-2')
return tf.tidy(() => {
const tensor = tf.browser
.fromPixels(img)
.resizeBilinear([60, 300])
.toFloat()
.div(255)
.expandDims(0);
return decodeCaptcha(model.predict(tensor))
});
}
(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 不存在或不是函数。`);
unsafeWindow.login_area.submit();
}
}, 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 {
codeResponse = await imageToCode(img);
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();
})();