├── README.md ├── server ├── app.js ├── ota.sh ├── protected │ ├── accounts.html │ ├── index.html │ ├── login.html │ ├── logs.html │ ├── nodes.html │ ├── ota.html │ └── set_password.html └── public │ ├── check_accounts.html │ └── notification_settings.html └── single ├── app.js ├── hy2ip.sh ├── install.sh ├── ota.sh └── public ├── config.html ├── hy2ip.html ├── index.html ├── log.html ├── newset.html ├── ota.html └── outbounds.html /README.md: -------------------------------------------------------------------------------- 1 | ## ● 声明: 2 | 非原创,本项目无大佬,本人小白,没有这个实力,全靠添义父 [@fjanenw](https://github.com/Qwsudo) 的打赏,以及群友编写调整。感谢 各位大佬 的奉献。 3 | 4 | ## ● 说明: 5 | 本项目为 网页保进程,和所谓的 “账号保活” 没有关系,实现的目标是无视官方杀不杀进程或删不删crontab后,后台自动扶梯,激活vps本地自动执行命令,启动进程,不需要登录SSH的任何操作。 6 | ## ● 适配: 7 | 适配 [饭奇骏](https://github.com/frankiejun/serv00-play) 大佬的 serv00-play 脚本。有问题联系本人 [机器人](https://t.me/SerokBot_bot) 。 8 | 9 | 【重点】:饭佬脚本中需要设置 6 选项,开启cron设 y ,建议59分钟。 10 | 11 | 【提醒】:[账号服务] 与 [本机保活] 无法共存(本人实力有限),意味着如果需要 多账号服务 就需要单独占用一个账号。 12 | 13 | ## ● 懒人一键自动安装: 14 | (不需要登陆面板),配置文件感谢群友 [@guitar295](https://t.me/guitar295) 贡献调整。 15 | 16 | bash <(curl -Ls https://raw.githubusercontent.com/ryty1/serv00-save-me/refs/heads/main/single/install.sh) 17 | 18 | ![Image Description](https://github.com/ryty1/alist-log/blob/main/github_images/0.jpg?raw=true) 19 | 20 | ## ● 功能(账号服务与本机保活无法共存): 21 | 22 | [账号服务定时访问设置](https://github.com/ryty1/web-visit) 由于波兰仔策略调整建议设置 2 小时一次,长时间进程无法拉起,这种需要手动访问账号的保活 或 账号服务 登录(不用登录)网页。 23 | 24 | 经测试cf worker项目设置每2小时一次基本可以正常! 25 | 26 | 账号服务:(只要装1台) 27 | 【4月2日后更新过的需要重新部署CF,有关键修改】 28 | 1、多账号管理(与保活连通) 29 | 2、多账号节点汇聚订阅 30 | 3、CF部署激活多账号进程(只有失败通知) 31 | 4、账号状态检测及监控 32 | 5、通知设置 33 | 6、日志管理 34 | 7、在线更新 35 | 36 | 37 | 本机保活:(装完把账号填到账号服务端) 38 | 1、进程激活, 39 | 2、更换HY2_IP 40 | 3、节点查看及提取 41 | 4、节点改名 42 | 5、修改配置参数 43 | 6、出站配置 44 | 7、查看日志及进程列表 45 | 8、在线更新 46 | 47 | 48 | ## ● 截图预览(部分功能展示): 49 | 50 | ![Image Description](https://raw.githubusercontent.com/ryty1/alist-log/refs/heads/main/github_images/7.png?raw=true) 51 | 52 | ![Image Description](https://raw.githubusercontent.com/ryty1/alist-log/refs/heads/main/github_images/8.png?raw=true) 53 | 54 | ## ● 自己可以杀掉进程再刷新网页,然后在SSH端 ps aux 查询进程 55 | 56 | ## ● 大厂优选域名 57 | 58 | | 序号 | 优选域名 | 说明 | 59 | |----|------------------------|----| 60 | | 1 | cdnjs.com | 推荐 | 61 | | 2 | www.racknerd.com | 推荐 | 62 | | 3 | ns.cloudflare.com | | 63 | | 4 | developers.cloudflare.com | | 64 | | 5 | www.fortnite.com | | 65 | | 6 | www.wto.org | | 66 | 67 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const session = require("express-session"); 3 | const FileStore = require("session-file-store")(session); 4 | const http = require("http"); 5 | const { exec } = require("child_process"); 6 | const socketIo = require("socket.io"); 7 | const axios = require("axios"); 8 | const fs = require("fs"); 9 | const path = require("path"); 10 | const cron = require("node-cron"); 11 | const TelegramBot = require("node-telegram-bot-api"); 12 | const bodyParser = require("body-parser"); 13 | const crypto = require("crypto"); 14 | 15 | const app = express(); 16 | const server = http.createServer(app); 17 | const io = socketIo(server); 18 | 19 | const PORT = 3000; 20 | const ACCOUNTS_FILE = path.join(__dirname, "accounts.json"); 21 | const SETTINGS_FILE = path.join(__dirname, "settings.json"); 22 | const PASSWORD_FILE = path.join(__dirname, "password.json"); 23 | const SESSION_DIR = path.join(__dirname, "sessions"); 24 | const SESSION_FILE = path.join(__dirname, "session_secret.json"); 25 | const otaScriptPath = path.join(__dirname, 'ota.sh'); 26 | 27 | app.use(express.json()); 28 | app.use(express.static(path.join(__dirname, "public"))); 29 | 30 | app.use((req, res, next) => { 31 | res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); 32 | res.setHeader("Pragma", "no-cache"); 33 | res.setHeader("Expires", "0"); 34 | next(); 35 | }); 36 | 37 | function getSessionSecret() { 38 | if (fs.existsSync(SESSION_FILE)) { 39 | return JSON.parse(fs.readFileSync(SESSION_FILE, "utf-8")).secret; 40 | } else { 41 | const secret = crypto.randomBytes(32).toString("hex"); 42 | fs.writeFileSync(SESSION_FILE, JSON.stringify({ secret }), "utf-8"); 43 | return secret; 44 | } 45 | } 46 | 47 | app.use(session({ 48 | store: new FileStore({ 49 | path: path.join(__dirname, "sessions"), 50 | ttl: 60 * 60, 51 | retries: 0, 52 | clearInterval: 3600 53 | }), 54 | secret: getSessionSecret(), 55 | resave: false, 56 | saveUninitialized: false, 57 | cookie: { secure: false, httpOnly: true } 58 | })); 59 | 60 | app.use(bodyParser.urlencoded({ extended: true })); 61 | 62 | function checkPassword(req, res, next) { 63 | if (!fs.existsSync(PASSWORD_FILE)) { 64 | return res.redirect("/setPassword"); 65 | } 66 | next(); 67 | } 68 | 69 | app.get("/checkSession", (req, res) => { 70 | if (req.session.authenticated) { 71 | res.status(200).json({ authenticated: true }); 72 | } else { 73 | res.status(401).json({ authenticated: false }); 74 | } 75 | }); 76 | 77 | function isAuthenticated(req, res, next) { 78 | if (req.session.authenticated) { 79 | return next(); 80 | } 81 | res.redirect("/login"); 82 | } 83 | 84 | app.get("/setPassword", (req, res) => { 85 | res.sendFile(path.join(__dirname, "protected", "set_password.html")); 86 | }); 87 | 88 | app.post("/setPassword", (req, res) => { 89 | const { password } = req.body; 90 | if (!password) { 91 | return res.status(400).send("密码不能为空"); 92 | } 93 | fs.writeFileSync(PASSWORD_FILE, JSON.stringify({ password }), "utf-8"); 94 | res.redirect("/login"); 95 | }); 96 | 97 | const errorCache = new Map(); 98 | async function sendErrorToTG(user, status, message) { 99 | try { 100 | const settings = getNotificationSettings(); 101 | if (!settings.telegramToken || !settings.telegramChatId) { 102 | console.log("❌ Telegram 设置不完整,无法发送通知"); 103 | return; 104 | } 105 | 106 | const now = Date.now(); 107 | const cacheKey = `${user}:${status}`; 108 | const lastSentTime = errorCache.get(cacheKey); 109 | 110 | // 只发送一次 404 错误 111 | if (status === 404 && lastSentTime) { 112 | console.log(`⏳ 404 状态已发送过 ${user},跳过通知`); 113 | return; 114 | } 115 | 116 | // 其他错误 3 小时内不重复发送 117 | if (status !== 404 && lastSentTime && now - lastSentTime < 3 * 60 * 60 * 1000) { 118 | console.log(`⏳ 3小时内已发送过 ${user} 的状态 ${status},跳过通知`); 119 | return; 120 | } 121 | 122 | // 记录发送时间 123 | errorCache.set(cacheKey, now); 124 | 125 | const bot = new TelegramBot(settings.telegramToken, { polling: false }); 126 | const nowStr = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); 127 | 128 | let seasons; 129 | try { 130 | const accountsData = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, "utf8")); 131 | seasons = accountsData[user]?.season?.toLowerCase(); 132 | } catch (err) { 133 | console.error("⚠️ 读取 accounts.json 失败:", err); 134 | } 135 | 136 | let titleBar, statusMessage, buttonText, buttonUrl; 137 | if (status === 403) { 138 | titleBar = "📥 Serv00 阵亡通知书"; 139 | statusMessage = "账号已封禁"; 140 | buttonText = "重新申请账号"; 141 | buttonUrl = "https://www.serv00.com/offer/create_new_account"; 142 | } else if (status === 404) { 143 | titleBar = "🟠 HtmlOnLive 提醒"; 144 | statusMessage = "保活未安装"; 145 | buttonText = "前往安装保活"; 146 | buttonUrl = "https://github.com/ryty1/serv00-save-me"; 147 | } else if (status >= 500 && status <= 599) { 148 | titleBar = "🔴 HtmlOnLive 失败通知"; 149 | statusMessage = "服务器错误"; 150 | buttonText = "查看服务器状态"; 151 | buttonUrl = "https://ssss.nyc.mn/"; 152 | } else { 153 | titleBar = "🔴 HtmlOnLive 失败通知"; 154 | statusMessage = `访问异常`; 155 | buttonText = "手动进入保活"; 156 | buttonUrl = `https://${user}.serv00.net/info`; 157 | } 158 | 159 | const formattedMessage = ` 160 | *${titleBar}* 161 | —————————————————— 162 | 👤 账号: \`${user}\` 163 | 🖥️ 主机: \`${seasons}.serv00.com\` 164 | 📶 状态: *${statusMessage}* 165 | 📝 详情: *${status}*•\`${message}\` 166 | —————————————————— 167 | 🕒 时间: \`${nowStr}\``; 168 | 169 | const options = { 170 | parse_mode: "Markdown", 171 | reply_markup: { 172 | inline_keyboard: [[ 173 | { text: buttonText, url: buttonUrl } 174 | ]] 175 | } 176 | }; 177 | 178 | await bot.sendMessage(settings.telegramChatId, formattedMessage, options); 179 | 180 | console.log(`✅ 已发送 Telegram 通知: ${user} - ${status}`); 181 | } catch (err) { 182 | console.error("❌ 发送 Telegram 通知失败:", err); 183 | } 184 | } 185 | 186 | app.get("/login", async (req, res) => { 187 | res.sendFile(path.join(__dirname, "protected", "login.html")); 188 | 189 | try { 190 | const accounts = await getAccounts(true); 191 | const users = Object.keys(accounts); 192 | 193 | const requests = users.map(user => 194 | axios.get(`https://${user}.serv00.net/info`, { 195 | timeout: 10000, 196 | headers: { 197 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 198 | "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", 199 | } 200 | }) 201 | .then(response => { 202 | if (response.status === 200 && response.data) { 203 | console.log(`✅ ${user} 保活成功,状态码: ${response.status},响应: ${response.data.length} 字节`); 204 | 205 | return new Promise(resolve => setTimeout(resolve, 3000)); 206 | } else { 207 | console.log(`❌ ${user} 保活失败,状态码: ${response.status},无数据`); 208 | sendErrorToTG(user, response.status, "响应数据为空"); 209 | } 210 | }) 211 | .catch(err => { 212 | if (err.response) { 213 | console.log(`❌ ${user} 保活失败,状态码: ${err.response.status}`); 214 | sendErrorToTG(user, err.response.status, err.response.statusText); 215 | } else { 216 | console.log(`❌ ${user} 保活失败: ${err.message}`); 217 | sendErrorToTG(user, "请求失败", err.message); 218 | } 219 | }) 220 | ); 221 | 222 | await Promise.allSettled(requests); 223 | console.log("✅ 所有账号的进程保活已访问完成"); 224 | 225 | } catch (error) { 226 | console.error("❌ 访问 /info 失败:", error); 227 | sendErrorToTG("系统", "全局错误", error.message); 228 | } 229 | }); 230 | 231 | app.post("/login", (req, res) => { 232 | const { password } = req.body; 233 | if (!fs.existsSync(PASSWORD_FILE)) { 234 | return res.status(400).send("密码文件不存在,请先设置密码"); 235 | } 236 | 237 | const savedPassword = JSON.parse(fs.readFileSync(PASSWORD_FILE, "utf-8")).password; 238 | if (password === savedPassword) { 239 | req.session.authenticated = true; 240 | res.redirect("/"); 241 | } else { 242 | res.status(401).send("密码错误"); 243 | } 244 | }); 245 | 246 | app.get("/online", async (req, res) => { 247 | try { 248 | const accounts = await getAccounts(true); 249 | const users = Object.keys(accounts); 250 | 251 | const requests = users.map(user => 252 | axios.get(`https://${user}.serv00.net/info`, { 253 | timeout: 10000, 254 | headers: { 255 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 256 | "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", 257 | } 258 | }) 259 | .then(response => { 260 | if (response.status === 200 && response.data) { 261 | console.log(`✅ ${user} 保活成功,状态码: ${response.status},响应: ${response.data.length} 字节`); 262 | 263 | return new Promise(resolve => setTimeout(resolve, 3000)); 264 | } else { 265 | console.log(`❌ ${user} 保活失败,状态码: ${response.status},无数据`); 266 | sendErrorToTG(user, response.status, "响应数据为空"); 267 | } 268 | }) 269 | .catch(err => { 270 | if (err.response) { 271 | console.log(`❌ ${user} 保活失败,状态码: ${err.response.status}`); 272 | sendErrorToTG(user, err.response.status, err.response.statusText); 273 | } else { 274 | console.log(`❌ ${user} 保活失败: ${err.message}`); 275 | sendErrorToTG(user, "请求失败", err.message); 276 | } 277 | }) 278 | ); 279 | 280 | // 等待所有请求完成 281 | await Promise.allSettled(requests); 282 | 283 | console.log("✅ 所有账号的进程保活已访问完成"); 284 | res.status(200).send("保活操作完成"); // 响应结束 285 | } catch (error) { 286 | console.error("❌ 访问 /info 失败:", error); 287 | sendErrorToTG("系统", "全局错误", error.message); 288 | res.status(500).send("系统错误"); 289 | } 290 | }); 291 | 292 | app.get("/login", async (req, res) => { 293 | res.sendFile(path.join(__dirname, "protected", "login.html")); 294 | }); 295 | 296 | app.post("/login", (req, res) => { 297 | const { password } = req.body; 298 | if (!fs.existsSync(PASSWORD_FILE)) { 299 | return res.status(400).send("密码文件不存在,请先设置密码"); 300 | } 301 | 302 | const savedPassword = JSON.parse(fs.readFileSync(PASSWORD_FILE, "utf-8")).password; 303 | if (password === savedPassword) { 304 | req.session.authenticated = true; 305 | res.redirect("/"); 306 | } else { 307 | res.status(401).send("密码错误"); 308 | } 309 | }); 310 | 311 | app.get("/logout", (req, res) => { 312 | try { 313 | if (fs.existsSync(SESSION_DIR)) { 314 | fs.readdirSync(SESSION_DIR).forEach(file => { 315 | const filePath = path.join(SESSION_DIR, file); 316 | if (file.endsWith(".json")) { 317 | if (fs.existsSync(filePath)) { 318 | fs.unlinkSync(filePath); 319 | console.log("已删除 session 登录密钥文件"); 320 | } 321 | } 322 | }); 323 | } 324 | } catch (error) { 325 | console.error("删除 session 文件失败:", error); 326 | } 327 | 328 | req.session.destroy(() => { 329 | res.redirect("/login"); 330 | }); 331 | }); 332 | 333 | 334 | const protectedRoutes = ["/", "/ota", "/accounts", "/nodes", "/online"]; 335 | protectedRoutes.forEach(route => { 336 | app.get(route, checkPassword, isAuthenticated, (req, res) => { 337 | res.sendFile(path.join(__dirname, "protected", route === "/" ? "index.html" : `${route.slice(1)}.html`)); 338 | }); 339 | }); 340 | 341 | const MAIN_SERVER_USER = process.env.USER || process.env.USERNAME || "default_user"; 342 | async function getAccounts(excludeMainUser = true) { 343 | if (!fs.existsSync(ACCOUNTS_FILE)) return {}; 344 | let accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, "utf-8")); 345 | if (excludeMainUser) { 346 | delete accounts[MAIN_SERVER_USER]; 347 | } 348 | return accounts; 349 | } 350 | 351 | io.on("connection", (socket) => { 352 | console.log("Client connected"); 353 | socket.on("startNodesSummary", () => { 354 | getNodesSummary(socket); 355 | }); 356 | 357 | socket.on("loadAccounts", async () => { 358 | const accounts = await getAccounts(true); 359 | socket.emit("accountsList", accounts); 360 | }); 361 | 362 | socket.on("saveAccount", async (accountData) => { 363 | const accounts = await getAccounts(false); 364 | accounts[accountData.user] = { 365 | user: accountData.user, 366 | season: accountData.season || "" 367 | }; 368 | fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2)); 369 | socket.emit("accountsList", await getAccounts(true)); 370 | }); 371 | 372 | socket.on("deleteAccount", async (user) => { 373 | const accounts = await getAccounts(false); 374 | delete accounts[user]; 375 | fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2)); 376 | socket.emit("accountsList", await getAccounts(true)); 377 | }); 378 | 379 | socket.on("updateSeason", async (data) => { 380 | const accounts = await getAccounts(false); 381 | if (accounts[data.user]) { 382 | accounts[data.user].season = data.season; 383 | fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2)); 384 | } 385 | socket.emit("accountsList", await getAccounts(true)); 386 | }); 387 | }); 388 | 389 | const SUB_FILE_PATH = path.join(__dirname, "sub.json"); 390 | 391 | // 过滤有效的节点(vmess 和 hysteria2) 392 | function filterNodes(nodes) { 393 | return nodes.filter(node => node.startsWith("vmess://") || node.startsWith("hysteria2://")); 394 | } 395 | 396 | let isProcessing = false; // 设置标志,防止重复请求 397 | 398 | async function getNodesSummary(socket) { 399 | // 防止重复请求 400 | if (isProcessing) { 401 | // console.log("请求已在处理中,忽略重复请求"); 402 | return; 403 | } 404 | 405 | isProcessing = true; // 设置为正在处理 406 | 407 | const accounts = await getAccounts(true); 408 | if (!accounts || Object.keys(accounts).length === 0) { 409 | console.log("⚠️ 未找到账号数据!"); 410 | socket.emit("nodesSummary", { successfulNodes: { hysteria2: [], vmess: [] }, failedAccounts: [] }); 411 | isProcessing = false; // 请求结束,重置标志 412 | return; 413 | } 414 | 415 | const users = Object.keys(accounts); 416 | let successfulNodes = { hysteria2: [], vmess: [] }; 417 | let failedAccounts = []; 418 | 419 | for (let user of users) { 420 | const nodeUrl = `https://${user}.serv00.net/node`; 421 | try { 422 | // console.log(`开始请求节点数据: ${nodeUrl}`); 423 | const nodeResponse = await axios.get(nodeUrl, { timeout: 5000 }); 424 | console.log(`✅ 账号 ${user} 采集完成!`); 425 | 426 | const nodeData = nodeResponse.data; 427 | const nodeLinks = filterNodes([ 428 | ...(nodeData.match(/vmess:\/\/[^\s<>"]+/g) || []), 429 | ...(nodeData.match(/hysteria2:\/\/[^\s<>"]+/g) || []) 430 | ]); 431 | 432 | nodeLinks.forEach(link => { 433 | if (link.startsWith("hysteria2://")) { 434 | successfulNodes.hysteria2.push(link); 435 | } else if (link.startsWith("vmess://")) { 436 | successfulNodes.vmess.push(link); 437 | } 438 | }); 439 | 440 | if (nodeLinks.length === 0) { 441 | console.log(`⚠️ 账号 ${user} 连接成功但无有效节点`); 442 | failedAccounts.push(user); 443 | } 444 | } catch (error) { 445 | console.log(`❌ 账号 ${user} 采集失败: ${error.message}`); 446 | failedAccounts.push(user); 447 | } 448 | } 449 | 450 | // 整理成 Base64 订阅格式 451 | const allNodes = [...successfulNodes.hysteria2, ...successfulNodes.vmess].join("\n"); 452 | const base64Sub = Buffer.from(allNodes).toString("base64"); 453 | 454 | // 生成 `sub.json` 455 | const subData = { sub: base64Sub }; 456 | fs.writeFileSync(SUB_FILE_PATH, JSON.stringify(subData, null, 4)); 457 | 458 | console.log("订阅文件 sub.json 已更新!"); 459 | 460 | socket.emit("nodesSummary", { successfulNodes, failedAccounts }); 461 | 462 | isProcessing = false; // 处理完毕 463 | } 464 | 465 | io.on("connection", (socket) => { 466 | console.log("客户端已连接"); 467 | 468 | socket.on("startNodesSummary", async () => { 469 | await getNodesSummary(socket); 470 | }); 471 | }); 472 | 473 | app.get('/sub', (req, res) => { 474 | try { 475 | const subData = JSON.parse(fs.readFileSync('sub.json', 'utf8')); // 解析 JSON 476 | if (subData.sub) { 477 | res.setHeader('Content-Type', 'text/plain'); // 纯文本 478 | res.send(subData.sub); // 只返回 Base64 订阅内容 479 | } else { 480 | res.status(500).send('订阅内容为空'); 481 | } 482 | } catch (err) { 483 | res.status(500).send('订阅文件读取失败'); 484 | } 485 | }); 486 | 487 | let cronJob = null; 488 | 489 | function getNotificationSettings() { 490 | if (!fs.existsSync(SETTINGS_FILE)) return {}; 491 | return JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8")); 492 | } 493 | 494 | function saveNotificationSettings(settings) { 495 | fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); 496 | } 497 | 498 | function resetCronJob() { 499 | if (cronJob) { 500 | cronJob.stop(); 501 | cronJob = null; 502 | } 503 | 504 | const settings = getNotificationSettings(); 505 | if (!settings || !settings.cronEnabled || !settings.cronExpression) return; 506 | 507 | if (!cron.validate(settings.cronExpression)) { 508 | return console.error("❌ 无效的 cron 表达式:", settings.cronExpression); 509 | } 510 | 511 | cronJob = cron.schedule(settings.cronExpression, () => { 512 | console.log("⏰ 运行通知任务..."); 513 | sendCheckResultsToTG(); 514 | }); 515 | 516 | console.log("✅ 定时任务已启动:", settings.cronExpression); 517 | } 518 | 519 | app.post("/setTelegramSettings", (req, res) => { 520 | const { telegramToken, telegramChatId } = req.body; 521 | if (!telegramToken || !telegramChatId) { 522 | return res.status(400).json({ message: "Telegram 配置不完整" }); 523 | } 524 | fs.writeFileSync(SETTINGS_FILE, JSON.stringify({ telegramToken, telegramChatId }, null, 2)); 525 | res.json({ message: "Telegram 设置已更新" }); 526 | }); 527 | app.get("/getTelegramSettings", (req, res) => { 528 | if (!fs.existsSync(SETTINGS_FILE)) { 529 | return res.json({ telegramToken: "", telegramChatId: "" }); 530 | } 531 | const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8")); 532 | res.json(settings); 533 | }); 534 | 535 | async function sendCheckResultsToTG() { 536 | try { 537 | const settings = getNotificationSettings(); 538 | if (!settings.telegramToken || !settings.telegramChatId) { 539 | console.log("❌ Telegram 设置不完整,无法发送通知"); 540 | return; 541 | } 542 | 543 | const bot = new TelegramBot(settings.telegramToken, { polling: false }); 544 | const response = await axios.post(`https://${process.env.USER}.serv00.net/checkAccounts`, {}); 545 | const data = response.data.results; 546 | 547 | if (!data || Object.keys(data).length === 0) { 548 | await bot.sendMessage(settings.telegramChatId, "📋 账号检测结果:没有账号需要检测", { parse_mode: "MarkdownV2" }); 549 | return; 550 | } 551 | 552 | let results = []; 553 | let maxUserLength = 0; 554 | let maxSeasonLength = 0; 555 | 556 | const users = Object.keys(data); 557 | const maxIndexLength = String(users.length).length; 558 | 559 | users.forEach(user => { 560 | maxUserLength = Math.max(maxUserLength, user.length); 561 | maxSeasonLength = Math.max(maxSeasonLength, (data[user]?.season || "").length); 562 | }); 563 | 564 | users.forEach((user, index) => { 565 | const paddedIndex = String(index + 1).padStart(maxIndexLength, "0"); 566 | const paddedUser = user.padEnd(maxUserLength, " "); 567 | const season = (data[user]?.season || "--").padEnd(maxSeasonLength + 1, " "); 568 | const status = data[user]?.status || "未知状态"; 569 | results.push(`${paddedIndex}. ${paddedUser} : ${season}- ${status}`); 570 | }); 571 | 572 | const beijingTime = new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" }); 573 | let message = `㊙️ 账号检测结果:\n\n\`\`\`\n${results.join("\n")}\n\`\`\`\n\n⏰ 北京时间:${beijingTime}`; 574 | const options = { 575 | parse_mode: "MarkdownV2", 576 | reply_markup: { 577 | inline_keyboard: [ 578 | [{ text: "🔍 其它账号检测", url: "https://checks.594880.xyz" }] 579 | ] 580 | } 581 | }; 582 | 583 | await bot.sendMessage(settings.telegramChatId, message, options); 584 | 585 | } catch (error) { 586 | console.error("❌ 发送 Telegram 失败:", error); 587 | } 588 | } 589 | 590 | app.get("/", isAuthenticated, (req, res) => { 591 | res.sendFile(path.join(__dirname, "protected", "index.html")); 592 | }); 593 | app.get("/getMainUser", isAuthenticated, (req, res) => { 594 | res.json({ mainUser: MAIN_SERVER_USER }); 595 | }); 596 | app.get("/accounts", isAuthenticated, (req, res) => { 597 | res.sendFile(path.join(__dirname, "protected", "accounts.html")); 598 | }); 599 | app.get("/nodes", isAuthenticated, (req, res) => { 600 | res.sendFile(path.join(__dirname, "protected", "nodes.html")); 601 | }); 602 | app.get("/info", (req, res) => { 603 | const user = req.query.user; 604 | if (!user) return res.status(400).send("用户未指定"); 605 | res.redirect(`https://${user}.serv00.net/info`); 606 | }); 607 | 608 | app.get("/checkAccountsPage", isAuthenticated, (req, res) => { 609 | res.sendFile(path.join(__dirname, "public", "check_accounts.html")); 610 | }); 611 | 612 | const statusMessages = { 613 | 200: "账号正常", 614 | 301: "账号未注册", 615 | 302: "账号正常", 616 | 403: "账号已封禁", 617 | 404: "账号正常", 618 | 500: "服务器错误", 619 | 502: "网关错误", 620 | 503: "VPS不可用", 621 | 504: "网关超时", 622 | }; 623 | 624 | app.post("/checkAccounts", async (req, res) => { 625 | try { 626 | const accounts = await getAccounts(); 627 | const users = Object.keys(accounts); 628 | 629 | if (users.length === 0) { 630 | return res.json({ status: "success", results: {} }); 631 | } 632 | 633 | let results = {}; 634 | const promises = users.map(async (username) => { 635 | const apiUrl = `https://${username}.serv00.net`; 636 | 637 | try { 638 | const response = await axios.get(apiUrl, { 639 | maxRedirects: 0, 640 | timeout: 5000 641 | }); 642 | const status = response.status; 643 | const message = statusMessages[status] || "未知状态"; 644 | results[username] = { 645 | status: message, 646 | season: accounts[username]?.season || "--" 647 | }; 648 | } catch (error) { 649 | let status = "检测失败"; 650 | 651 | if (error.response) { 652 | status = error.response.status; 653 | } else if (error.code === 'ECONNABORTED') { 654 | status = "请求超时"; 655 | } 656 | 657 | results[username] = { 658 | status: statusMessages[status] || "未知状态", 659 | season: accounts[username]?.season || "--" 660 | }; 661 | } 662 | }); 663 | 664 | await Promise.all(promises); 665 | 666 | let orderedResults = {}; 667 | users.forEach(user => { 668 | orderedResults[user] = results[user]; 669 | }); 670 | 671 | res.json({ status: "success", results: orderedResults }); 672 | 673 | } catch (error) { 674 | console.error("批量账号检测错误:", error); 675 | res.status(500).json({ status: "error", message: "检测失败,请稍后再试" }); 676 | } 677 | }); 678 | 679 | // 获取通知设置 680 | app.get("/getNotificationSettings", (req, res) => { 681 | res.json(getNotificationSettings()); 682 | }); 683 | 684 | // 设置通知参数 685 | app.post("/setNotificationSettings", (req, res) => { 686 | const { telegramToken, telegramChatId, cronEnabled, cronExpression } = req.body; 687 | 688 | if (!telegramToken || !telegramChatId) { 689 | return res.status(400).json({ message: "Token 和 Chat ID 不能为空" }); 690 | } 691 | 692 | if (cronEnabled && (!cronExpression || !cron.validate(cronExpression))) { 693 | return res.status(400).json({ message: "无效的 Cron 表达式" }); 694 | } 695 | 696 | const settings = { telegramToken, telegramChatId, cronEnabled, cronExpression }; 697 | saveNotificationSettings(settings); 698 | 699 | resetCronJob(); 700 | 701 | res.json({ message: "✅ 设置已保存并生效" }); 702 | }); 703 | 704 | // 服务器启动时初始化任务 705 | resetCronJob(); 706 | 707 | app.get("/notificationSettings", isAuthenticated, (req, res) => { 708 | res.sendFile(path.join(__dirname, "public", "notification_settings.html")); 709 | }); 710 | 711 | app.get("/catlog-data", isAuthenticated, (req, res) => { 712 | const errorLogFilePath = path.join(process.env.HOME, "domains", `${MAIN_SERVER_USER}.serv00.net`, "logs", "error.log"); 713 | 714 | fs.readFile(errorLogFilePath, 'utf8', (err, data) => { 715 | if (err) { 716 | return res.status(500).send('Error reading log file.'); 717 | } 718 | res.send(data); 719 | }); 720 | }); 721 | 722 | app.post("/clear-log", isAuthenticated, (req, res) => { 723 | const errorLogFilePath = path.join(process.env.HOME, "domains", `${MAIN_SERVER_USER}.serv00.net`, "logs", "error.log"); 724 | 725 | fs.writeFile(errorLogFilePath, '', (err) => { 726 | if (err) { 727 | return res.status(500).send('日志清理失败'); 728 | } 729 | res.send('日志清理完成'); 730 | }); 731 | }); 732 | 733 | app.get("/catlog", isAuthenticated, (req, res) => { 734 | res.sendFile(path.join(__dirname, "protected", "logs.html")); 735 | }); 736 | 737 | app.get('/ota/update', isAuthenticated, async (req, res) => { 738 | console.log("🚀 开始 OTA 更新..."); 739 | 740 | const { keepAlive } = req.query; 741 | let keepAliveOutput = ''; 742 | 743 | if (keepAlive === 'true') { 744 | try { 745 | const accounts = await getAccounts(); 746 | const users = Object.keys(accounts); 747 | 748 | console.log(`🔄 检测到 ${users.length} 个账号,开始保活端更新...`); 749 | 750 | for (const user of users) { 751 | try { 752 | const keepAliveUrl = `https://${user}.serv00.net/ota/update`; 753 | // console.log(`🔄 访问: ${keepAliveUrl}`); 754 | 755 | const response = await axios.get(keepAliveUrl, { timeout: 20000 }); 756 | const output = response.data.output || '未返回内容'; 757 | 758 | keepAliveOutput += `👤 ${user},更新结果: \n${output}\n`; 759 | console.log(`✅ 账号 ${user} 保活端更新完成`); 760 | } catch (error) { 761 | keepAliveOutput += `👤 ${user},更新失败: \n${error.message}\n`; 762 | console.error(`❌ 账号 ${user} 保活端更新失败: ${error.message}`); 763 | } 764 | } 765 | } catch (error) { 766 | console.error(`❌ 获取账号列表失败: ${error.message}`); 767 | return res.status(500).json({ success: false, message: `获取账号列表失败: ${error.message}` }); 768 | } 769 | } 770 | 771 | const downloadScriptCommand = 'curl -Ls -o /tmp/ota.sh https://raw.githubusercontent.com/ryty1/serv00-save-me/refs/heads/main/server/ota.sh'; 772 | 773 | exec(downloadScriptCommand, (error, stdout, stderr) => { 774 | if (error) { 775 | console.error(`❌ 下载失败: ${error.message}`); 776 | return res.status(500).json({ success: false, message: `下载失败: ${error.message}` }); 777 | } 778 | 779 | console.log("✅ 更新工具下载完成"); 780 | const executeScriptCommand = 'bash /tmp/ota.sh'; 781 | 782 | exec(executeScriptCommand, (error, stdout, stderr) => { 783 | exec('rm -f /tmp/ota.sh', () => console.log('✅ 缓存文件清理完成')); 784 | 785 | if (error) { 786 | console.error(`❌ 执行失败: ${error.message}`); 787 | return res.status(500).json({ success: false, message: `执行失败: ${error.message}` }); 788 | } 789 | 790 | console.log("✅ 账号服务 OTA 更新完成"); 791 | 792 | // 组合最终输出内容,保持原格式,仅在前面追加保活端日志 793 | const finalOutput = keepAliveOutput + (stdout || '执行成功'); 794 | 795 | res.json({ success: true, output: finalOutput }); 796 | }); 797 | }); 798 | }); 799 | 800 | app.get('/ota', isAuthenticated, (req, res) => { 801 | res.sendFile(path.join(__dirname, "protected", "ota.html")); 802 | }); 803 | 804 | cron.schedule("0 */12 * * *", () => { 805 | const logFile = path.join(process.env.HOME, "domains", `${username}.serv00.net`, "logs", "error.log"); 806 | if (fs.existsSync(logFile)) { 807 | fs.truncateSync(logFile, 0); // 清空文件内容 808 | console.log("✅ 日志文件已清空:", new Date().toLocaleString()); 809 | } 810 | }); 811 | 812 | server.listen(PORT, () => { 813 | console.log(`🚀 服务己启动,监听端口: ${PORT}`); 814 | }); 815 | -------------------------------------------------------------------------------- /server/ota.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 配置变量 4 | USER=$(whoami) 5 | USER_NAME=$(echo "$USER" | tr '[:upper:]' '[:lower:]') # 获取当前用户名并转换为小写 6 | REPO_PATH="/home/$USER/serv00-save-me" 7 | SERVER_PATH="$REPO_PATH/server" 8 | TARGET_PATH="/home/$USER/domains/$USER_NAME.serv00.net/public_nodejs" 9 | BRANCH="main" # 根据你的仓库调整分支 10 | 11 | # 设置 GIT_DISCOVERY_ACROSS_FILESYSTEM 环境变量(避免跨文件系统的错误) 12 | export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 13 | 14 | # 进入仓库目录 15 | cd "$REPO_PATH" || { echo "🚫 目录不是 Git 环境!"; exit 1; } 16 | 17 | # 检查仓库是否正确初始化 18 | if [ ! -d ".git" ]; then 19 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 20 | echo "🚫 运行环境错误" 21 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 22 | exit 1 23 | fi 24 | 25 | # 记录 server 目录下的变动文件,排除 .sh 和 .md 文件 26 | echo "⬇️ 账号服务更新" 27 | git fetch origin "$BRANCH" >/dev/null 2>&1 28 | CHANGED_FILES=$(git diff --name-only origin/"$BRANCH" -- server | grep -Ev '\.sh$|\.md$') 29 | 30 | # 如果没有文件变动,则退出 31 | if [ -z "$CHANGED_FILES" ]; then 32 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 33 | echo "✅ 文件均为最新!" 34 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 35 | exit 0 36 | fi 37 | 38 | # 打印有文件更新 39 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 40 | echo "💡 发现 有文件变动:" 41 | for file in $CHANGED_FILES; do 42 | RELATIVE_PATH=$(echo "$file" | sed -e 's/^server\///' -e 's/^protected\///' -e 's/^public\///') 43 | echo "🎯 $RELATIVE_PATH" 44 | done 45 | 46 | # 先存储本地修改,避免冲突 47 | git stash >/dev/null 2>&1 48 | if [ $? -ne 0 ]; then 49 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 50 | echo "🚫 更新失败!" 51 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 52 | exit 1 53 | fi 54 | 55 | # 拉取最新代码 56 | git reset --hard origin/"$BRANCH" >/dev/null 2>&1 57 | 58 | # 遍历变更的文件并复制到目标路径 59 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 60 | echo "🔄 更新内容:" 61 | for file in $CHANGED_FILES; do 62 | RELATIVE_PATH=${file#server/} # 去掉 "server/" 前缀 63 | TARGET_FILE="$TARGET_PATH/$RELATIVE_PATH" # 保持相对路径一致 64 | 65 | rm -f "$SERVER_PATH/ota.sh" "$REPO_PATH/README.md"; 66 | 67 | # 如果是文件删除(在仓库中删除),则删除目标路径的文件 68 | if ! git ls-files --error-unmatch "$file" >/dev/null 2>&1; then 69 | if [ -f "$TARGET_FILE" ]; then 70 | rm -f "$TARGET_FILE" 71 | echo "🗑️ 清理无效文件:$(basename "$TARGET_FILE")" 72 | fi 73 | else 74 | # 复制文件 75 | cp -f "$SERVER_PATH/$RELATIVE_PATH" "$TARGET_FILE" 76 | echo "✅ $(basename "$TARGET_FILE")" 77 | fi 78 | done 79 | 80 | devil www restart "$USER_NAME.serv00.net" >/dev/null 2>&1 81 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 82 | echo "🎉 更 新 完 成!" 83 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 84 | -------------------------------------------------------------------------------- /server/protected/accounts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 账号管理 7 | 124 | 125 | 126 |
127 |

