├── .github └── FUNDING.yml ├── README.md ├── _worker.js ├── vps_plus.js └── vps_plus旧版.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | # .github/FUNDING.yml 3 | 4 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 5 | patreon: # Replace with a single Patreon username 6 | open_collective: # Replace with a single Open Collective username 7 | ko_fi: # Replace with a single Ko-fi username 8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | liberapay: # Replace with a single Liberapay username 11 | issuehunt: # Replace with a single IssueHunt username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 15 | thanks_dev: # Replace with a single thanks.dev username 16 | custom: https://blog.24811213.xyz/pages/donate/ 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VPS 到期提醒 2 | 基于Cloudflare Workers构建的VPS到期提醒可视化面板,让用户能够一目了然地查看VPS的状态、IP、ASN、国家、城市、注册商、注册日期、过期日期、`年费价格`、`剩余价值`,并可在到期前通过TG机器人向用户推送通知。 3 | 4 | **DEMO**: 5 | 6 | - **_worker.js**:不显示年费和剩余价值的版本 7 | - **vps_plus.js**:显示年费和剩余价值的版本 8 | 9 | ## 部署方法 10 | 11 | **worker部署** 12 | 13 | - 在cf中创建一个workers,复制`js`代码到workers中,点击保存并部署 14 | - 绑定一个KV,KV变量名`VPS_TG_KV`,KV空间名随意 15 | - 设置以下环境变量: 16 | 17 | ## 变量设置 18 | | 变量名 | 填写示例 | 说明 | 是否必填 | 19 | | ------ | ------- | ------ | ------ | 20 | | PASS | 123456 | 前端访问密码,默认为`123456` | 是 | 21 | | RATE_API | f5bc********7d1e66**** | 到此处[注册](https://www.exchangerate-api.com/)免费的汇率API KEY | 是 | 22 | | TGID | 652***4200 | TG机器人ID,不需要通知可不填 | 否 | 23 | | TGTOKEN | 60947***43:BBCrcWzLb000000vdtt0jy000000-uKM7p8 | TG机器人TOKEN,不需要通知可不填 | 否 | 24 | 25 | ## 域名信息json文件格式 26 | **示例**,其中的`price`为`年费价格`,需要保留2位小数,支持美元(USD)和人民币(CNY);`store`指商家名称,`storeURL`指商家链接 27 | ``` 28 | [ 29 | { 30 | "ip": "209.138.178.63", 31 | "startday": "2025-01-11", 32 | "endday": "2026-01-10", 33 | "price": "11.99USD", 34 | "store": "DartNode", 35 | "storeURL": "https://app.dartnode.com/" 36 | }, 37 | { 38 | "ip": "141.150.63.49", 39 | "startday": "2024-12-31", 40 | "endday": "2026-02-14", 41 | "price": "88.88CNY", 42 | "store": "ChunkServe", 43 | "storeURL": "https://billing.chunkserve.com/" 44 | }, 45 | { 46 | "ip": "31.88.142.101", 47 | "startday": "2025-01-19", 48 | "endday": "2027-01-19", 49 | "price": "11.99USD", 50 | "store": "Dasabo", 51 | "storeURL": "https://my.dasabo.com/" 52 | } 53 | ] 54 | ``` 55 | 请修改其中信息为你自己的vps,并将这个内容存为json文件,放到公开仓库或私有gist生成一个直链 56 | **直链格式类似以下** 57 | ``` 58 | https://gist.githubusercontent.com/用户名/591b80ed80baabcdef369a330bb8e88e/raw/vpsinfo.json 59 | ``` 60 | 61 | ## TG 通知的文字样式 62 | ``` 63 | 🚨 [VPS到期提醒] 🚨 64 | ==================== 65 | 🌍 国家: US | 城市: Chicago 66 | 💻 IP 地址: 8.8.8.8 67 | ⏳ 剩余时间: 3 天 68 | 📅 到期日期: 2025-01-28 69 | ⚠️ 点击续期:[Dasabo](https://my.dasabo.com) 70 | ``` 71 | 续期那里可直接点击商家名称跳转到商家主页 72 | 73 | ## 使用方法 74 | - 访问你的worker项目域名,会提示输入密码,输入你在环境变量中设置的密码 75 | - 首次登录会直接跳转到设置页,在设置页中填入`存储VPS信息的URL直链` 76 | - 点击保存即跳转到vps到期监控信息页面 77 | 78 | ![image](https://github.com/user-attachments/assets/d7489572-1cf7-42ba-aa56-e44123cf15a9) 79 | 80 | ![image](https://github.com/user-attachments/assets/6fbef2e9-6071-4605-b961-ca785f18d0f9) 81 | 82 | ![image](https://github.com/user-attachments/assets/38041a99-6f0f-4ee6-9a59-f663389c5b59) 83 | 84 | 85 | -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | // 验证密码 2 | function verifyPassword(password, env) { 3 | const validPassword = env.PASS || "123456"; 4 | return password === validPassword; 5 | } 6 | 7 | // 从KV获取配置 8 | async function getConfig(kv) { 9 | const config = { 10 | sitename: await kv.get('sitename') || "VPS到期监控", 11 | vpsurl: await kv.get('vpsurl') || "", 12 | days: await kv.get('days') || "5" 13 | }; 14 | return config; 15 | } 16 | 17 | // 保存配置到KV 18 | async function saveConfig(kv, config) { 19 | await Promise.all([ 20 | kv.put('sitename', config.sitename), 21 | kv.put('vpsurl', config.vpsurl), 22 | kv.put('days', config.days) 23 | ]); 24 | } 25 | 26 | function escapeMD2(text) { 27 | return text.replace(/([_*[\]()~`>#+\-=|{}.!\\])/g, '\\$1'); 28 | } 29 | 30 | // tg消息发送函数 31 | async function sendtgMessage(message, env) { 32 | const tgid = env.TGID; 33 | const tgtoken = env.TGTOKEN; 34 | if (!tgid || !tgtoken) { 35 | console.log('缺少变量 TGID 或 TGTOKEN,跳过消息发送'); 36 | return; 37 | } 38 | 39 | const safemessage = escapeMD2(message); 40 | const url = `https://api.telegram.org/bot${tgtoken}/sendMessage`; 41 | const params = { 42 | chat_id: tgid, 43 | text: safemessage, 44 | parse_mode: 'MarkdownV2', 45 | // parse_mode: 'HTML', // 使用 HTML 则不需要转义 Markdown 特殊字符 46 | }; 47 | try { 48 | await fetch(url, { 49 | method: 'POST', 50 | headers: { 'Content-Type': 'application/json' }, 51 | body: JSON.stringify(params), 52 | }); 53 | } catch (error) { 54 | console.error('Telegram 消息推送失败:', error); 55 | } 56 | } 57 | 58 | // 获取IP的国家、城市、ASN信息 59 | async function ipinfo_query(vpsjson) { 60 | const ipjson = await Promise.all(vpsjson.map(async ({ ip }) => { 61 | const apiUrl = `https://ip.eooce.com/${ip}`; 62 | try { 63 | const ipResponse = await fetch(apiUrl); 64 | if (ipResponse.ok) { 65 | const { country_code, city, asn } = await ipResponse.json(); 66 | return { ip, country_code, city, asn }; 67 | } else { 68 | console.error(`IP查询失败: ${ip}`); 69 | return null; 70 | } 71 | } catch (error) { 72 | console.error(`请求IP信息失败: ${ip}`, error); 73 | return null; 74 | } 75 | })); 76 | return ipjson.filter(info => info !== null) || []; 77 | } 78 | 79 | export default { 80 | async fetch(request, env) { 81 | const url = new URL(request.url); 82 | const path = url.pathname; 83 | const cookies = request.headers.get('Cookie') || ''; 84 | const isAuthenticated = cookies.includes(`password=${env.PASS || "123456"}`); 85 | const config = await getConfig(env.VPS_TG_KV); 86 | 87 | // 登录路由 88 | if (path === '/login') { 89 | if (request.method === 'POST') { 90 | const formData = await request.formData(); 91 | const password = formData.get('password'); 92 | 93 | if (verifyPassword(password, env)) { 94 | return new Response(null, { 95 | status: 302, 96 | headers: { 97 | 'Location': '/', 98 | 'Set-Cookie': `password=${password}; path=/; HttpOnly` 99 | } 100 | }); 101 | } else { 102 | return new Response(generateLoginHTML(true), { 103 | headers: { 'Content-Type': 'text/html' } 104 | }); 105 | } 106 | } 107 | return new Response(generateLoginHTML(), { 108 | headers: { 'Content-Type': 'text/html' } 109 | }); 110 | } 111 | 112 | // 验证是否已登录 113 | if (!isAuthenticated) { 114 | return Response.redirect(`${url.origin}/login`, 302); 115 | } 116 | 117 | // 设置路由 118 | if (path === '/settings') { 119 | if (request.method === 'POST') { 120 | const formData = await request.formData(); 121 | const newConfig = { 122 | sitename: formData.get('sitename'), 123 | vpsurl: formData.get('vpsurl'), 124 | days: formData.get('days') 125 | }; 126 | 127 | if (!newConfig.vpsurl) { 128 | return new Response(generateSettingsHTML(newConfig, true), { 129 | headers: { 'Content-Type': 'text/html' } 130 | }); 131 | } 132 | 133 | await saveConfig(env.VPS_TG_KV, newConfig); 134 | return Response.redirect(url.origin, 302); 135 | } 136 | 137 | return new Response(generateSettingsHTML(config), { 138 | headers: { 'Content-Type': 'text/html' } 139 | }); 140 | } 141 | 142 | // 主页路由 143 | if (!config.vpsurl) { 144 | return Response.redirect(`${url.origin}/settings`, 302); 145 | } 146 | 147 | try { 148 | const response = await fetch(config.vpsurl); 149 | if (!response.ok) { 150 | throw new Error('网络响应失败'); 151 | } 152 | const vpsjson = await response.json(); 153 | if (!Array.isArray(vpsjson)) { 154 | throw new Error('JSON 数据格式不正确'); 155 | } 156 | // 合并 vpsjson 和 ipdata  157 | const ipjson = await ipinfo_query(vpsjson); 158 | const vpsdata = vpsjson.map(vps => { 159 | const ipdata = ipjson.find(ip => ip.ip === vps.ip); // 查找匹配的 IP 信息 160 | if (ipdata) { 161 | return { ...vps, ...ipdata }; 162 | } 163 | return vps; // 如果没有找到 IP 信息,返回原始数据 164 | }); 165 | 166 | // 检查即将到期的VPS并发送 Telegram 消息 167 | for (const info of vpsdata) { 168 | const endday = new Date(info.endday); 169 | const today = new Date(); 170 | const daysRemaining = Math.ceil((endday - today) / (1000 * 60 * 60 * 24)); 171 | 172 | if (daysRemaining > 0 && daysRemaining <= Number(config.days)) { 173 | const message = `🚨 [VPS到期提醒] 🚨 174 | ==================== 175 | 🌍 国家: ${info.country_code} | 城市: ${info.city} 176 | 💻 IP 地址: ${info.ip} 177 | ⏳ 剩余时间: ${daysRemaining} 天 178 | 📅 到期日期: ${info.endday} 179 | ⚠️ 点击续期:[${info.store}](${info.storeURL})`; 180 | 181 | const lastSent = await env.VPS_TG_KV.get(info.ip); // 检查是否已发送过通知 182 | if (!lastSent || (new Date(lastSent).toISOString().split('T')[0] !== today.toISOString().split('T')[0])) { 183 | await sendtgMessage(message, env); 184 | await env.VPS_TG_KV.put(info.ip, new Date().toISOString()); // 更新 KV 存储的发送时间   185 | } 186 | } 187 | } 188 | 189 | // 处理 generateHTML 的返回值 190 | const htmlContent = await generateHTML(vpsdata, config.sitename); 191 | return new Response(htmlContent, { 192 | headers: { 'Content-Type': 'text/html' }, 193 | }); 194 | 195 | } catch (error) { 196 | console.error("Fetch error:", error); 197 | return new Response("无法获取或解析VPS的json文件", { status: 500 }); 198 | } 199 | } 200 | }; 201 | 202 | // 生成登录页面HTML 203 | function generateLoginHTML(isError = false) { 204 | return ` 205 | 206 | 207 | 208 | 209 | 210 | 登录 - VPS到期监控 211 | 212 | 281 | 282 | 283 |
284 |

