├── 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 | 
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 | 
51 |
52 | 
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 |
140 |
143 | |
144 | 操作 |
145 |
146 |
147 |
148 |
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 |
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 |
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 |
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 |
134 |
⚠️ 不同 IP 更新后原线路会失效,请复制新信息使用。
135 |
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 |
91 |
92 |
93 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/single/public/newset.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 节点改名
7 |
147 |
148 |
149 |
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 |
--------------------------------------------------------------------------------