账号管理

128 | 129 |
130 | 131 | 132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 144 | 145 | 146 | 147 | 148 |
#账号 140 | 143 | 操作
149 |
150 | 151 | 152 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /server/protected/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 主页 7 | 93 | 94 | 95 | 96 | 97 | 98 |
99 |

Serv00 账 号 管 理

100 |

本机账号:加载中...

101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /server/protected/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 登录 7 | 86 | 87 | 88 | 89 |
90 |

登录

91 |
92 | 93 | 94 | 95 |

96 |
97 |
98 | 99 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /server/protected/logs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 日志管理 7 | 98 | 99 | 100 | 101 |
102 |

日志管理

103 |
104 | 105 | 106 |
107 |
加载中...
108 |
Logs will appear here...
109 | 110 |
111 | 112 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /server/protected/nodes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 科学订阅 7 | 132 | 133 | 134 |
135 |

科学订阅

136 |

订阅地址:

137 |
138 | 139 | 140 |
141 | 142 |
143 | 146 | 147 |
148 | 149 |
150 |
151 | 说明:
152 | 1. 点击“节点采集”获取数据,自动生成订阅。
153 | 2. 如节点配置有修改更新,需要进行一次节点采集行为。
154 | 3. 订阅地址支持 Sing-box、Loon、V2ray、小火箭等客户端直接订阅。 155 |
156 |
157 | 158 |
暂无失败账号
159 |
160 | 161 | 162 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /server/protected/ota.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 保活端 OTA 更新 7 | 163 | 164 | 165 |
166 |