VPS到期监控

285 |
密码错误,请重试
286 |
287 |
288 | 289 | 290 |
291 | 292 |
293 |
294 | 295 | 296 | `; 297 | } 298 | 299 | // 生成设置页面HTML 300 | function generateSettingsHTML(config, showError = false) { 301 | return ` 302 | 303 | 304 | 305 | 306 | 307 | 设置 - VPS到期监控 308 | 309 | 407 | 408 | 409 |
410 |

系统设置

411 |
存储VPS信息的URL直链为必填项
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 | // 生成主页HTML 439 | async function generateHTML(vpsdata, sitename) { 440 | const rows = await Promise.all(vpsdata.map(async info => { 441 | const startday = new Date(info.startday); 442 | const endday = new Date(info.endday); 443 | const today = new Date(); 444 | const totalDays = (endday - startday) / (1000 * 60 * 60 * 24); 445 | const daysElapsed = (today - startday) / (1000 * 60 * 60 * 24); 446 | const progressPercentage = Math.min(100, Math.max(0, (daysElapsed / totalDays) * 100)); 447 | const daysRemaining = Math.ceil((endday - today) / (1000 * 60 * 60 * 24)); 448 | const isExpired = today > endday; 449 | const statusColor = isExpired ? '#e74c3c' : '#2ecc71'; 450 | const statusText = isExpired ? '已过期' : '正常'; 451 | 452 | return ` 453 | 454 | 455 | ${info.ip}  456 | ${info.asn} 457 | ${info.country_code} 458 | ${info.city} 459 | ${info.store} 460 | ${info.startday} 461 | ${info.endday} 462 | ${isExpired ? '已过期' : daysRemaining + ' 天'} 463 | 464 |
465 |
466 |
467 | 468 | 469 | `; 470 | })); 471 | return generateFormHTML(sitename, rows); 472 | } 473 | 474 | function generateFormHTML(sitename, rows) { 475 | return ` 476 | 477 | 478 | 479 | 480 | 481 | ${sitename} 482 | 483 | 608 | 617 | 618 | 619 |
620 |
621 |

${sitename}

622 | 设置 623 |
624 |
625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | ${rows.join('')} 642 | 643 |
状态IPASN国家城市商家注册日到期日剩余天数使用进度
644 |
645 |
646 | 653 | 654 | 655 | `; 656 | } 657 | -------------------------------------------------------------------------------- /vps_plus.js: -------------------------------------------------------------------------------- 1 | // 从KV获取配置 2 | async function getConfig(env) { 3 | const kv = env.VPS_TG_KV; 4 | if (!kv) throw new Error("KV变量: VPS_TG_KV不存在"); 5 | try { 6 | const [sitename, vpsurl, days] = await Promise.all([ 7 | kv.get("sitename"), kv.get("vpsurl"), kv.get("days"), 8 | ]); 9 | return { 10 | sitename: sitename || "VPS到期监控", 11 | vpsurl: vpsurl || "", 12 | days: Number(days) || 5, 13 | }; 14 | } catch (error) { 15 | console.error("获取KV数据失败:", error); 16 | throw error; 17 | } 18 | } 19 | 20 | // 保存配置到KV 21 | async function saveConfig(env, newConfig) { 22 | const kv = env.VPS_TG_KV; 23 | try { 24 | await Promise.all([ 25 | kv.put("sitename", newConfig.sitename.trim()), 26 | kv.put("vpsurl", newConfig.vpsurl.trim()), 27 | kv.put("days", newConfig.days.trim()), 28 | ]); 29 | } catch (error) { 30 | console.error("保存KV数据失败:", error); 31 | throw error; 32 | } 33 | } 34 | 35 | // 获取 vps json 数据并解析 36 | async function getVpsData(env) { 37 | const { vpsurl } = await getConfig(env); 38 | if (!vpsurl) throw new Error("请在设置界面输入存储VPS信息的URL直链并保存"); 39 | 40 | try { 41 | const requestOptions = { 42 | headers: { 43 | "Cache-Control": "no-cache, no-store, must-revalidate", 44 | Pragma: "no-cache", 45 | Expires: "0", 46 | }, 47 | }; 48 | const response = await fetch(vpsurl, requestOptions); 49 | if (!response.ok) { 50 | throw new Error(`获取VPS数据失败, HTTP状态码: ${response.status}`); 51 | } 52 | const vpsjson = await response.json(); 53 | if (!Array.isArray(vpsjson) || vpsjson.length === 0) { 54 | throw new Error("VPS数据格式格式不是json"); 55 | } 56 | return vpsjson; 57 | } catch (error) { 58 | console.error("获取 VPS 数据失败:", error); 59 | throw error; 60 | } 61 | } 62 | 63 | // 通用IP查询请求函数 64 | async function fetchIPInfo(ip, { urlBuilder, dataParser }, timeout = 3000) { 65 | const controller = new AbortController(); 66 | const timeoutId = setTimeout(() => controller.abort(), timeout); 67 | try { 68 | const response = await fetch(urlBuilder(ip), { 69 | signal: controller.signal 70 | }); 71 | if (!response.ok) throw new Error(`HTTP ${response.status}`); 72 | const data = await response.json(); 73 | return dataParser(data); 74 | } catch (error) { 75 | if (error.name !== 'AbortError') { 76 | console.error(`[${urlBuilder(ip)}] 请求失败:`, error.message); 77 | } 78 | return null; 79 | } finally { 80 | clearTimeout(timeoutId); 81 | } 82 | } 83 | 84 | // 获取IP地址的国家、城市、ASN信息 85 | async function ipinfo_query(vpsjson) { 86 | const IP_API = [ 87 | { 88 | name: 'ip.eooce', 89 | urlBuilder: (ip) => `https://ip.eooce.com/${ip}`, 90 | dataParser: (data) => ({ 91 | country_code: data.country_code || 'Unknown', 92 | city: data.city || 'Unknown', 93 | asn: data.asn || 'Unknown' 94 | }) 95 | }, 96 | { 97 | name: 'ipinfo.io', 98 | urlBuilder: (ip) => `https://ipinfo.io/${ip}/json`, 99 | dataParser: (data) => ({ 100 | country_code: data.country || 'Unknown', 101 | city: data.city || 'Unknown', 102 | asn: (data.org?.split(' ')[0] || '').startsWith('AS') 103 | ? data.org.split(' ')[0] 104 | : 'Unknown' 105 | }) 106 | } 107 | ]; 108 | const ipjson = await Promise.allSettled( 109 | vpsjson.map(async ({ ip }) => { 110 | try { 111 | const requests = IP_API.map(provider => 112 | fetchIPInfo(ip, provider) 113 | .then(data => data ? { provider: provider.name, data } : null) 114 | .catch(() => null) 115 | ); 116 | const result = await Promise.any(requests).catch(() => null); 117 | 118 | if (result?.data) { 119 | return { 120 | ip, 121 | country_code: result.data.country_code || 'Unknown', 122 | city: result.data.city || 'Unknown', 123 | asn: result.data.asn || 'Unknown' 124 | }; 125 | } 126 | return null; 127 | } catch (error) { 128 | return null; 129 | } 130 | }) 131 | ); 132 | return ipjson 133 | .filter(result => result.status === 'fulfilled' && result.value !== null) 134 | .map(result => result.value); 135 | } 136 | 137 | // 将IP信息与vps信息合并为一个新的数组 138 | function getMergeData(vpsjson, ipjson) { 139 | const ipMap = new Map(ipjson.map((ipdata) => [ipdata.ip, ipdata])); 140 | return vpsjson.map((vps) => { 141 | const mergeData = vps.ip ? ipMap.get(vps.ip) : null; 142 | return mergeData 143 | ? { 144 | ...vps, 145 | country_code: mergeData.country_code || "Unknown", 146 | city: mergeData.city || "Unknown", 147 | asn: mergeData.asn || "Unknown", 148 | } 149 | : vps; // 如果没有找到IP信息,返回原始数据 150 | }); 151 | } 152 | 153 | // 获取实时汇率数据 154 | async function getRates(env) { 155 | const apis = [ 156 | { 157 | url: "https://v2.xxapi.cn/api/exchange?from=USD&to=CNY&amount=1", 158 | parser: data => data.code === 200 && data.data?.rate 159 | ? { rawCNY: data.data.rate, timestamp: data.data.update_at } 160 | : null 161 | }, 162 | { 163 | url: "https://v2.xxapi.cn/api/allrates", 164 | parser: data => data.code === 200 && data.data?.rates?.CNY?.rate 165 | ? { rawCNY: data.data.rates.CNY.rate, timestamp: data.data.update_at } 166 | : null 167 | }, 168 | { 169 | url: `https://v6.exchangerate-api.com/v6/${env.RATE_API}/latest/USD`, 170 | parser: data => data.result === "success" && data.conversion_rates?.CNY 171 | ? { rawCNY: data.conversion_rates.CNY, timestamp: data.time_last_update_unix * 1000 } 172 | : null 173 | } 174 | ]; 175 | 176 | let rawCNY = null, timestamp = null; 177 | 178 | // 遍历 API 配置,获取数据 179 | for (const api of apis) { 180 | const parsed = await fetchData(api); 181 | if (parsed) { 182 | rawCNY ||= parsed.rawCNY; // 获取汇率 183 | timestamp ||= parsed.timestamp; // 获取时间戳 184 | if (rawCNY !== null && timestamp) break; // 如果都获取到了有效数据,跳出循环 185 | } 186 | } 187 | 188 | // 判断是否获得了有效的汇率数字 189 | if (typeof rawCNY === "number" && !isNaN(rawCNY)) { 190 | return { 191 | rateCNYnum: Number(rawCNY), 192 | rateTimestamp: new Date(timestamp).toISOString() 193 | }; 194 | } else { 195 | console.error("获取汇率数据失败,使用默认值"); 196 | return { 197 | rateCNYnum: 7.29, 198 | rateTimestamp: new Date().toISOString() 199 | }; 200 | } 201 | } 202 | 203 | // API 请求逻辑,包括超时控制、错误处理和解析数据 204 | async function fetchData(api) { 205 | const controller = new AbortController(); 206 | const timeoutId = setTimeout(() => controller.abort(), 500); 207 | try { 208 | const response = await fetch(api.url, { signal: controller.signal }); 209 | if (!response.ok) { 210 | console.error(`API 请求失败 ${api.url},状态码:${response.status}`); 211 | return null; 212 | } 213 | const data = await response.json(); 214 | return api.parser(data); // 返回解析后的数据 215 | } catch (err) { 216 | if (err.name !== "AbortError") { 217 | console.error(`API 请求错误 ${api.url}:`, err); 218 | } 219 | return null; 220 | } finally { 221 | clearTimeout(timeoutId); 222 | } 223 | } 224 | 225 | // 构建TG消息模板并在到期前发送提醒 226 | async function tgTemplate(mergeData, config, env) { 227 | const today = new Date().toISOString().split("T")[0]; 228 | await Promise.allSettled( 229 | mergeData.map(async (info) => { 230 | const endday = new Date(info.endday); 231 | const daysRemaining = Math.ceil((endday - new Date(today)) / (1000 * 60 * 60 * 24)); 232 | if (daysRemaining > 0 && daysRemaining <= Number(config.days)) { 233 | const message = `🚨 [VPS到期提醒] 🚨 234 | ==================== 235 | 🌍 VPS位置: ${info.country_code} | ${info.city} 236 | 💻 IP 地址: ${info.ip} 237 | ⏳ 剩余时间: ${daysRemaining} 天 238 | 📅 到期日期: ${info.endday} 239 | ⚠️ 点击续期:[${info.store}](${info.storeURL})`; 240 | 241 | const lastSent = await env.VPS_TG_KV.get(info.ip); // 检查是否已发送过通知 242 | if (!lastSent || lastSent.split("T")[0] !== today) { 243 | const isSent = await sendtgMessage(message, env); 244 | if (isSent) await env.VPS_TG_KV.put(info.ip, new Date().toISOString()); 245 | } 246 | } 247 | }) 248 | ); 249 | } 250 | 251 | // tg消息发送函数 252 | async function sendtgMessage(message, env) { 253 | if (!env.TGID || !env.TGTOKEN) { 254 | console.log("缺少变量 TGID 或 TGTOKEN, 跳过消息发送"); 255 | return; 256 | } 257 | 258 | const safemessage = message.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1"); 259 | const tgApiurl = `https://api.telegram.org/bot${env.TGTOKEN}/sendMessage`; 260 | const params = { 261 | chat_id: env.TGID, 262 | text: safemessage, 263 | parse_mode: "MarkdownV2", 264 | }; 265 | 266 | try { 267 | const response = await fetch(tgApiurl, { 268 | method: "POST", 269 | headers: { "Content-Type": "application/json" }, 270 | body: JSON.stringify(params), 271 | }); 272 | if (!response.ok) { 273 | console.error("Telegram 消息推送失败,状态码:", response.status); 274 | return false; 275 | } 276 | return true; 277 | } catch (error) { 278 | console.error("Telegram 消息推送失败:", error); 279 | return false; 280 | } 281 | } 282 | 283 | // 处理登录路由 284 | async function handleLogin(request, validPassword) { 285 | if (request.method === "POST") { 286 | const formData = await request.formData(); 287 | const password = formData.get("password"); 288 | 289 | if (password === validPassword) { 290 | return new Response(null, { 291 | status: 302, 292 | headers: { 293 | Location: "/", 294 | "Set-Cookie": `password=${password}; path=/; HttpOnly; Secure`, 295 | }, 296 | }); 297 | } else { 298 | return new Response(generateLoginHTML(true), { 299 | headers: { "Content-Type": "text/html" }, 300 | }); 301 | } 302 | } 303 | return new Response(generateLoginHTML(), { 304 | headers: { "Content-Type": "text/html" }, 305 | }); 306 | } 307 | 308 | // 处理设置路由 309 | async function handleSettings(request, config, env) { 310 | if (request.method === "POST") { 311 | const formData = await request.formData(); 312 | const newConfig = { 313 | sitename: formData.get("sitename"), 314 | vpsurl: formData.get("vpsurl"), 315 | days: formData.get("days"), 316 | }; 317 | 318 | if (!newConfig.vpsurl) { 319 | return new Response(generateSettingsHTML(newConfig, true), { 320 | headers: { "Content-Type": "text/html" }, 321 | }); 322 | } 323 | 324 | await saveConfig(env, newConfig); 325 | return Response.redirect(new URL("/", request.url).toString(), 302); 326 | } 327 | 328 | return new Response(generateSettingsHTML(config), { 329 | headers: { "Content-Type": "text/html" }, 330 | }); 331 | } 332 | 333 | // 处理根路由 334 | async function handleRoot(env, config) { 335 | try { 336 | const vpsjson = await getVpsData(env); 337 | if (!vpsjson) throw new Error("VPS json数据为空或无法加载数据"); 338 | const ipjson = await ipinfo_query(vpsjson); 339 | if (!ipjson) throw new Error("IP 信息查询失败"); 340 | const mergeData = getMergeData(vpsjson, ipjson); 341 | const ratejson = await getRates(env); 342 | 343 | await tgTemplate(mergeData, config, env); 344 | const htmlContent = await generateHTML(mergeData, ratejson, config.sitename); 345 | return new Response(htmlContent, { 346 | headers: { "Content-Type": "text/html" }, 347 | }); 348 | } catch (error) { 349 | console.error("Fetch error:", error); 350 | let errorMessage = "无法获取或解析VPS的json文件"; 351 | if (error.message.includes("VPS json数据为空或无法加载数据")) { 352 | errorMessage = "请检查 vpsurl 直链的格式及 json 内容格式"; 353 | } else if (error.message.includes("IP 信息查询失败")) { 354 | errorMessage = "IP 信息查询失败,可能是外部服务不可用"; 355 | } else { 356 | errorMessage = "未知错误,请稍后重试"; 357 | } 358 | return new Response(errorMessage, { status: 500 }); 359 | } 360 | } 361 | 362 | // 导出 fetch 函数 363 | export default { 364 | async fetch(request, env) { 365 | const url = new URL(request.url); 366 | const path = url.pathname; 367 | const validPassword = env.PASS || "123456"; 368 | const cookies = request.headers.get("Cookie") || ""; 369 | const isAuth = cookies.includes(`password=${validPassword}`); 370 | const config = await getConfig(env); 371 | 372 | if (!isAuth && path !== "/login") { 373 | return Response.redirect(`${url.origin}/login`, 302); 374 | } 375 | 376 | if (!config.vpsurl && path !== "/settings" && path !=="/login") { 377 | return Response.redirect(`${url.origin}/settings`, 302); 378 | } 379 | 380 | switch (path) { 381 | case "/login": 382 | return await handleLogin(request, validPassword); // 登录路由  383 | case "/settings": 384 | return await handleSettings(request, config, env); // 设置路由  385 | default: 386 | return await handleRoot(env, config); // 根路由 387 | } 388 | }, 389 | 390 | async scheduled(event, env, ctx) { 391 | try { 392 | const config = await getConfig(env); 393 | const vpsjson = await getVpsData(env); 394 | const ipjson = await ipinfo_query(vpsjson); 395 | const mergeData = getMergeData(vpsjson, ipjson); 396 | await tgTemplate(mergeData, config, env); 397 | console.log("Corn 执行时间:", new Date().toISOString()); 398 | } catch (error) { 399 | console.error("Cron 执行失败:", error); 400 | } 401 | }, 402 | }; 403 | 404 | // 生成主页HTML 405 | async function generateHTML(mergeData, ratejson, sitename) { 406 | const rows = await Promise.all( 407 | mergeData.map(async (info) => { 408 | const today = new Date(); 409 | const endday = new Date(info.endday); 410 | const daysRemaining = Math.ceil((endday - today) / (1000 * 60 * 60 * 24)); 411 | const isExpired = today > endday; 412 | const statusColor = isExpired ? "#e74c3c" : "#2ecc71"; 413 | const statusText = isExpired ? "已过期" : "正常"; 414 | 415 | // 计算年费价格和剩余价值 416 | const [, price, unit] = info.price.match(/^([\d.]+)([A-Za-z]+)$/) || []; 417 | const priceNum = parseFloat(price); 418 | const rateCNYnum = ratejson?.rateCNYnum || 7.29; 419 | const [ValueUSD, ValueCNY] = unit === "USD" 420 | ? [(priceNum / 365) * daysRemaining, (priceNum / 365) * daysRemaining * rateCNYnum] 421 | : [(priceNum / 365) * daysRemaining / rateCNYnum, (priceNum / 365) * daysRemaining]; 422 | const formatValueUSD = `${ValueUSD.toFixed(2)}USD`; 423 | const formatValueCNY = `${ValueCNY.toFixed(2)}CNY`; 424 | 425 | return ` 426 | 427 | 428 | ${info.ip} 429 | ${info.asn} 430 | ${info.country_code} 431 | ${info.city} 432 | ${info.store} 433 | ${info.startday} 434 | ${info.endday} 435 | ${isExpired ? "已过期" : daysRemaining + "天"} 436 | ${info.price} 437 | ${formatValueUSD} | ${formatValueCNY} 438 | 439 | `; 440 | }) 441 | ); 442 | return generateFormHTML(sitename, rows, ratejson); 443 | } 444 | 445 | function generateFormHTML(sitename, rows, ratejson) { 446 | const { rateCNYnum, rateTimestamp } = ratejson; 447 | const BeijingTime = new Date(rateTimestamp).toLocaleString("zh-CN", { 448 | timeZone: "Asia/Shanghai", 449 | hour12: false, // 使用24小时制 450 | }); 451 | 452 | return ` 453 | 454 | 455 | 456 | 457 | 458 | ${sitename} 459 | 460 | 582 | 591 | 592 | 593 |
594 |
595 |