账号端 OTA 更新

167 | 168 | 169 |
170 | 同步更新保活端 171 | 175 |
176 |

(打开开关后,请耐心等待完成)

177 | 178 | 179 | 180 | 181 | 182 |
183 |
184 | 185 | 221 | 222 | -------------------------------------------------------------------------------- /server/protected/set_password.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 设置密码 7 | 87 | 88 | 89 | 90 |
91 |

设置密码

92 |
93 | 94 | 95 | 96 |

97 |
98 |
99 | 100 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /server/public/check_accounts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 账号检测 7 | 106 | 107 | 108 |
109 |

账号检测

110 | 111 |
112 |
113 | 114 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /server/public/notification_settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 通知 & Telegram 设置 7 | 135 | 136 | 137 |
138 |

通知 & Telegram 设置

139 | 140 |
141 |
142 | 143 | 147 |
148 | 149 | 150 | 151 | 152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 |
162 |
163 | 164 | 218 | 219 | -------------------------------------------------------------------------------- /single/app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require("express"); 3 | const { exec } = require("child_process"); 4 | const util = require('util'); 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | const app = express(); 8 | 9 | const username = process.env.USER.toLowerCase(); 10 | const DOMAIN_DIR = path.join(process.env.HOME, "domains", `${username}.serv00.net`, "public_nodejs"); 11 | const scriptPath = path.join(process.env.HOME, "serv00-play", "singbox", "start.sh"); 12 | const configFilePath = path.join(__dirname, 'config.json'); 13 | const SINGBOX_CONFIG_PATH = path.join(process.env.HOME, "serv00-play", "singbox", "singbox.json"); 14 | const CONFIG_PATH = path.join(process.env.HOME, "serv00-play", "singbox", "config.json"); 15 | 16 | app.use(express.static(path.join(__dirname, 'public'))); 17 | app.use(express.json()); 18 | 19 | let logs = []; 20 | function logMessage(message) { 21 | logs.push(message); 22 | if (logs.length > 10) logs.shift(); 23 | } 24 | 25 | function executeCommand(command, actionName, isStartLog = false) { 26 | return new Promise((resolve, reject) => { 27 | exec(command, (err, stdout, stderr) => { 28 | const timestamp = new Date().toLocaleString(); 29 | if (err) { 30 | logMessage(`${actionName} 执行失败: ${err.message}`); 31 | reject(err.message); 32 | return; 33 | } 34 | if (stderr) { 35 | logMessage(`${actionName} 执行标准错误输出: ${stderr}`); 36 | } 37 | const successMsg = `${actionName} 执行成功:\n${stdout}`; 38 | logMessage(successMsg); 39 | if (isStartLog) latestStartLog = successMsg; 40 | resolve(stdout); 41 | }); 42 | }); 43 | } 44 | 45 | async function runShellCommand() { 46 | console.log("start 被调用"); 47 | const command = `cd ${process.env.HOME}/serv00-play/singbox/ && bash start.sh`; 48 | try { 49 | await executeCommand(command, "start.sh"); 50 | } catch (err) { 51 | console.error("start 失败:", err); 52 | } 53 | } 54 | 55 | async function stopShellCommand() { 56 | console.log("stop 被调用"); 57 | const command = `cd ${process.env.HOME}/serv00-play/singbox/ && bash killsing-box.sh`; 58 | try { 59 | await executeCommand(command, "killsing-box.sh"); 60 | } catch (err) { 61 | console.error("stop 失败:", err); 62 | } 63 | } 64 | 65 | async function KeepAlive() { 66 | console.log("KeepAlive 被调用"); 67 | const command = `cd ${process.env.HOME}/serv00-play/ && bash keepalive.sh`; 68 | try { 69 | await executeCommand(command, "keepalive.sh"); 70 | } catch (err) { 71 | console.error("KeepAlive 失败:", err); 72 | } 73 | } 74 | 75 | // setInterval(KeepAlive, 1800000); 76 | 77 | app.get("/info", async (req, res) => { 78 | const htmlContent = ` 79 | 80 | 81 | 82 | 83 | 系统状态 84 | 192 | 193 | 194 |
195 |
196 |
197 |
198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
206 |
207 | 208 | 223 | 224 | `; 225 | 226 | res.send(htmlContent); 227 | 228 | // 后端异步执行任务 229 | try { 230 | // await KeepAlive(); // 运行 KeepAlive 231 | // await runShellCommand(); // 启动 SingBox 232 | } catch (err) { 233 | console.error("后台任务执行失败:", err); 234 | } 235 | }); 236 | 237 | app.use(express.urlencoded({ extended: true })); 238 | function executeHy2ipScript(logMessages, callback) { 239 | const downloadCommand = "curl -Ls https://raw.githubusercontent.com/ryty1/serv00-save-me/refs/heads/main/single/hy2ip.sh -o /tmp/hy2ip.sh"; 240 | exec(downloadCommand, (error, stdout, stderr) => { 241 | if (error) { 242 | return callback(error, "", stderr); 243 | } 244 | const executeCommand = "bash /tmp/hy2ip.sh"; 245 | exec(executeCommand, (error, stdout, stderr) => { 246 | exec("rm -f /tmp/hy2ip.sh", (err) => { 247 | if (err) { 248 | console.error(`❌ 删除临时文件失败: ${err.message}`); 249 | } else { 250 | console.log("✅ 临时文件已删除"); 251 | } 252 | }); 253 | 254 | callback(error, stdout, stderr); 255 | }); 256 | }); 257 | } 258 | 259 | app.get("/hy2ip", (req, res) => { 260 | res.sendFile(path.join(__dirname, "public", "hy2ip.html")); 261 | }); 262 | 263 | app.post("/hy2ip/execute", (req, res) => { 264 | const confirmation = req.body.confirmation?.trim(); 265 | 266 | if (confirmation !== "更新") { 267 | return res.json({ success: false, errorMessage: "输入错误!请返回并输入“更新”以确认。" }); 268 | } 269 | 270 | try { 271 | let logMessages = []; 272 | 273 | executeHy2ipScript(logMessages, (error, stdout, stderr) => { 274 | let updatedIp = ""; 275 | 276 | if (stdout) { 277 | let outputMessages = stdout.split("\n"); 278 | outputMessages.forEach(line => { 279 | if (line.includes("SingBox 配置文件成功更新IP为")) { 280 | updatedIp = line.split("SingBox 配置文件成功更新IP为")[1].trim(); 281 | } 282 | if (line.includes("Config 配置文件成功更新IP为")) { 283 | updatedIp = line.split("Config 配置文件成功更新IP为")[1].trim(); 284 | } 285 | }); 286 | updatedIp = updatedIp.replace(/\x1B\[[0-9;]*m/g, ""); 287 | 288 | if (updatedIp && updatedIp !== "未找到可用的 IP!") { 289 | logMessages.push("命令执行成功"); 290 | logMessages.push(`SingBox 配置文件成功更新IP为 ${updatedIp}`); 291 | logMessages.push(`Config 配置文件成功更新IP为 ${updatedIp}`); 292 | logMessages.push("sing-box 已重启"); 293 | res.json({ success: true, ip: updatedIp, logs: logMessages }); 294 | } else { 295 | logMessages.push("命令执行成功"); 296 | logMessages.push("没有找到有效 IP"); 297 | res.json({ success: false, errorMessage: "没有找到有效的 IP", logs: logMessages }); 298 | } 299 | } 300 | }); 301 | } catch (error) { 302 | let logMessages = ["命令执行成功", "没有找到有效 IP"]; 303 | res.json({ success: false, errorMessage: "命令执行失败", logs: logMessages }); 304 | } 305 | }); 306 | 307 | app.get("/api/log", (req, res) => { 308 | const command = "ps aux"; 309 | 310 | exec(command, (err, stdout, stderr) => { 311 | if (err) { 312 | return res.json({ 313 | error: true, 314 | message: `执行错误: ${err.message}`, 315 | logs: logs.length ? logs.slice(-2).map(log => `📔 ${log}`).join("\n") : ["暂无日志"], // 只返回最近 2 条日志 316 | processOutput: "" 317 | }); 318 | } 319 | 320 | res.json({ 321 | error: false, 322 | message: "成功获取数据", 323 | logs: logs.length ? logs.slice(-2).map(log => `📔 ${log}`).join("\n") : ["暂无日志"], // 只返回最近 2 条日志 324 | processOutput: stdout.trim() 325 | }); 326 | }); 327 | }); 328 | 329 | app.get("/log", (req, res) => { 330 | res.sendFile(path.join(__dirname, "public", "log.html")); 331 | }); 332 | 333 | app.get("/node", (req, res) => { 334 | const filePath = path.join(process.env.HOME, "serv00-play/singbox/list"); 335 | fs.readFile(filePath, "utf8", (err, data) => { 336 | if (err) { 337 | res.type("html").send(`
无法读取文件: ${err.message}
`); 338 | return; 339 | } 340 | 341 | const cleanedData = data 342 | .replace(/(vmess:\/\/|hysteria2:\/\/|proxyip:\/\/|https:\/\/)/g, '\n$1') 343 | .trim(); 344 | 345 | const vmessPattern = /vmess:\/\/[^\n]+/g; 346 | const hysteriaPattern = /hysteria2:\/\/[^\n]+/g; 347 | const httpsPattern = /https:\/\/[^\n]+/g; 348 | const proxyipPattern = /proxyip:\/\/[^\n]+/g; 349 | const vmessConfigs = cleanedData.match(vmessPattern) || []; 350 | const hysteriaConfigs = cleanedData.match(hysteriaPattern) || []; 351 | const httpsConfigs = cleanedData.match(httpsPattern) || []; 352 | const proxyipConfigs = cleanedData.match(proxyipPattern) || []; 353 | const allConfigs = [...vmessConfigs, ...hysteriaConfigs, ...httpsConfigs, ...proxyipConfigs]; 354 | 355 | let htmlContent = ` 356 | 357 | 358 | 359 | 节点信息 360 | 436 | 437 | 438 |
439 |