${sitename}

596 | 设置 597 |
598 |
599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | ${rows.join("")} 617 | 618 |
状态IP地址ASN国家城市商家注册日到期日剩余天数年费价格剩余价值
619 |
620 |
621 | 629 | 630 | 631 | `; 632 | } 633 | 634 | // 生成登录页面HTML 635 | function generateLoginHTML(isError = false) { 636 | return ` 637 | 638 | 639 | 640 | 641 | 642 | 登录 - VPS到期监控 643 | 644 | 713 | 714 | 715 |
716 |

VPS到期监控

717 |
密码错误,请重试
718 |
719 |
720 | 721 | 722 |
723 | 724 |
725 |
726 | 727 | 728 | `; 729 | } 730 | 731 | // 生成设置页面HTML 732 | function generateSettingsHTML(config, showError = false) { 733 | return ` 734 | 735 | 736 | 737 | 738 | 739 | 设置 - VPS到期监控 740 | 741 | 839 | 840 | 841 |
842 |

系统设置

843 |
存储VPS信息的URL直链为必填项
844 |
845 |
846 |
847 | 848 | 849 |
850 |
851 | 852 | 853 |
854 |
855 |
856 | 857 | 858 |
859 |
860 | 861 | 返回 862 |
863 |
864 |
865 | 866 | 867 | `; 868 | } 869 | -------------------------------------------------------------------------------- /vps_plus旧版.js: -------------------------------------------------------------------------------- 1 | // 从KV获取配置 2 | async function getConfig(env) { 3 | const kv = env.VPS_TG_KV; 4 | if (!kv) throw new Error("KV变量: VPS_TG_KV不存在"); 5 | try { 6 | const [sitename, vpsurl, days] = await Promise.all([ 7 | kv.get("sitename"), kv.get("vpsurl"), kv.get("days"), 8 | ]); 9 | return { 10 | sitename: sitename || "VPS到期监控", 11 | vpsurl: vpsurl || "", 12 | days: Number(days) || 5, 13 | }; 14 | } catch (error) { 15 | console.error("获取KV数据失败:", error); 16 | throw error; 17 | } 18 | } 19 | 20 | // 保存配置到KV 21 | async function saveConfig(env, newConfig) { 22 | const kv = env.VPS_TG_KV; 23 | try { 24 | await Promise.all([ 25 | kv.put("sitename", newConfig.sitename.trim()), 26 | kv.put("vpsurl", newConfig.vpsurl.trim()), 27 | kv.put("days", newConfig.days.trim()), 28 | ]); 29 | } catch (error) { 30 | console.error("保存KV数据失败:", error); 31 | throw error; 32 | } 33 | } 34 | 35 | // 获取 vps json 数据并解析 36 | async function getVpsData(env) { 37 | const { vpsurl } = await getConfig(env); 38 | if (!vpsurl) throw new Error("请在设置界面输入存储VPS信息的URL直链并保存"); 39 | 40 | try { 41 | // 添加请求头,避免缓存 42 | const requestOptions = { 43 | headers: { 44 | "Cache-Control": "no-cache, no-store, must-revalidate", 45 | Pragma: "no-cache", 46 | Expires: "0", 47 | }, 48 | }; 49 | const response = await fetch(vpsurl, requestOptions); 50 | if (!response.ok) { 51 | throw new Error(`获取VPS数据失败, HTTP状态码: ${response.status}`); 52 | } 53 | const vpsjson = await response.json(); 54 | if (!Array.isArray(vpsjson) || vpsjson.length === 0) { 55 | throw new Error("VPS数据格式格式不是json"); 56 | } 57 | return vpsjson; 58 | } catch (error) { 59 | console.error("获取 VPS 数据失败:", error); 60 | throw error; 61 | } 62 | } 63 | 64 | // 获取IP地址的国家、城市、ASN信息 65 | async function ipinfo_query(vpsjson) { 66 | const ipjson = await Promise.allSettled( 67 | vpsjson.map(async ({ ip }) => { 68 | const ipapiUrl = `https://ip.eooce.com/${ip}`; 69 | try { 70 | const ipresponse = await fetch(ipapiUrl); 71 | if (!ipresponse.ok) { 72 | console.error(`IP查询失败: ${ip},状态码: ${ipresponse.status}`); 73 | return null; 74 | } 75 | const { country_code, city, asn } = await ipresponse.json(); 76 | return { ip, country_code, city, asn }; 77 | } catch (error) { 78 | console.error(`请求IP信息失败: ${ip}`, error); 79 | return null; 80 | } 81 | }) 82 | ); 83 | return ipjson 84 | .filter(result => result.status === 'fulfilled' && result.value !== null) 85 | .map(result => result.value); 86 | } 87 | 88 | // 将IP信息与vps信息合并为一个新的数组 89 | function getMergeData(vpsjson, ipjson) { 90 | const ipMap = new Map(ipjson.map((ipdata) => [ipdata.ip, ipdata])); 91 | return vpsjson.map((vps) => { 92 | const mergeData = vps.ip ? ipMap.get(vps.ip) : null; 93 | return mergeData 94 | ? { 95 | ...vps, 96 | country_code: mergeData.country_code || "Unknown", 97 | city: mergeData.city || "Unknown", 98 | asn: mergeData.asn || "Unknown", 99 | } 100 | : vps; // 如果没有找到IP信息,返回原始数据 101 | }); 102 | } 103 | 104 | // 获取实时汇率数据 105 | async function getRates(env) { 106 | const apis = [ 107 | { 108 | url: "https://v2.xxapi.cn/api/exchange?from=USD&to=CNY&amount=1", 109 | parser: data => data.code === 200 && data.data?.rate 110 | ? { rawCNY: data.data.rate, timestamp: data.data.update_at } 111 | : null 112 | }, 113 | { 114 | url: "https://v2.xxapi.cn/api/allrates", 115 | parser: data => data.code === 200 && data.data?.rates?.CNY?.rate 116 | ? { rawCNY: data.data.rates.CNY.rate, timestamp: data.data.update_at } 117 | : null 118 | }, 119 | { 120 | url: `https://v6.exchangerate-api.com/v6/${env.RATE_API}/latest/USD`, 121 | parser: data => data.result === "success" && data.conversion_rates?.CNY 122 | ? { rawCNY: data.conversion_rates.CNY, timestamp: data.time_last_update_unix * 1000 } 123 | : null 124 | } 125 | ]; 126 | 127 | let rawCNY = null, timestamp = null; 128 | 129 | // 遍历 API 配置,获取数据 130 | for (const api of apis) { 131 | const parsed = await fetchData(api); 132 | if (parsed) { 133 | rawCNY ||= parsed.rawCNY; // 获取汇率 134 | timestamp ||= parsed.timestamp; // 获取时间戳 135 | if (rawCNY !== null && timestamp) break; // 如果都获取到了有效数据,跳出循环 136 | } 137 | } 138 | 139 | // 判断是否获得了有效的汇率数字 140 | if (typeof rawCNY === "number" && !isNaN(rawCNY)) { 141 | return { 142 | rateCNYnum: Number(rawCNY), 143 | rateTimestamp: new Date(timestamp).toISOString() 144 | }; 145 | } else { 146 | console.error("获取汇率数据失败,使用默认值"); 147 | return { 148 | rateCNYnum: 7.29, 149 | rateTimestamp: new Date().toISOString() 150 | }; 151 | } 152 | } 153 | 154 | // API 请求逻辑,包括超时控制、错误处理和解析数据 155 | async function fetchData(api) { 156 | const controller = new AbortController(); 157 | const timeoutId = setTimeout(() => controller.abort(), 500); 158 | try { 159 | const response = await fetch(api.url, { signal: controller.signal }); 160 | if (!response.ok) { 161 | console.error(`API 请求失败 ${api.url},状态码:${response.status}`); 162 | return null; 163 | } 164 | const data = await response.json(); 165 | return api.parser(data); // 返回解析后的数据 166 | } catch (err) { 167 | if (err.name !== "AbortError") { 168 | console.error(`API 请求错误 ${api.url}:`, err); 169 | } 170 | return null; 171 | } finally { 172 | clearTimeout(timeoutId); 173 | } 174 | } 175 | 176 | // 构建TG消息模板并在到期前发送提醒 177 | async function tgTemplate(mergeData, config, env) { 178 | const today = new Date().toISOString().split("T")[0]; 179 | await Promise.allSettled( 180 | mergeData.map(async (info) => { 181 | const endday = new Date(info.endday); 182 | const daysRemaining = Math.ceil((endday - new Date(today)) / (1000 * 60 * 60 * 24)); 183 | if (daysRemaining > 0 && daysRemaining <= Number(config.days)) { 184 | const message = `🚨 [VPS到期提醒] 🚨 185 | ==================== 186 | 🌍 VPS位置: ${info.country_code} | ${info.city} 187 | 💻 IP 地址: ${info.ip} 188 | ⏳ 剩余时间: ${daysRemaining} 天 189 | 📅 到期日期: ${info.endday} 190 | ⚠️ 点击续期:[${info.store}](${info.storeURL})`; 191 | 192 | const lastSent = await env.VPS_TG_KV.get(info.ip); // 检查是否已发送过通知 193 | if (!lastSent || lastSent.split("T")[0] !== today) { 194 | const isSent = await sendtgMessage(message, env); 195 | if (isSent) await env.VPS_TG_KV.put(info.ip, new Date().toISOString()); 196 | } 197 | } 198 | }) 199 | ); 200 | } 201 | 202 | // tg消息发送函数 203 | async function sendtgMessage(message, env) { 204 | if (!env.TGID || !env.TGTOKEN) { 205 | console.log("缺少变量 TGID 或 TGTOKEN, 跳过消息发送"); 206 | return; 207 | } 208 | 209 | const safemessage = message.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1"); 210 | const tgApiurl = `https://api.telegram.org/bot${env.TGTOKEN}/sendMessage`; 211 | const params = { 212 | chat_id: env.TGID, 213 | text: safemessage, 214 | parse_mode: "MarkdownV2", 215 | }; 216 | 217 | try { 218 | const response = await fetch(tgApiurl, { 219 | method: "POST", 220 | headers: { "Content-Type": "application/json" }, 221 | body: JSON.stringify(params), 222 | }); 223 | if (!response.ok) { 224 | console.error("Telegram 消息推送失败,状态码:", response.status); 225 | return false; 226 | } 227 | return true; 228 | } catch (error) { 229 | console.error("Telegram 消息推送失败:", error); 230 | return false; 231 | } 232 | } 233 | 234 | // 处理登录路由 235 | async function handleLogin(request, validPassword) { 236 | if (request.method === "POST") { 237 | const formData = await request.formData(); 238 | const password = formData.get("password"); 239 | 240 | if (password === validPassword) { 241 | return new Response(null, { 242 | status: 302, 243 | headers: { 244 | Location: "/", 245 | "Set-Cookie": `password=${password}; path=/; HttpOnly; Secure`, 246 | }, 247 | }); 248 | } else { 249 | return new Response(generateLoginHTML(true), { 250 | headers: { "Content-Type": "text/html" }, 251 | }); 252 | } 253 | } 254 | return new Response(generateLoginHTML(), { 255 | headers: { "Content-Type": "text/html" }, 256 | }); 257 | } 258 | 259 | // 处理设置路由 260 | async function handleSettings(request, config, env) { 261 | if (request.method === "POST") { 262 | const formData = await request.formData(); 263 | const newConfig = { 264 | sitename: formData.get("sitename"), 265 | vpsurl: formData.get("vpsurl"), 266 | days: formData.get("days"), 267 | }; 268 | 269 | if (!newConfig.vpsurl) { 270 | return new Response(generateSettingsHTML(newConfig, true), { 271 | headers: { "Content-Type": "text/html" }, 272 | }); 273 | } 274 | 275 | await saveConfig(env, newConfig); 276 | return Response.redirect(new URL("/", request.url).toString(), 302); 277 | } 278 | 279 | return new Response(generateSettingsHTML(config), { 280 | headers: { "Content-Type": "text/html" }, 281 | }); 282 | } 283 | 284 | // 处理根路由 285 | async function handleRoot(env, config) { 286 | try { 287 | const vpsjson = await getVpsData(env); 288 | if (!vpsjson) throw new Error("VPS json数据为空或无法加载数据"); 289 | const ipjson = await ipinfo_query(vpsjson); 290 | if (!ipjson) throw new Error("IP 信息查询失败"); 291 | const mergeData = getMergeData(vpsjson, ipjson); 292 | const ratejson = await getRates(env); 293 | 294 | await tgTemplate(mergeData, config, env); 295 | const htmlContent = await generateHTML(mergeData, ratejson, config.sitename); 296 | return new Response(htmlContent, { 297 | headers: { "Content-Type": "text/html" }, 298 | }); 299 | } catch (error) { 300 | console.error("Fetch error:", error); 301 | let errorMessage = "无法获取或解析VPS的json文件"; 302 | if (error.message.includes("VPS json数据为空或无法加载数据")) { 303 | errorMessage = "请检查 vpsurl 直链的格式及 json 内容格式"; 304 | } else if (error.message.includes("IP 信息查询失败")) { 305 | errorMessage = "IP 信息查询失败,可能是外部服务不可用"; 306 | } else { 307 | errorMessage = "未知错误,请稍后重试"; 308 | } 309 | return new Response(errorMessage, { status: 500 }); 310 | } 311 | } 312 | 313 | // 导出 fetch 函数 314 | export default { 315 | async fetch(request, env) { 316 | const url = new URL(request.url); 317 | const path = url.pathname; 318 | const validPassword = env.PASS || "123456"; 319 | const cookies = request.headers.get("Cookie") || ""; 320 | const isAuth = cookies.includes(`password=${validPassword}`); 321 | const config = await getConfig(env); 322 | 323 | if (!isAuth && path !== "/login") { 324 | return Response.redirect(`${url.origin}/login`, 302); 325 | } 326 | 327 | if (!config.vpsurl && path !== "/settings" && path !=="/login") { 328 | return Response.redirect(`${url.origin}/settings`, 302); 329 | } 330 | 331 | switch (path) { 332 | case "/login": 333 | return await handleLogin(request, validPassword); // 登录路由  334 | case "/settings": 335 | return await handleSettings(request, config, env); // 设置路由  336 | default: 337 | return await handleRoot(env, config); // 根路由 338 | } 339 | }, 340 | 341 | async scheduled(event, env, ctx) { 342 | try { 343 | const config = await getConfig(env); 344 | const vpsjson = await getVpsData(env); 345 | const ipjson = await ipinfo_query(vpsjson); 346 | const mergeData = getMergeData(vpsjson, ipjson); 347 | await tgTemplate(mergeData, config, env); 348 | console.log("Corn 执行时间:", new Date().toISOString()); 349 | } catch (error) { 350 | console.error("Cron 执行失败:", error); 351 | } 352 | }, 353 | }; 354 | 355 | // 生成主页HTML 356 | async function generateHTML(mergeData, ratejson, sitename) { 357 | const rows = await Promise.all( 358 | mergeData.map(async (info) => { 359 | const today = new Date(); 360 | const endday = new Date(info.endday); 361 | const daysRemaining = Math.ceil((endday - today) / (1000 * 60 * 60 * 24)); 362 | const isExpired = today > endday; 363 | const statusColor = isExpired ? "#e74c3c" : "#2ecc71"; 364 | const statusText = isExpired ? "已过期" : "正常"; 365 | 366 | // 计算年费价格和剩余价值 367 | const [, price, unit] = info.price.match(/^([\d.]+)([A-Za-z]+)$/) || []; 368 | const priceNum = parseFloat(price); 369 | const rateCNYnum = ratejson?.rateCNYnum || 7.29; 370 | const [ValueUSD, ValueCNY] = unit === "USD" 371 | ? [(priceNum / 365) * daysRemaining, (priceNum / 365) * daysRemaining * rateCNYnum] 372 | : [(priceNum / 365) * daysRemaining / rateCNYnum, (priceNum / 365) * daysRemaining]; 373 | const formatValueUSD = `${ValueUSD.toFixed(2)}USD`; 374 | const formatValueCNY = `${ValueCNY.toFixed(2)}CNY`; 375 | 376 | return ` 377 | 378 | 379 | ${info.ip} 380 | ${info.asn} 381 | ${info.country_code} 382 | ${info.city} 383 | ${info.store} 384 | ${info.startday} 385 | ${info.endday} 386 | ${isExpired ? "已过期" : daysRemaining + "天"} 387 | ${info.price} 388 | ${formatValueUSD} | ${formatValueCNY} 389 | 390 | `; 391 | }) 392 | ); 393 | return generateFormHTML(sitename, rows, ratejson); 394 | } 395 | 396 | function generateFormHTML(sitename, rows, ratejson) { 397 | const { rateCNYnum, rateTimestamp } = ratejson; 398 | const BeijingTime = new Date(rateTimestamp).toLocaleString("zh-CN", { 399 | timeZone: "Asia/Shanghai", 400 | hour12: false, // 使用24小时制 401 | }); 402 | 403 | return ` 404 | 405 | 406 | 407 | 408 | 409 | ${sitename} 410 | 411 | 533 | 542 | 543 | 544 |
545 |
546 |

${sitename}

547 | 设置 548 |
549 |
550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | ${rows.join("")} 568 | 569 |
状态IP地址ASN国家城市商家注册日到期日剩余天数年费价格剩余价值
570 |
571 |
572 | 580 | 581 | 582 | `; 583 | } 584 | 585 | // 生成登录页面HTML 586 | function generateLoginHTML(isError = false) { 587 | return ` 588 | 589 | 590 | 591 | 592 | 593 | 登录 - VPS到期监控 594 | 595 | 664 | 665 | 666 |
667 |

VPS到期监控

668 |
密码错误,请重试
669 |
670 |
671 | 672 | 673 |
674 | 675 |
676 |
677 | 678 | 679 | `; 680 | } 681 | 682 | // 生成设置页面HTML 683 | function generateSettingsHTML(config, showError = false) { 684 | return ` 685 | 686 | 687 | 688 | 689 | 690 | 设置 - VPS到期监控 691 | 692 | 790 | 791 | 792 |
793 |

系统设置

794 |
存储VPS信息的URL直链为必填项
795 |
796 |
797 |
798 | 799 | 800 |
801 |
802 | 803 | 804 |
805 |
806 |
807 | 808 | 809 |
810 |
811 | 812 | 返回 813 |
814 |
815 |
816 | 817 | 818 | `; 819 | } 820 | --------------------------------------------------------------------------------