节点信息

440 |
441 | `; 442 | 443 | allConfigs.forEach((config) => { 444 | htmlContent += `
${config.trim()}
`; // 去掉首尾空格 445 | }); 446 | 447 | htmlContent += ` 448 |
449 | 450 |
451 | 452 | 466 | 467 | 468 | `; 469 | res.type("html").send(htmlContent); 470 | }); 471 | }); 472 | 473 | function getConfigFile() { 474 | console.log('检查配置文件是否存在:', configFilePath); 475 | 476 | try { 477 | if (fs.existsSync(configFilePath)) { 478 | console.log('配置文件已存在,读取文件内容...'); 479 | return JSON.parse(fs.readFileSync(configFilePath, 'utf8')); 480 | } else { 481 | console.log('配置文件不存在,创建默认配置并写入...'); 482 | const defaultConfig = { 483 | vmessname: "Argo-vmess", 484 | hy2name: "Hy2", 485 | HIDE_USERNAME: false 486 | }; 487 | fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig)); 488 | console.log('配置文件已创建:', configFilePath); 489 | 490 | writeDefaultConfigToScript(defaultConfig); 491 | return defaultConfig; 492 | } 493 | } catch (error) { 494 | console.error('读取配置文件时出错:', error); 495 | return null; 496 | } 497 | } 498 | 499 | function writeDefaultConfigToScript(config) { 500 | console.log('写入默认配置到脚本:', scriptPath); 501 | let scriptContent; 502 | 503 | try { 504 | scriptContent = fs.readFileSync(scriptPath, 'utf8'); 505 | } catch (error) { 506 | console.error('读取脚本文件时出错:', error); 507 | return; 508 | } 509 | 510 | const exportListFuncPattern = /export_list\(\)\s*{\n([\s\S]*?)}/m; 511 | const match = scriptContent.match(exportListFuncPattern); 512 | 513 | if (match) { 514 | let exportListContent = match[1]; 515 | 516 | if (!exportListContent.includes('custom_vmess')) { 517 | exportListContent = ` custom_vmess="${config.vmessname}"\n` + exportListContent; 518 | } 519 | if (!exportListContent.includes('custom_hy2')) { 520 | exportListContent = ` custom_hy2="${config.hy2name}"\n` + exportListContent; 521 | } 522 | 523 | scriptContent = scriptContent.replace(exportListFuncPattern, `export_list() {\n${exportListContent}}`); 524 | } else { 525 | console.log("没有找到 export_list() 函数,无法插入变量定义。"); 526 | } 527 | 528 | scriptContent = scriptContent.replaceAll(/vmessname=".*?"/g, `vmessname="\$custom_vmess-\$host-\$user"`); 529 | scriptContent = scriptContent.replaceAll(/hy2name=".*?"/g, `hy2name="\$custom_hy2-\$host-\$user"`); 530 | 531 | if (config.HIDE_USERNAME) { 532 | scriptContent = scriptContent.replaceAll(/user=".*?"/g, `user="\$(whoami | tail -c 2 | head -c 1)"`); 533 | } else { 534 | scriptContent = scriptContent.replaceAll(/user=".*?"/g, `user="\$(whoami)"`); 535 | } 536 | 537 | scriptContent = scriptContent.replace(/\n{2,}/g, '\n').trim(); 538 | 539 | try { 540 | fs.writeFileSync(scriptPath, scriptContent); 541 | console.log('脚本已更新:', scriptPath); 542 | } catch (error) { 543 | console.error('写入脚本文件时出错:', error); 544 | } 545 | } 546 | 547 | async function updateConfigFile(config) { 548 | console.log('更新配置文件:', configFilePath); 549 | try { 550 | fs.writeFileSync(configFilePath, JSON.stringify(config)); 551 | console.log('配置文件更新成功'); 552 | } catch (error) { 553 | console.error('更新配置文件时出错:', error); 554 | return; 555 | } 556 | 557 | console.log('更新脚本内容:', scriptPath); 558 | let scriptContent; 559 | 560 | try { 561 | scriptContent = fs.readFileSync(scriptPath, 'utf8'); 562 | } catch (error) { 563 | console.error('读取脚本文件时出错:', error); 564 | return; 565 | } 566 | 567 | scriptContent = scriptContent.replaceAll(/custom_vmess=".*?"/g, `custom_vmess="${config.vmessname}"`); 568 | scriptContent = scriptContent.replaceAll(/custom_hy2=".*?"/g, `custom_hy2="${config.hy2name}"`); 569 | scriptContent = scriptContent.replaceAll(/vmessname=".*?"/g, `vmessname="\$custom_vmess-\$host-\$user"`); 570 | scriptContent = scriptContent.replaceAll(/hy2name=".*?"/g, `hy2name="\$custom_hy2-\$host-\$user"`); 571 | 572 | if (config.HIDE_USERNAME) { 573 | scriptContent = scriptContent.replaceAll(/user=".*?"/g, `user="\$(whoami | tail -c 2 | head -c 1)"`); 574 | } else { 575 | scriptContent = scriptContent.replaceAll(/user=".*?"/g, `user="\$(whoami)"`); 576 | } 577 | 578 | scriptContent = scriptContent.replace(/\n{2,}/g, '\n').trim(); 579 | 580 | try { 581 | fs.writeFileSync(scriptPath, scriptContent); 582 | console.log('脚本更新成功:', scriptPath); 583 | } catch (error) { 584 | console.error('写入脚本文件时出错:', error); 585 | return; 586 | } 587 | stopShellCommand(); 588 | setTimeout(() => { 589 | runShellCommand(); 590 | }, 3000); 591 | } 592 | 593 | app.get('/api/get-config', (req, res) => { 594 | const config = getConfigFile(); 595 | res.json(config); 596 | }); 597 | 598 | app.post('/api/update-config', (req, res) => { 599 | const { vmessname, hy2name, HIDE_USERNAME } = req.body; 600 | const newConfig = { vmessname, hy2name, HIDE_USERNAME }; 601 | 602 | updateConfigFile(newConfig); 603 | 604 | res.json({ success: true }); 605 | }); 606 | 607 | app.get('/newset', (req, res) => { 608 | res.sendFile(path.join(__dirname, "public", 'newset.html')); 609 | }); 610 | 611 | app.get('/getConfig', (req, res) => { 612 | fs.readFile(SINGBOX_CONFIG_PATH, 'utf8', (err, data) => { 613 | if (err) { 614 | return res.status(500).json({ error: '读取配置文件失败' }); 615 | } 616 | 617 | try { 618 | const config = JSON.parse(data); 619 | res.json({ 620 | GOOD_DOMAIN: config.GOOD_DOMAIN, 621 | ARGO_AUTH: config.ARGO_AUTH, 622 | ARGO_DOMAIN: config.ARGO_DOMAIN 623 | }); 624 | } catch (parseError) { 625 | return res.status(500).json({ error: '解析 JSON 失败' }); 626 | } 627 | }); 628 | }); 629 | 630 | app.post('/updateConfig', async (req, res) => { 631 | const { GOOD_DOMAIN, ARGO_AUTH, ARGO_DOMAIN } = req.body; 632 | 633 | if (!GOOD_DOMAIN && !ARGO_AUTH && !ARGO_DOMAIN) { 634 | return res.status(400).json({ success: false, error: '请至少填写一个字段' }); 635 | } 636 | 637 | try { 638 | const data = fs.readFileSync(SINGBOX_CONFIG_PATH, 'utf8'); 639 | const config = JSON.parse(data); 640 | 641 | if (GOOD_DOMAIN) config.GOOD_DOMAIN = GOOD_DOMAIN; 642 | if (ARGO_AUTH) config.ARGO_AUTH = ARGO_AUTH; 643 | if (ARGO_DOMAIN) config.ARGO_DOMAIN = ARGO_DOMAIN; 644 | 645 | fs.writeFileSync(SINGBOX_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8'); 646 | console.log('配置已更新'); 647 | 648 | stopShellCommand(); 649 | setTimeout(() => { 650 | runShellCommand(); 651 | }, 3000); 652 | 653 | res.json({ success: true, message: '配置更新成功并重启singbox' }); 654 | 655 | } catch (err) { 656 | console.error('更新失败:', err); 657 | res.status(500).json({ success: false, error: '更新失败,请稍后再试' }); 658 | } 659 | }); 660 | 661 | app.get("/config", (req, res) => { 662 | res.sendFile(path.join(__dirname, "public", "config.html")); 663 | }); 664 | 665 | function readConfig() { 666 | try { 667 | return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); 668 | } catch (err) { 669 | console.error("读取配置文件失败:", err); 670 | return null; 671 | } 672 | } 673 | 674 | function writeConfig(config) { 675 | try { 676 | fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8"); 677 | console.log("配置文件更新成功!"); 678 | stopShellCommand(); 679 | setTimeout(() => { 680 | runShellCommand(); 681 | }, 3000); 682 | } catch (err) { 683 | console.error("写入配置文件失败:", err); 684 | } 685 | } 686 | 687 | app.get("/getOutboundStatus", (req, res) => { 688 | let config = readConfig(); 689 | if (!config) return res.status(500).json({ error: "读取配置失败" }); 690 | 691 | let status = "未出站"; 692 | if (config.outbounds.some(outbound => outbound.type === "wireguard")) { 693 | status = "已配置 WireGuard"; 694 | } else if (config.outbounds.some(outbound => outbound.type === "socks")) { 695 | status = "已配置 Socks"; 696 | } 697 | 698 | res.json({ status }); 699 | }); 700 | 701 | app.post("/setWireGuard", (req, res) => { 702 | let config = readConfig(); 703 | if (!config) return res.status(500).json({ error: "读取配置失败" }); 704 | 705 | config.outbounds = config.outbounds.filter(outbound => outbound.type !== "socks"); 706 | 707 | config.outbounds.unshift({ 708 | "type": "wireguard", 709 | "tag": "wireguard-out", 710 | "server": "162.159.195.100", 711 | "server_port": 4500, 712 | "local_address": [ 713 | "172.16.0.2/32", 714 | "2606:4700:110:83c7:b31f:5858:b3a8:c6b1/128" 715 | ], 716 | "private_key": "mPZo+V9qlrMGCZ7+E6z2NI6NOV34PD++TpAR09PtCWI=", 717 | "peer_public_key": "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo=", 718 | "reserved": [26, 21, 228] 719 | }); 720 | 721 | if (config.route && config.route.rules.length > 0) { 722 | config.route.rules[0].outbound = "wireguard-out"; 723 | } 724 | 725 | writeConfig(config); 726 | res.json({ message: "WireGuard 出站已设置" }); 727 | }); 728 | 729 | app.post("/setSocks", (req, res) => { 730 | const { server, server_port, username, password } = req.body; 731 | if (!server || !server_port || !username || !password) { 732 | return res.status(400).json({ error: "参数不完整" }); 733 | } 734 | 735 | let config = readConfig(); 736 | if (!config) return res.status(500).json({ error: "读取配置失败" }); 737 | 738 | config.outbounds = config.outbounds.filter(outbound => outbound.type !== "wireguard"); 739 | 740 | config.outbounds.unshift({ 741 | "type": "socks", 742 | "tag": "socks5_outbound", 743 | "server": server, 744 | "server_port": parseInt(server_port), 745 | "version": "5", 746 | "username": username, 747 | "password": password 748 | }); 749 | 750 | if (config.route && config.route.rules.length > 0) { 751 | config.route.rules[0].outbound = "socks5_outbound"; 752 | } 753 | 754 | writeConfig(config); 755 | res.json({ message: "Socks 出站已设置" }); 756 | }); 757 | 758 | app.post("/disableOutbound", (req, res) => { 759 | let config = readConfig(); 760 | if (!config) return res.status(500).json({ error: "读取配置失败" }); 761 | 762 | config.outbounds = config.outbounds.filter(outbound => 763 | outbound.type !== "wireguard" && outbound.type !== "socks" 764 | ); 765 | 766 | if (config.route && config.route.rules.length > 0) { 767 | config.route.rules[0].outbound = "direct"; 768 | } 769 | 770 | writeConfig(config); 771 | res.json({ message: "已关闭出站" }); 772 | }); 773 | 774 | app.get("/outbounds", (req, res) => { 775 | res.sendFile(path.join(__dirname, "public", "outbounds.html")); 776 | }); 777 | 778 | app.get("/api/posts", (req, res) => { 779 | function getRandomPost() { 780 | const titles = [ 781 | "Something interesting happened today", 782 | "I have a JavaScript question", 783 | "How to quickly improve writing skills?", 784 | "Sharing my recent trip", 785 | "Has anyone used ChatGPT to write code?", 786 | "3-month fitness results, sharing insights", 787 | "Can anyone recommend a good book?", 788 | "What side jobs are people doing?", 789 | "Will AI replace humans in the future?", 790 | "Have you ever encountered a pitfall in investing?", 791 | "The importance of networking", 792 | "My thoughts on the latest tech trends", 793 | "How to stay productive while working from home", 794 | "Building a startup from scratch", 795 | "What are your goals for this year?", 796 | "How to develop a growth mindset", 797 | "Exploring the concept of work-life balance", 798 | "Has anyone tried learning a new language recently?", 799 | "How to manage stress effectively", 800 | "A step-by-step guide to personal finance" 801 | ]; 802 | 803 | const contents = [ 804 | "I heard a stranger on the subway talking about his entrepreneurial experience, it was really inspiring, I feel like I should do something too.", 805 | "I've been learning JavaScript recently and encountered a strange bug. The console doesn't show any errors, but the function just doesn't work. Has anyone encountered this?", 806 | "If I write 500 words every day, will it improve my writing skills? Has anyone tried it?", 807 | "I went to Yunnan last month, and experienced the sunrise at Lugu Lake for the first time. It was truly amazing, I highly recommend visiting if you get the chance.", 808 | "I've been using ChatGPT to help write Python code, and sometimes the solutions it gives are even simpler than mine. It's amazing.", 809 | "I've been working out for 3 months and have lost 10kg, from 80kg to 70kg. The process was tough, but I'm happy with the results. Here's my training plan.", 810 | "I'm reading 'The Three-Body Problem' recently, and Liu Cixin's imagination is incredible. Does anyone have book recommendations with a similar style?", 811 | "Has anyone tried doing a side job recently? I’m doing the no-inventory business on Xianyu, and it’s surprisingly profitable. Anyone interested?", 812 | "AI development is speeding up. Will it really affect our jobs in the future? What do you think?", 813 | "I got scammed recently. I bought a fund, and it dropped 10% in 3 days. Investment really shouldn’t be done blindly.", 814 | "I recently joined a networking event, and I must say, it was a game-changer. Meeting new people with similar interests is so valuable.", 815 | "I’ve been diving deep into the tech world lately and just wanted to share my thoughts on the latest trends like AI and blockchain. It’s a thrilling time!", 816 | "I’ve been working remotely for a while now, and here are some of my tips for staying productive when you're at home all day.", 817 | "Started working on a startup idea, and I’m learning a lot. Here's how I went from concept to execution. Any tips or advice for beginners?", 818 | "This year, I’m focused on improving my personal growth. What are your top goals for 2025? Let’s share and motivate each other!", 819 | "I’ve been reading a lot about the importance of having a growth mindset. How do you foster this kind of mindset in your life?", 820 | "Lately, I’ve been thinking about how to better manage work-life balance. It’s not easy, but I believe small changes can make a huge difference.", 821 | "Has anyone tried learning a new language lately? I just started learning Spanish. It’s tough but exciting!", 822 | "Dealing with stress is something I’ve been focusing on recently. What are some effective strategies you use to manage stress in daily life?", 823 | "I just put together a personal finance plan for the year. It’s a great way to get on track financially. Anyone else have a finance strategy they follow?" 824 | ]; 825 | 826 | const authors = [ 827 | "ryty1", "Watermelon", "Chef", "iorjhg", "Fan Qijun", "uehsgwg", "Zhou Jiu", "Wu Shi", "Zheng Shiyi", "He Chenguang", 828 | "Lily", "Jack", "Tom", "Maggie", "Sophie", "Luke", "Eva", "James", "Ella", "Daniel", "Sophia" 829 | ]; 830 | 831 | function getRandomTime() { 832 | const timeOptions = [ 833 | "5 minutes ago", "20 minutes ago", "1 hour ago", "3 hours ago", "yesterday", "2 days ago", "1 week ago" 834 | ]; 835 | return timeOptions[Math.floor(Math.random() * timeOptions.length)]; 836 | } 837 | 838 | function getRandomInteraction() { 839 | return `👍 ${Math.floor(Math.random() * 100)} 💬 ${Math.floor(Math.random() * 50)}`; 840 | } 841 | 842 | return { 843 | title: titles[Math.floor(Math.random() * titles.length)], 844 | content: contents[Math.floor(Math.random() * contents.length)], 845 | author: authors[Math.floor(Math.random() * authors.length)], 846 | date: getRandomTime(), 847 | interaction: getRandomInteraction() 848 | }; 849 | } 850 | 851 | const posts = Array.from({ length: 10 }, getRandomPost); 852 | res.json(posts); 853 | }); 854 | 855 | app.get("/", (req, res) => { 856 | res.sendFile(path.join(__dirname, "public", "index.html")); 857 | }); 858 | 859 | app.get('/ota/update', (req, res) => { 860 | console.log("🚀 开始 OTA 更新..."); 861 | 862 | const downloadScriptCommand = 'curl -Ls -o /tmp/ota.sh https://raw.githubusercontent.com/ryty1/serv00-save-me/refs/heads/main/single/ota.sh'; 863 | 864 | exec(downloadScriptCommand, (error, stdout, stderr) => { 865 | if (error) { 866 | console.error(`❌ 下载失败: ${error.message}`); 867 | return res.status(500).json({ success: false, message: `下载失败: ${error.message}` }); 868 | } 869 | 870 | console.log("✅ 下载完成"); 871 | const executeScriptCommand = 'bash /tmp/ota.sh'; 872 | 873 | exec(executeScriptCommand, (error, stdout, stderr) => { 874 | exec('rm -f /tmp/ota.sh', () => console.log('✅ 清理完成')); 875 | 876 | if (error) { 877 | console.error(`❌ 执行失败: ${error.message}`); 878 | return res.status(500).json({ success: false, message: `执行失败: ${error.message}` }); 879 | } 880 | 881 | console.log("✅ 脚本执行完成"); 882 | res.json({ success: true, output: stdout || '执行成功' }); 883 | }); 884 | }); 885 | }); 886 | 887 | app.get('/ota', (req, res) => { 888 | res.sendFile(path.join(__dirname, "public", "ota.html")); 889 | }); 890 | 891 | app.use((req, res, next) => { 892 | const validPaths = ["/", "/info", "/hy2ip", "/node", "/log", "/newset", "/config", "/outbounds"]; 893 | if (validPaths.includes(req.path)) { 894 | return next(); 895 | } 896 | res.status(404).send("页面未找到"); 897 | }); 898 | app.listen(3000, () => { 899 | const timestamp = new Date().toLocaleString(); 900 | const startMsg = `${timestamp} 服务器已启动,监听端口 3000`; 901 | logMessage(startMsg); 902 | console.log(startMsg); 903 | }); 904 | -------------------------------------------------------------------------------- /single/hy2ip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 字体上色 3 | A() { 4 | echo -e "\033[32m$1\033[0m" 5 | } 6 | B() { 7 | echo -e "\033[31m$1\033[0m" 8 | } 9 | C() { 10 | local D=$(hostname) 11 | local E=$(echo "$D" | awk -F'[s.]' '{print $2}') 12 | local F=("cache${E}.serv00.com" "web${E}.serv00.com" "$D") 13 | for G in "${F[@]}"; do 14 | local H=$(curl -s --max-time 10 "https://ss.fkj.pp.ua/api/getip?host=$G") 15 | if [[ "$H" =~ "not found" ]]; then 16 | echo "未识别主机 ${G}!" 17 | continue 18 | fi 19 | local I=$(echo "$H" | awk -F "|" '{print $1}') 20 | local J=$(echo "$H" | awk -F "|" '{print $2}') 21 | if [[ "$J" == "Accessible" ]]; then 22 | echo "$I" 23 | return 0 24 | fi 25 | done 26 | echo "" 27 | return 1 28 | } 29 | K() { 30 | local L="$1" 31 | local M="$2" 32 | if [[ ! -f "$L" ]]; then 33 | B "配置文件 $L 不存在!" 34 | return 1 35 | fi 36 | jq --arg N "$M" ' 37 | (.inbounds[] | select(.tag == "hysteria-in") | .listen) = $N 38 | ' "$L" > temp.json && mv temp.json "$L" 39 | 40 | if [[ $? -eq 0 ]]; then 41 | A "SingBox 配置文件成功更新IP为 $M" 42 | else 43 | B "更新配置文件失败!" 44 | return 1 45 | fi 46 | } 47 | O() { 48 | local P="$1" 49 | local Q="$2" 50 | if [[ ! -f "$P" ]]; then 51 | B "配置文件 $P 不存在!" 52 | return 1 53 | fi 54 | jq --arg R "$Q" ' 55 | .HY2IP = $R 56 | ' "$P" > temp.json && mv temp.json "$P" 57 | 58 | if [[ $? -eq 0 ]]; then 59 | A "Config 配置文件成功更新IP为 $Q" 60 | else 61 | B "更新配置文件失败!" 62 | return 1 63 | fi 64 | } 65 | S() { 66 | local T="$HOME/serv00-play/singbox/config.json" 67 | local U="$HOME/serv00-play/singbox/singbox.json" 68 | local V=$(C) 69 | echo "有效 IP: $V" 70 | if [[ -z "$V" ]]; then 71 | B "没有可用 IP!" 72 | return 1 73 | fi 74 | K "$T" "$V" 75 | O "$U" "$V" 76 | echo "正在重启 sing-box..." 77 | W 78 | sleep 5 79 | X 80 | } 81 | W() { 82 | cd ~/serv00-play/singbox/ && bash killsing-box.sh 83 | } 84 | X() { 85 | cd ~/serv00-play/singbox/ && bash start.sh 86 | } 87 | S 88 | -------------------------------------------------------------------------------- /single/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | X() { 4 | local Y=$1 5 | local CMD=$2 6 | local O=("▖" "▘" "▝" "▗") 7 | local i=0 8 | 9 | printf "[ ] %s" "$Y" 10 | 11 | eval "$CMD" > /dev/null 2>&1 & 12 | local PID=$! 13 | 14 | while kill -0 "$PID" 2>/dev/null; do 15 | printf "\r[%s] %s" "${O[i]}" "$Y" 16 | i=$(( (i + 1) % 4 )) 17 | sleep 0.1 18 | done 19 | 20 | wait "$PID" 21 | local EXIT_CODE=$? 22 | 23 | printf "\r \r" 24 | if [[ $EXIT_CODE -eq 0 ]]; then 25 | printf "[\033[0;32mOK\033[0m] %s\n" "$Y" 26 | else 27 | printf "[\033[0;31mNO\033[0m] %s\n" "$Y" 28 | fi 29 | } 30 | 31 | U=$(whoami) 32 | V=$(echo "$U" | tr '[:upper:]' '[:lower:]') 33 | W="$V.serv00.net" 34 | A1="/home/$U/domains/$W" 35 | A2="$A1/public_nodejs" 36 | B1="$A2/public" 37 | A3="https://github.com/ryty1/serv00-save-me.git" 38 | 39 | echo "请选择保活类型:" 40 | echo "1. 本机保活" 41 | echo "2. 账号服务" 42 | echo "3. 一键卸载" 43 | read -p "请输入选择: " choice 44 | 45 | if [[ "$choice" -eq 3 ]]; then 46 | # **一键卸载** 47 | if [[ -d "$HOME/serv00-save-me" ]]; then 48 | X "删除 类型域名" "cd && devil www del \"$W\"" 49 | 50 | if [[ -d "$A1" ]]; then 51 | rm -rf "$A1" 52 | fi 53 | 54 | X "恢复 默认域名" "devil www add \"$W\" php" 55 | 56 | if [[ -d "$B1" ]]; then 57 | rm -rf "$B1" 58 | fi 59 | 60 | X "脚本 卸载完成" "rm -rf "$HOME/serv00-save-me"" 61 | else 62 | echo "🚫 未安装,无需卸载" 63 | fi 64 | exit 0 65 | 66 | fi 67 | 68 | # **安装逻辑** 69 | if [[ "$choice" -eq 1 ]]; then 70 | TARGET_FOLDER="single" 71 | DELETE_FOLDER="server" 72 | DEPENDENCIES="dotenv basic-auth express" 73 | echo "开始进行 本机保活配置" 74 | elif [[ "$choice" -eq 2 ]]; then 75 | TZ_MODIFIED=0 76 | if [[ "$(date +%Z)" != "CST" ]]; then 77 | export TZ='Asia/Shanghai' 78 | echo "export TZ='Asia/Shanghai'" >> ~/.profile 79 | source ~/.profile 80 | TZ_MODIFIED=1 81 | fi 82 | 83 | TARGET_FOLDER="server" 84 | DELETE_FOLDER="single" 85 | DEPENDENCIES="body-parser express-session session-file-store dotenv express socket.io node-cron node-telegram-bot-api axios" 86 | echo "开始进行 账号服务配置" 87 | else 88 | echo "无效选择,退出脚本" 89 | exit 1 90 | fi 91 | 92 | echo " ———————————————————————————————————————————————————————————— " 93 | 94 | X "删除 默认域名" "cd && devil www del \"$W\"" 95 | 96 | if [[ -d "$A1" ]]; then 97 | rm -rf "$A1" 98 | fi 99 | 100 | X "创建 类型域名" "devil www add \"$W\" nodejs /usr/local/bin/node22" 101 | 102 | if [[ -d "$B1" ]]; then 103 | rm -rf "$B1" 104 | fi 105 | 106 | cd "$A2" && npm init -y > /dev/null 2>&1 107 | X "安装 环境依赖" "npm install $DEPENDENCIES" 108 | 109 | # 使用 sparse-checkout 来只拉取指定文件夹 110 | cd && git clone --no-checkout "$A3" "$HOME/serv00-save-me" > /dev/null 2>&1 111 | cd "$HOME/serv00-save-me" || exit 1 112 | 113 | # 配置 sparse-checkout,拉取指定文件夹 114 | git sparse-checkout init --cone 115 | git sparse-checkout set "$TARGET_FOLDER" # 只拉取 single 或 server 文件夹 116 | 117 | # 拉取完成后,删除仓库的临时文件夹 118 | git checkout main > /dev/null 2>&1 119 | cd "$HOME" || exit 1 120 | 121 | # 复制拉取到的文件到目标目录并保留结构 122 | if [[ -d "$HOME/serv00-save-me" ]]; then 123 | cp -r "$HOME/serv00-save-me/$TARGET_FOLDER/." "$A2/" 124 | else 125 | exit 1 126 | fi 127 | 128 | # 复制到目标目录 129 | X "下载 配置文件" 130 | 131 | rm -f "$HOME/serv00-save-me/README.md" 132 | 133 | # 删除不需要的文件 134 | if [[ "$choice" -eq 1 ]]; then 135 | for file in "$A2/install.sh" "$A2/hy2ip.sh" "$A2/ota.sh" "$HOME/serv00-save-me/single/install.sh" "$HOME/serv00-save-me/single/hy2ip.sh" "$HOME/serv00-save-me/single/ota.sh"; do 136 | rm -f "$file" 137 | done 138 | chmod 755 "$A2/app.js" > /dev/null 2>&1 139 | echo "" 140 | echo " ┌───────────────────────────────────────────────────┐ " 141 | echo " │ 【 恭 喜 】 本机保活 部署已完成 │ " 142 | echo " ├───────────────────────────────────────────────────┤ " 143 | echo " │ 保活地址: │ " 144 | printf " │ → %-46s │\n" "https://$W/info" 145 | echo " └───────────────────────────────────────────────────┘ " 146 | echo "" 147 | 148 | else 149 | for file in "$A2/ota.sh" "$HOME/serv00-save-me/server/ota.sh"; do 150 | rm -f "$file" 151 | done 152 | chmod 755 "$A2/app.js" > /dev/null 2>&1 153 | 154 | echo "" 155 | echo " ┌───────────────────────────────────────────────────┐ " 156 | echo " │ 【 恭 喜 】 账号服务 部署已完成 │ " 157 | echo " ├───────────────────────────────────────────────────┤ " 158 | echo " │ 账号服务 只要部署1个,多了无用 │ " 159 | echo " ├───────────────────────────────────────────────────┤ " 160 | echo " │ 服务地址: │ " 161 | printf " │ → %-46s │\n" "https://$W/" 162 | echo " └───────────────────────────────────────────────────┘ " 163 | echo "" 164 | fi 165 | 166 | # 如果时区被修改,提示用户重新登录 167 | if [[ "$TZ_MODIFIED" -eq 1 ]]; then 168 | echo " ┌───────────────────────────────────────────────────┐ " 169 | echo " │ 全部安装完成,还需其它操作请重登陆 │ " 170 | echo " └───────────────────────────────────────────────────┘ " 171 | sleep 3 172 | kill -9 $PPID 173 | fi 174 | -------------------------------------------------------------------------------- /single/ota.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 配置变量 4 | USER=$(whoami) 5 | USER_NAME=$(echo "$USER" | tr '[:upper:]' '[:lower:]') # 获取当前用户名并转换为小写 6 | REPO_PATH="$HOME/serv00-save-me" 7 | SINGLE_PATH="$REPO_PATH/single" 8 | TARGET_PATH="/home/$USER/domains/$USER_NAME.serv00.net/public_nodejs" 9 | BRANCH="main" # 根据你的仓库调整分支 10 | 11 | # 设置 GIT_DISCOVERY_ACROSS_FILESYSTEM 环境变量(避免跨文件系统的错误) 12 | export GIT_DISCOVERY_ACROSS_FILESYSTEM=1 13 | 14 | # 进入仓库目录 15 | cd "$REPO_PATH" || { echo "🚫 目录不是 Git 环境!"; exit 1; } 16 | 17 | # 检查仓库是否正确初始化 18 | if [ ! -d ".git" ]; then 19 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 20 | echo "🚫 运行环境错误" 21 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 22 | exit 1 23 | fi 24 | 25 | # 记录 single 目录下的变动文件,排除 .sh 和 .md 文件 26 | git fetch origin "$BRANCH" >/dev/null 2>&1 27 | CHANGED_FILES=$(git diff --name-only origin/"$BRANCH" -- single | grep -Ev '\.sh$|\.md$') 28 | 29 | # 如果没有文件变动,则退出 30 | if [ -z "$CHANGED_FILES" ]; then 31 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 32 | echo "✅ 文件均为最新!" 33 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 34 | exit 0 35 | fi 36 | 37 | # 打印有文件更新 38 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 39 | echo "💡 发现 有文件变动:" 40 | for file in $CHANGED_FILES; do 41 | RELATIVE_PATH=$(echo "$file" | sed -e 's/^single\///' -e 's/^public\///') 42 | echo "🎯 $RELATIVE_PATH" 43 | done 44 | 45 | # 先存储本地修改,避免冲突 46 | git stash >/dev/null 2>&1 47 | if [ $? -ne 0 ]; then 48 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 49 | echo "🚫 更新失败!" 50 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 51 | exit 1 52 | fi 53 | 54 | # 拉取最新代码 55 | git reset --hard origin/"$BRANCH" >/dev/null 2>&1 56 | 57 | # 遍历变更的文件并复制到目标路径 58 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 59 | echo "🔄 更新内容:" 60 | for file in $CHANGED_FILES; do 61 | RELATIVE_PATH=${file#single/} # 去掉 "single/" 前缀 62 | TARGET_FILE="$TARGET_PATH/$RELATIVE_PATH" # 保持相对路径一致 63 | 64 | rm -f "$SINGLE_PATH/ota.sh" "$SINGLE_PATH/hy2ip.sh" "$SINGLE_PATH/install.sh" "$REPO_PATH/README.md"; 65 | 66 | # 如果是文件删除(在仓库中删除),则删除目标路径的文件 67 | if ! git ls-files --error-unmatch "$file" >/dev/null 2>&1; then 68 | if [ -f "$TARGET_FILE" ]; then 69 | rm -f "$TARGET_FILE" 70 | echo "🗑️ 清理无效文件:$(basename "$TARGET_FILE")" 71 | fi 72 | else 73 | # 复制文件 74 | cp -f "$SINGLE_PATH/$RELATIVE_PATH" "$TARGET_FILE" 75 | echo "✅ $(basename "$TARGET_FILE")" 76 | fi 77 | done 78 | 79 | # 更新完成后重启服务 80 | devil www restart "$USER_NAME.serv00.net" >/dev/null 2>&1 81 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 82 | echo "🎉 更 新 完 成!" 83 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━" 84 | -------------------------------------------------------------------------------- /single/public/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 修改配置 7 | 91 | 92 | 93 |
94 |

配置修改(可单改)

95 | 96 |

当前优选: 加载中...

97 | 98 | 99 |

【注】:以下两项需要对应修改 cloudflare 配置!

100 |

当前ARGO_TOKEN: 加载中...

101 | 102 | 103 |

当前ARGO域名: 加载中...

104 | 105 | 106 | 107 |
108 | 109 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /single/public/hy2ip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HY2_IP 更新 7 | 121 | 122 | 123 |
124 |

HY2_IP 更新

125 | 126 |

127 | 128 |
129 | 130 | 133 |
134 |

⚠️ 不同 IP 更新后原线路会失效,请复制新信息使用。

135 |
136 |

日志:

137 |
138 |
139 |
140 | 141 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /single/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Windstorm Technology Forum 7 | 93 | 94 | 95 | 96 |
97 |

Windstorm Technology Forum

98 |
Loading...
99 |
100 | 101 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /single/public/log.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 日志与进程详情 7 | 84 | 85 | 86 |
87 |
最近日志:\n加载中...
88 |
89 |
进程详情:\n加载中...
90 |
91 |
92 | 93 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /single/public/newset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 节点改名 7 | 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 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /single/public/ota.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 保活端 OTA 更新 7 | 106 | 107 | 108 |
109 |

保活端 OTA 更新

110 | 111 |
112 |
113 | 114 | 153 | 154 | -------------------------------------------------------------------------------- /single/public/outbounds.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 出站设置 7 | 159 | 160 | 161 | 162 |
163 |

出站设置

164 | 165 |
166 | 当前状态: 加载中... 167 |
168 | 169 |
170 | 171 | 177 |
178 | 179 | 180 |
181 |

WireGuard 出站

182 |

默认通用 配置

183 | 184 |
185 | 186 | 187 |
188 |

Socks 配置

189 |
190 | 191 |
192 |
193 | 194 |
195 |
196 | 197 |
198 |
199 | 200 |
201 | 202 |
203 |
204 | 205 |
206 | 207 |
208 |
209 | 210 | 312 | 313 | 314 | 315 | --------------------------------------------------------------------------------