├── LICENSE ├── README.md ├── agent.sh ├── argows.js ├── setup-argo.sh ├── setup-ws.sh └── setup.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mqiancheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebSocket服务器部署工具 2 | 3 | 这是一个简化的WebSocket服务器部署工具,用于在共享主机环境中快速部署WebSocket服务和哪吒探针。 4 | 5 | ## 目录 6 | 7 | - [特点](#特点) 8 | - [使用方法](#使用方法) 9 | - [快速开始](#快速开始) 10 | - [功能选项](#功能选项) 11 | - [配置选项](#配置选项) 12 | - [创建Node.js应用](#创建nodejs应用) 13 | - [日志记录](#日志记录) 14 | - [订阅地址](#订阅地址) 15 | - [常见问题](#常见问题) 16 | - [自动保活功能](#自动保活功能) 17 | - [设置自动保活](#设置自动保活) 18 | - [自动保活工作原理](#自动保活工作原理) 19 | - [注意事项](#注意事项) 20 | - [贡献](#贡献) 21 | - [许可证](#许可证) 22 | 23 | ## 特点 24 | 25 | - 简单易用的交互式菜单界面 26 | - 自动检测域名目录和Node.js环境 27 | - 引导用户创建Node.js应用 28 | - 自动安装和配置WebSocket服务 29 | - 可选安装和配置哪吒探针(支持v0和v1版本自动识别) 30 | - 支持自定义反代域名 31 | - 支持VLESS协议 32 | - 提供Base64编码的订阅地址 33 | - 简洁高效的代理服务 34 | - 运行统计功能 35 | - 详细的日志记录 36 | 37 | ## 使用方法 38 | 39 | ### 快速开始 40 | 41 | 一键下载并运行脚本: 42 | 43 | ```bash 44 | curl -L https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/setup-ws.sh -o setup-ws.sh && chmod +x setup-ws.sh && ./setup-ws.sh 45 | ``` 46 | 47 | ### 功能选项 48 | 49 | 脚本提供以下功能选项: 50 | 51 | 1. **修改配置文件**: 52 | - 从系统中检测到的域名列表中选择一个域名 53 | - 设置节点名称、端口、UUID等配置信息 54 | - 如果设置的端口被占用,脚本会提示您输入新的端口 55 | - 可选配置哪吒探针服务器地址和密钥(自动识别v0/v1版本) 56 | - 所有配置信息保存在`~/tmp/ws_config/ws_config.conf`文件中 57 | 58 | 2. **启动WebSocket代理服务**: 59 | - 自动创建index.js和package.json文件 60 | - 使用Node.js虚拟环境启动服务 61 | - 安装依赖并启动服务 62 | - 显示订阅地址,根据选择的协议显示相应信息 63 | 64 | 3. **启动哪吒探针**: 65 | - 下载并安装哪吒探针 66 | - 使用保存的配置信息启动探针 67 | 68 | 0. **退出脚本**: 69 | - 安全退出脚本 70 | 71 | 5. **强制重新安装**: 72 | - 停止所有相关进程 73 | - 删除所有相关文件 74 | - 清理配置信息和日志 75 | 76 | ## 配置选项 77 | 78 | 在"修改配置文件"选项中,您需要提供以下信息: 79 | 80 | - **域名**:脚本会自动扫描您的域名目录,您可以从列表中选择或手动输入 81 | - **节点名称**:显示在订阅信息中的节点名称(默认:hostvps) 82 | - **监听端口**:WebSocket服务器监听的端口(默认:3000) 83 | - **UUID**:可以自动生成或手动输入,用于WebSocket连接验证 84 | - **反代域名**:用于代理连接的反代域名(默认:www.visa.com.tw) 85 | - **哪吒服务器地址**:哪吒探针服务器地址(可选,自动识别v0/v1版本) 86 | - v1格式:`nz.example.com:443`(包含端口号) 87 | - v0格式:`nz.example.com`(不包含端口号,会额外询问端口) 88 | - **哪吒客户端密钥**:哪吒探针的客户端密钥(可选) 89 | 90 | 配置文件保存在`~/tmp/ws_config/ws_config.conf`中,包含以下内容: 91 | 92 | ``` 93 | # WebSocket服务配置 94 | DOMAIN="yourdomain.com" # 您的域名 95 | DOMAIN_DIR="/home/user/domains/yourdomain.com/public_html" # 域名目录 96 | NODE_NAME="NodeWS" # 节点名称 97 | PORT="3000" # 内部端口 98 | UUID="a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6" # UUID 99 | CF_DOMAIN="www.visa.com.tw" # 反代域名 100 | PROTOCOL="vless" # 代理协议 101 | PROTOCOL_NAME="VLESS" # 协议名称 102 | 103 | # 哪吒探针配置 104 | NEZHA_SERVER="nezha.example.com:5555" # 哪吒服务器地址(v1格式) 105 | NEZHA_KEY="your_nezha_key" # 哪吒客户端密钥 106 | NEZHA_PORT="" # 哪吒v0版本端口(仅v0版本需要) 107 | NEZHA_TLS="true" # v1版本:true/false,v0版本:--tls/空 108 | ``` 109 | 110 | ## 创建Node.js应用 111 | 112 | 如果脚本检测到您尚未创建Node.js应用,它会引导您完成以下步骤: 113 | 114 | 1. 进入控制面板 -> Node.js APP 115 | 2. 点击"创建应用程序" 116 | 3. Node.js版本: 选择最新版本 117 | 4. Application root: domains/您的域名/public_html 118 | 5. Application startup file: index.js 119 | 6. 点击"创建"按钮 120 | 121 | 创建完成后,重新运行脚本继续配置。 122 | 123 | ## 日志记录 124 | 125 | 脚本会在用户主目录下创建详细的日志文件(格式:`~/tmp/ws_setup_logs/ws_setup_日期时间.log`),记录安装过程中的每一步操作。如果遇到问题,请查看此日志文件以获取详细信息。 126 | 127 | ## 订阅地址 128 | 129 | WebSocket服务启动后,脚本会自动显示您的VLESS订阅地址: 130 | 131 | ``` 132 | 您的VLESS订阅地址是:https://您的域名/sub 133 | ``` 134 | 135 | 例如:`https://example.com/sub` 136 | 137 | 订阅地址会返回一个Base64编码的VLESS链接,可以直接导入到支持VLESS协议的客户端中。 138 | 139 | VLESS链接使用您配置的反代域名作为服务器地址,使用您的实际域名作为SNI和Host参数,这样可以提高连接成功率。 140 | 141 | ## 常见问题 142 | 143 | 1. **WebSocket服务无法启动**: 144 | - 确保已在控制面板中创建Node.js应用 145 | - 如果出现端口占用错误,脚本会提示您输入新的端口号 146 | - 输入新端口后,重新启动WebSocket服务即可 147 | 148 | 2. **哪吒探针未连接**: 149 | - 确认服务器地址和密钥是否正确 150 | - 检查是否正确识别了哪吒版本(v0/v1) 151 | - 检查网络连接是否正常 152 | 153 | 3. **订阅地址返回"It works!"**: 154 | - 检查WebSocket服务是否正常启动 155 | - 查看node.log文件了解可能的错误 156 | - 访问`/debug`路径查看当前配置信息 157 | 158 | 4. **域名目录检测失败**: 159 | - 确认您的域名是否已在控制面板中创建 160 | - 检查域名目录结构是否正确 161 | 162 | ## 自动保活功能 163 | 164 | 脚本提供了自动保活功能,可以通过定时任务自动检查并重启服务。这对于共享主机环境特别有用,因为服务器可能会定期清理长时间运行的进程。 165 | 166 | ### 设置自动保活 167 | 168 | 1. 在控制面板中找到"Cron Jobs"(定时任务)功能 169 | 2. 添加以下两个定时任务: 170 | 171 | **系统重启后自动启动服务**: 172 | - 选择"Run on @reboot"选项 173 | - 命令:`cd $HOME && $HOME/setup-ws.sh check_and_start_all` 174 | - 如果您担心收到电子邮件通知,请选择"阻止电子邮件"选项 175 | 176 | **定期检查并重启服务**: 177 | - 频率:每5-10分钟(Cron表达式:`*/10 * * * *`) 178 | - 命令:`cd $HOME && $HOME/setup-ws.sh check_and_start_all` 179 | - 如果您担心收到电子邮件通知,请选择"阻止电子邮件"选项 180 | 181 | > **注意**:许多共享主机的控制面板在选择"阻止电子邮件"选项时会自动在命令末尾添加重定向(如 `>/dev/null 2>&1`)。这是正常现象,不会影响脚本的执行。即使出现重复的重定向也不用担心,命令仍然可以正常工作。 182 | 183 | ### 自动保活工作原理 184 | 185 | 1. **WebSocket服务保活**: 186 | - 脚本会检查WebSocket服务是否运行 187 | - 如果服务未运行,会通过访问订阅地址(`/sub`)来触发服务启动 188 | - 这利用了Web服务器的按需启动机制 189 | 190 | 2. **哪吒探针保活**: 191 | - 脚本会检查哪吒探针是否运行 192 | - 如果探针未运行,会使用配置文件中的信息重新启动探针 193 | 194 | 3. **日志记录**: 195 | - 自动保活的操作会记录在`~/tmp/ws_setup_logs/cron_autorestart.log`文件中 196 | - 您可以查看此日志了解自动保活的运行情况 197 | 198 | ## 注意事项 199 | 200 | - 哪吒探针服务器地址格式会自动识别v0/v1版本 201 | - 强制重新安装会删除所有相关文件和日志 202 | - 自动保活功能需要先运行脚本并完成配置 203 | - 配置信息保存在`~/tmp/ws_config/ws_config.conf`文件中 204 | - 日志文件存储在`~/tmp/ws_setup_logs/`目录下 205 | - 仅支持VLESS协议 206 | 207 | ## 贡献 208 | 209 | 欢迎提交问题和改进建议到GitHub仓库:[https://github.com/mqiancheng/host-node-ws](https://github.com/mqiancheng/host-node-ws) 210 | 211 | ## 许可证 212 | 213 | MIT 214 | 215 | -------------------------------------------------------------------------------- /agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NZ_BASE_PATH="$HOME/nezha" 4 | NZ_AGENT_PATH="${NZ_BASE_PATH}/agent" 5 | 6 | red='\033[0;31m' 7 | green='\033[0;32m' 8 | yellow='\033[0;33m' 9 | plain='\033[0m' 10 | 11 | err() { 12 | printf "${red}%s${plain}\n" "$*" >&2 13 | } 14 | 15 | success() { 16 | printf "${green}%s${plain}\n" "$*" 17 | } 18 | 19 | warn() { 20 | printf "${red}%s${plain}\n" "$*" 21 | } 22 | 23 | info() { 24 | printf "${yellow}%s${plain}\n" "$*" 25 | } 26 | 27 | deps_check() { 28 | deps="wget unzip grep" 29 | set -- "$api_list" 30 | for dep in $deps; do 31 | if ! command -v "$dep" >/dev/null 2>&1; then 32 | err "$dep not found, please install it first." 33 | exit 1 34 | fi 35 | done 36 | } 37 | 38 | env_check() { 39 | mach=$(uname -m) 40 | case "$mach" in 41 | amd64|x86_64) 42 | os_arch="amd64" 43 | ;; 44 | i386|i686) 45 | os_arch="386" 46 | ;; 47 | aarch64|arm64) 48 | os_arch="arm64" 49 | ;; 50 | *arm*) 51 | os_arch="arm" 52 | ;; 53 | s390x) 54 | os_arch="s390x" 55 | ;; 56 | riscv64) 57 | os_arch="riscv64" 58 | ;; 59 | mips) 60 | os_arch="mips" 61 | ;; 62 | mipsel|mipsle) 63 | os_arch="mipsle" 64 | ;; 65 | *) 66 | err "Unknown architecture: $uname" 67 | exit 1 68 | ;; 69 | esac 70 | 71 | system=$(uname) 72 | case "$system" in 73 | *Linux*) 74 | os="linux" 75 | ;; 76 | *Darwin*) 77 | os="darwin" 78 | ;; 79 | *FreeBSD*) 80 | os="freebsd" 81 | ;; 82 | *) 83 | err "Unknown architecture: $system" 84 | exit 1 85 | ;; 86 | esac 87 | } 88 | 89 | init() { 90 | deps_check 91 | env_check 92 | } 93 | 94 | install() { 95 | echo "Installing..." 96 | 97 | # 检查是否已经下载了文件 98 | if [ -f "nezha-agent_linux_amd64.zip" ] && [ "$os" = "linux" ] && [ "$os_arch" = "amd64" ]; then 99 | echo "Using existing file in current directory: nezha-agent_linux_amd64.zip" 100 | AGENT_ZIP="nezha-agent_linux_amd64.zip" 101 | else 102 | # 尝试获取最新版本 103 | LATEST_VERSION=$(curl -s https://api.github.com/repos/nezhahq/agent/releases/latest | grep -o '"tag_name": ".*"' | cut -d'"' -f4) 104 | if [ -n "$LATEST_VERSION" ]; then 105 | echo "Found latest version: $LATEST_VERSION" 106 | NZ_AGENT_URL="https://github.com/nezhahq/agent/releases/download/${LATEST_VERSION}/nezha-agent_${os}_${os_arch}.zip" 107 | else 108 | echo "Failed to get latest version, using fallback version v1.12.2" 109 | NZ_AGENT_URL="https://github.com/nezhahq/agent/releases/download/v1.12.2/nezha-agent_${os}_${os_arch}.zip" 110 | fi 111 | 112 | echo "Downloading from: $NZ_AGENT_URL" 113 | AGENT_ZIP="nezha-agent_${os}_${os_arch}.zip" 114 | 115 | # 尝试下载最新版本 116 | if ! wget -T 60 -O "$AGENT_ZIP" "$NZ_AGENT_URL"; then 117 | echo "Failed to download latest version, trying fallback version v1.12.2" 118 | 119 | # 设置回退版本URL 120 | FALLBACK_URL="https://github.com/nezhahq/agent/releases/download/v1.12.2/nezha-agent_${os}_${os_arch}.zip" 121 | 122 | echo "Downloading fallback from: $FALLBACK_URL" 123 | 124 | # 尝试下载回退版本 125 | if ! wget -T 60 -O "$AGENT_ZIP" "$FALLBACK_URL"; then 126 | err "Download nezha-agent release failed, check your network connectivity" 127 | exit 1 128 | fi 129 | fi 130 | fi 131 | 132 | # 验证文件是否存在 133 | if [ ! -f "$AGENT_ZIP" ]; then 134 | err "File not found: $AGENT_ZIP" 135 | exit 1 136 | fi 137 | 138 | # 创建agent目录 139 | mkdir -p "$NZ_AGENT_PATH" 140 | if [ $? -ne 0 ]; then 141 | err "Failed to create directory: $NZ_AGENT_PATH" 142 | exit 1 143 | fi 144 | 145 | # 解压文件 146 | echo "Extracting file to $NZ_AGENT_PATH..." 147 | unzip -o "$AGENT_ZIP" -d "$NZ_AGENT_PATH" 148 | if [ $? -ne 0 ]; then 149 | err "Failed to extract file" 150 | exit 1 151 | fi 152 | 153 | # 检查解压后的文件是否存在 154 | if [ ! -f "$NZ_AGENT_PATH/nezha-agent" ]; then 155 | err "Extracted file not found: $NZ_AGENT_PATH/nezha-agent" 156 | # 尝试直接复制已下载的文件到目标位置 157 | if [ -f "nezha-agent" ]; then 158 | echo "Found nezha-agent in current directory, copying..." 159 | cp "nezha-agent" "$NZ_AGENT_PATH/nezha-agent" 160 | chmod +x "$NZ_AGENT_PATH/nezha-agent" 161 | else 162 | exit 1 163 | fi 164 | fi 165 | 166 | # 设置执行权限 167 | chmod +x "$NZ_AGENT_PATH/nezha-agent" 168 | 169 | path="$NZ_AGENT_PATH/config.yml" 170 | if [ -f "$path" ]; then 171 | random=$(LC_ALL=C tr -dc a-z0-9 67 | /(vless|vmess|trojan|hysteria2|tuic):\/\//.test(line) 68 | ); 69 | 70 | if (nodes.length === 0) return; 71 | 72 | console.log(`Attempting to delete ${nodes.length} nodes from subscription service...`); 73 | return axios.post(`${UPLOAD_URL}/api/delete-nodes`, 74 | JSON.stringify({ nodes }), 75 | { headers: { 'Content-Type': 'application/json' } } 76 | ).then(response => { 77 | console.log('Nodes deleted successfully'); 78 | return response; 79 | }).catch((error) => { 80 | console.error(`Failed to delete nodes: ${error.message}`); 81 | return null; 82 | }); 83 | } catch (err) { 84 | console.error(`Error in deleteNodes: ${err.message}`); 85 | return null; 86 | } 87 | } 88 | 89 | //清理历史文件 90 | function cleanupOldFiles() { 91 | console.log('Cleaning up old files...'); 92 | const pathsToDelete = ['web', 'bot', 'npm', 'php', 'sub.txt', 'boot.log', 'tunnel.json', 'tunnel.yml', 'config.yaml']; 93 | pathsToDelete.forEach(file => { 94 | const filePath = path.join(FILE_PATH, file); 95 | // 使用rm -rf处理文件和目录,忽略不存在的文件错误 96 | exec(`rm -rf ${filePath}`).catch(() => { }); 97 | }); 98 | console.log('Old files cleanup completed'); 99 | } 100 | 101 | // 根路由 102 | app.get("/", function (req, res) { 103 | res.send("Proxy service with Nezha Agent and Cloudflare Tunnel is running!"); 104 | }); 105 | 106 | // 生成xr-ay配置文件 107 | function generateXrayConfig() { 108 | console.log('Generating xr-ay configuration...'); 109 | const config = { 110 | log: { access: '/dev/null', error: '/dev/null', loglevel: 'none' }, 111 | inbounds: [ 112 | { port: ARGO_PORT, protocol: 'vless', settings: { clients: [{ id: UUID, flow: 'xtls-rprx-vision' }], decryption: 'none', fallbacks: [{ dest: 3001 }, { path: "/vless-argo", dest: 3002 }, { path: "/vmess-argo", dest: 3003 }, { path: "/trojan-argo", dest: 3004 }] }, streamSettings: { network: 'tcp' } }, 113 | { port: 3001, listen: "127.0.0.1", protocol: "vless", settings: { clients: [{ id: UUID }], decryption: "none" }, streamSettings: { network: "tcp", security: "none" } }, 114 | { port: 3002, listen: "127.0.0.1", protocol: "vless", settings: { clients: [{ id: UUID, level: 0 }], decryption: "none" }, streamSettings: { network: "ws", security: "none", wsSettings: { path: "/vless-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, 115 | { port: 3003, listen: "127.0.0.1", protocol: "vmess", settings: { clients: [{ id: UUID, alterId: 0 }] }, streamSettings: { network: "ws", wsSettings: { path: "/vmess-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, 116 | { port: 3004, listen: "127.0.0.1", protocol: "trojan", settings: { clients: [{ password: UUID }] }, streamSettings: { network: "ws", security: "none", wsSettings: { path: "/trojan-argo" } }, sniffing: { enabled: true, destOverride: ["http", "tls", "quic"], metadataOnly: false } }, 117 | ], 118 | dns: { servers: ["https+local://8.8.8.8/dns-query"] }, 119 | outbounds: [{ protocol: "freedom", tag: "direct" }, { protocol: "blackhole", tag: "block" }] 120 | }; 121 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 122 | console.log('xr-ay configuration generated successfully'); 123 | } 124 | 125 | // 判断系统架构 126 | function getSystemArchitecture() { 127 | const arch = os.arch(); 128 | if (arch === 'arm' || arch === 'arm64' || arch === 'aarch64') { 129 | return 'arm'; 130 | } else { 131 | return 'amd'; 132 | } 133 | } 134 | 135 | // 下载对应系统架构的依赖文件 136 | function downloadFile(fileName, fileUrl, callback) { 137 | const filePath = path.join(FILE_PATH, fileName); 138 | 139 | // 检查文件是否已存在,如果存在则跳过下载 140 | if (fs.existsSync(filePath)) { 141 | console.log(`${fileName} already exists, skipping download.`); 142 | setTimeout(() => callback(null, fileName), 0); 143 | return; 144 | } 145 | 146 | console.log(`Attempting to download ${fileName} from ${fileUrl}`); 147 | const writer = fs.createWriteStream(filePath); 148 | 149 | axios({ 150 | method: 'get', 151 | url: fileUrl, 152 | responseType: 'stream', 153 | timeout: 30000 // 30秒超时 154 | }) 155 | .then(response => { 156 | response.data.pipe(writer); 157 | 158 | writer.on('finish', () => { 159 | writer.close(); 160 | console.log(`Download ${fileName} successfully`); 161 | callback(null, fileName); 162 | }); 163 | 164 | writer.on('error', err => { 165 | fs.unlink(filePath, () => { }); // 清理不完整的文件 166 | const errorMessage = `Download ${fileName} failed (writer error): ${err.message}`; 167 | console.error(errorMessage); 168 | callback(errorMessage); 169 | }); 170 | }) 171 | .catch(err => { 172 | if (fs.existsSync(filePath)) { 173 | fs.unlink(filePath, () => { }); // 清理可能不完整的文件 174 | } 175 | const errorMessage = `Download ${fileName} failed (axios error): ${err.message}`; 176 | console.error(errorMessage); 177 | callback(errorMessage); 178 | }); 179 | } 180 | 181 | // 下载并运行依赖文件 182 | async function downloadFilesAndRun() { 183 | const architecture = getSystemArchitecture(); 184 | console.log(`Detected system architecture: ${architecture}`); 185 | const filesToDownload = getFilesForArchitecture(architecture); 186 | 187 | if (filesToDownload.length === 0) { 188 | console.log(`Can't find files for the current architecture`); 189 | return; 190 | } 191 | 192 | console.log(`Files to download: ${filesToDownload.map(f => f.fileName).join(', ')}`); 193 | 194 | const downloadPromises = filesToDownload.map(fileInfo => { 195 | return new Promise((resolve, reject) => { 196 | downloadFile(fileInfo.fileName, fileInfo.fileUrl, (err, fileName) => { 197 | if (err) { 198 | console.error(`Failed to download ${fileInfo.fileName}: ${err}`); 199 | resolve(null); // 允许其他下载继续 200 | } else { 201 | resolve(fileName); 202 | } 203 | }); 204 | }); 205 | }); 206 | 207 | try { 208 | const downloadedFiles = await Promise.all(downloadPromises); 209 | const successfullyDownloaded = downloadedFiles.filter(name => name !== null); 210 | 211 | // 检查必要文件是否下载成功 212 | if (!successfullyDownloaded.includes('web')) { 213 | console.error("Essential file 'web' (proxy service) failed to download."); 214 | } 215 | if (!successfullyDownloaded.includes('bot')) { 216 | console.error("Essential file 'bot' (cloudflared) failed to download."); 217 | } 218 | if (NEZHA_SERVER && NEZHA_KEY) { 219 | const nezhaAgentFile = NEZHA_PORT ? 'npm' : 'php'; 220 | if (!successfullyDownloaded.includes(nezhaAgentFile)) { 221 | console.error(`Essential file '${nezhaAgentFile}' (Nezha agent) failed to download.`); 222 | } 223 | } 224 | 225 | console.log('File download process completed.'); 226 | } catch (err) { 227 | console.error('Error downloading files:', err); 228 | return; 229 | } 230 | 231 | // 授权和运行 232 | function authorizeFiles(filePaths) { 233 | const newPermissions = 0o775; // rwxrwxr-x 234 | filePaths.forEach(relativeFilePath => { 235 | const absoluteFilePath = path.join(FILE_PATH, relativeFilePath); 236 | if (fs.existsSync(absoluteFilePath)) { 237 | try { 238 | fs.chmodSync(absoluteFilePath, newPermissions); 239 | console.log(`Empowerment success for ${absoluteFilePath}: ${newPermissions.toString(8)}`); 240 | } catch (err) { 241 | console.error(`Empowerment failed for ${absoluteFilePath}: ${err}`); 242 | } 243 | } else { 244 | console.warn(`Cannot authorize ${absoluteFilePath}: File does not exist.`); 245 | } 246 | }); 247 | } 248 | 249 | // 确定需要授权的文件 250 | const filesToAuthorize = ['web', 'bot']; 251 | if (NEZHA_SERVER && NEZHA_KEY) { 252 | filesToAuthorize.push(NEZHA_PORT ? 'npm' : 'php'); 253 | } 254 | authorizeFiles(filesToAuthorize); 255 | 256 | //运行ne-zha 257 | if (NEZHA_SERVER && NEZHA_KEY && fs.existsSync(path.join(FILE_PATH, NEZHA_PORT ? 'npm' : 'php'))) { 258 | if (!NEZHA_PORT) { // 使用哪吒v1 (php文件) 259 | // 检测哪吒是否开启TLS 260 | const port = NEZHA_SERVER.includes(':') ? NEZHA_SERVER.split(':').pop() : ''; 261 | const tlsPorts = new Set(['443', '8443', '2096', '2087', '2083', '2053']); 262 | const nezhatls = tlsPorts.has(port) ? 'true' : 'false'; 263 | 264 | // 生成 config.yaml 265 | const configYaml = ` 266 | client_secret: ${NEZHA_KEY} 267 | debug: false 268 | disable_auto_update: true 269 | disable_command_execute: false 270 | disable_force_update: true 271 | disable_nat: false 272 | disable_send_query: false 273 | gpu: false 274 | insecure_tls: false 275 | ip_report_period: 1800 276 | report_delay: 1 277 | server: ${NEZHA_SERVER} 278 | skip_connection_count: false 279 | skip_procs_count: false 280 | temperature: false 281 | tls: ${nezhatls} 282 | use_gitee_to_upgrade: false 283 | use_ipv6_country_code: false 284 | uuid: ${UUID}`; 285 | 286 | try { 287 | fs.writeFileSync(nezhaConfigYamlPath, configYaml); 288 | console.log('Generated Nezha v1 config.yaml'); 289 | 290 | // 运行 php (哪吒v1 agent) 291 | const command = `nohup ${phpPath} -c "${nezhaConfigYamlPath}" >/dev/null 2>&1 &`; 292 | await exec(command); 293 | console.log('Nezha v1 agent (php) is starting...'); 294 | await new Promise((resolve) => setTimeout(resolve, 1000)); 295 | } catch (error) { 296 | console.error(`Nezha v1 agent (php) startup error: ${error}`); 297 | } 298 | } else { // 使用哪吒v0 (npm文件) 299 | let NEZHA_TLS = ''; 300 | const tlsPorts = ['443', '8443', '2096', '2087', '2083', '2053']; 301 | if (tlsPorts.includes(NEZHA_PORT)) { 302 | NEZHA_TLS = '--tls'; 303 | } 304 | const command = `nohup ${npmPath} -s ${NEZHA_SERVER}:${NEZHA_PORT} -p ${NEZHA_KEY} ${NEZHA_TLS} >/dev/null 2>&1 &`; 305 | try { 306 | await exec(command); 307 | console.log('Nezha v0 agent (npm) is starting...'); 308 | await new Promise((resolve) => setTimeout(resolve, 1000)); 309 | } catch (error) { 310 | console.error(`Nezha v0 agent (npm) startup error: ${error}`); 311 | } 312 | } 313 | } else if (NEZHA_SERVER && NEZHA_KEY) { 314 | console.warn('Nezha variables are set, but the agent executable is missing. Skipping Nezha agent start.'); 315 | } else { 316 | console.log('Nezha variables not fully set, skipping Nezha agent start.'); 317 | } 318 | 319 | //运行xr-ay 320 | if (fs.existsSync(webPath)) { 321 | const command1 = `nohup ${webPath} -c ${configPath} >/dev/null 2>&1 &`; 322 | try { 323 | await exec(command1); 324 | console.log('xr-ay proxy service (web) is running'); 325 | await new Promise((resolve) => setTimeout(resolve, 1000)); 326 | } catch (error) { 327 | console.error(`xr-ay proxy service (web) running error: ${error}`); 328 | } 329 | } else { 330 | console.error("xr-ay proxy service executable 'web' not found. Proxy service will not start."); 331 | } 332 | 333 | // 运行cloud-fared 334 | if (fs.existsSync(botPath)) { 335 | let args; 336 | 337 | if (ARGO_AUTH && ARGO_DOMAIN) { 338 | // 用户指定了域名和认证(Token或JSON) 339 | if (ARGO_AUTH.match(/^[A-Z0-9a-z=]{120,250}$/)) { 340 | // Token认证 341 | args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 run --token ${ARGO_AUTH}`; 342 | console.log('Starting Cloudflare Tunnel with Token.'); 343 | } else if (ARGO_AUTH.includes('TunnelSecret') && fs.existsSync(tunnelYmlPath)) { 344 | // JSON认证(tunnel.yml应该已由argoType创建) 345 | args = `tunnel --edge-ip-version auto --config ${tunnelYmlPath} run`; 346 | console.log('Starting Cloudflare Tunnel with JSON credentials file.'); 347 | } else { 348 | console.warn('ARGO_AUTH specified but format not recognized as Token or JSON secret. Attempting temporary tunnel.'); 349 | // 如果认证格式不正确但设置了域名,则回退到临时隧道 350 | args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile ${bootLogPath} --loglevel info --url http://localhost:${ARGO_PORT}`; 351 | console.log('Starting Temporary Cloudflare Tunnel (fallback).'); 352 | } 353 | } else { 354 | // 临时隧道(未指定域名/认证或只指定了一个) 355 | args = `tunnel --edge-ip-version auto --no-autoupdate --protocol http2 --logfile ${bootLogPath} --loglevel info --url http://localhost:${ARGO_PORT}`; 356 | console.log('Starting Temporary Cloudflare Tunnel.'); 357 | } 358 | 359 | try { 360 | await exec(`nohup ${botPath} ${args} >/dev/null 2>&1 &`); 361 | console.log('Cloudflare Tunnel (bot) is starting...'); 362 | await new Promise((resolve) => setTimeout(resolve, 4000)); // 增加延迟,让隧道有时间写入日志 363 | } catch (error) { 364 | console.error(`Error starting Cloudflare Tunnel (bot): ${error}`); 365 | } 366 | } else { 367 | console.warn("Cloudflare Tunnel executable 'bot' not found. Skipping tunnel start."); 368 | } 369 | 370 | await new Promise((resolve) => setTimeout(resolve, 5000)); 371 | } 372 | 373 | //根据系统架构返回对应的url 374 | function getFilesForArchitecture(architecture) { 375 | let baseFiles = []; 376 | console.log(`Determining files for architecture: ${architecture}`); 377 | 378 | // 代理服务 (web) 总是需要的 379 | const webUrl = architecture === 'arm' 380 | ? "https://arm64.ssss.nyc.mn/web" 381 | : "https://amd64.ssss.nyc.mn/web"; 382 | baseFiles.push({ fileName: "web", fileUrl: webUrl }); 383 | 384 | // Cloudflared (bot) 总是需要的 385 | const botUrl = architecture === 'arm' 386 | ? "https://arm64.ssss.nyc.mn/2go" 387 | : "https://amd64.ssss.nyc.mn/2go"; 388 | baseFiles.push({ fileName: "bot", fileUrl: botUrl }); 389 | 390 | // 哪吒探针仅在配置时下载 391 | if (NEZHA_SERVER && NEZHA_KEY) { 392 | if (NEZHA_PORT) { // 哪吒v0 (npm) 393 | const npmUrl = architecture === 'arm' 394 | ? "https://arm64.ssss.nyc.mn/agent" 395 | : "https://amd64.ssss.nyc.mn/agent"; 396 | baseFiles.push({ 397 | fileName: "npm", 398 | fileUrl: npmUrl 399 | }); 400 | } else { // 哪吒v1 (php) 401 | const phpUrl = architecture === 'arm' 402 | ? "https://arm64.ssss.nyc.mn/v1" 403 | : "https://amd64.ssss.nyc.mn/v1"; 404 | baseFiles.push({ 405 | fileName: "php", 406 | fileUrl: phpUrl 407 | }); 408 | } 409 | } else { 410 | console.log("Nezha agent download skipped (not configured)."); 411 | } 412 | 413 | return baseFiles; 414 | } 415 | 416 | // 获取固定隧道json/yml 417 | function argoType() { 418 | if (!ARGO_AUTH || !ARGO_DOMAIN) { 419 | console.log("ARGO_DOMAIN or ARGO_AUTH variable is empty/missing, will use temporary tunnel if needed."); 420 | return; 421 | } 422 | 423 | if (ARGO_AUTH.includes('TunnelSecret')) { 424 | console.log("ARGO_AUTH appears to be JSON secret. Generating tunnel.json and tunnel.yml"); 425 | try { 426 | // 尝试解析JSON部分以获取隧道ID 427 | let tunnelID = "unknown-tunnel-id"; // 默认回退 428 | try { 429 | const parsedAuth = JSON.parse(ARGO_AUTH); 430 | tunnelID = parsedAuth.TunnelID || tunnelID; 431 | } catch (parseError) { 432 | console.warn("Could not parse ARGO_AUTH as JSON to extract TunnelID, using default in tunnel.yml"); 433 | // 如果JSON解析失败,使用正则表达式作为不太可靠的回退 434 | const match = ARGO_AUTH.match(/"TunnelID":"([^"]+)"/); 435 | if (match && match[1]) { 436 | tunnelID = match[1]; 437 | } 438 | } 439 | 440 | fs.writeFileSync(tunnelJsonPath, ARGO_AUTH); 441 | 442 | // 指向ARGO_PORT的入口 443 | const tunnelYaml = ` 444 | tunnel: ${tunnelID} 445 | credentials-file: ${tunnelJsonPath} 446 | protocol: http2 447 | 448 | ingress: 449 | - hostname: ${ARGO_DOMAIN} 450 | service: http://localhost:${ARGO_PORT} 451 | originRequest: 452 | noTLSVerify: true # 如果本地服务使用自签名证书,保留此项 453 | - service: http_status:404 # 不匹配主机名的请求的默认回退 454 | `; 455 | fs.writeFileSync(tunnelYmlPath, tunnelYaml); 456 | console.log(`Generated tunnel.yml for domain ${ARGO_DOMAIN} pointing to http://localhost:${ARGO_PORT}`); 457 | 458 | } catch (error) { 459 | console.error(`Error writing tunnel configuration files: ${error}`); 460 | // 清理可能损坏的文件 461 | fs.unlink(tunnelJsonPath, () => { }); 462 | fs.unlink(tunnelYmlPath, () => { }); 463 | } 464 | 465 | } else if (ARGO_AUTH.match(/^[A-Z0-9a-z=]{120,250}$/)) { 466 | console.log("ARGO_AUTH appears to be a Tunnel Token. Tunnel will be run directly with the token."); 467 | // token认证运行命令不需要tunnel.yml 468 | // 如果切换认证类型,清理任何旧的yml/json文件 469 | fs.unlink(tunnelJsonPath, () => { }); 470 | fs.unlink(tunnelYmlPath, () => { }); 471 | } else { 472 | console.warn("ARGO_AUTH format is unrecognized. Tunnel setup might rely on temporary tunnel logic."); 473 | // 如果认证格式错误,清理任何旧的yml/json文件 474 | fs.unlink(tunnelJsonPath, () => { }); 475 | fs.unlink(tunnelYmlPath, () => { }); 476 | } 477 | } 478 | 479 | // 获取隧道domain 480 | async function extractDomains() { 481 | // 首先调用argoType生成tunnel.yml(如果需要) 482 | argoType(); 483 | await new Promise(resolve => setTimeout(resolve, 100)); // 小延迟确保文件写入完成 484 | 485 | let argoDomain; 486 | 487 | if (ARGO_DOMAIN) { 488 | // 如果用户提供ARGO_DOMAIN,直接使用它(固定隧道) 489 | argoDomain = ARGO_DOMAIN; 490 | console.log(`Using specified fixed tunnel domain: ${argoDomain}`); 491 | await generateLinks(argoDomain); 492 | } else if (fs.existsSync(botPath)) { 493 | // 尝试从临时隧道日志中获取域名 494 | console.log('Attempting to extract temporary tunnel domain from boot log...'); 495 | await new Promise(resolve => setTimeout(resolve, 5000)); // 等待更长时间让日志文件出现/填充 496 | 497 | try { 498 | if (!fs.existsSync(bootLogPath)) { 499 | console.log('boot.log not found. Cannot extract temporary domain. Tunnel might still be starting.'); 500 | return; // 如果日志尚不存在,则退出函数 501 | } 502 | 503 | const fileContent = fs.readFileSync(bootLogPath, 'utf-8'); 504 | const lines = fileContent.split('\n'); 505 | const argoDomains = []; 506 | // 更新正则表达式以捕获不同的日志格式 507 | const domainRegex = /https:\/\/([a-zA-Z0-9-]+-[a-zA-Z0-9-]+\.trycloudflare\.com)/; 508 | 509 | lines.forEach((line) => { 510 | const domainMatch = line.match(domainRegex); 511 | if (domainMatch && domainMatch[1]) { 512 | const domain = domainMatch[1]; 513 | // 仅添加唯一域名 514 | if (!argoDomains.includes(domain)) { 515 | argoDomains.push(domain); 516 | } 517 | } 518 | }); 519 | 520 | if (argoDomains.length > 0) { 521 | argoDomain = argoDomains[argoDomains.length - 1]; // 获取找到的最新域名 522 | console.log(`Extracted temporary tunnel domain: ${argoDomain}`); 523 | await generateLinks(argoDomain); 524 | } else { 525 | console.log('Temporary tunnel domain not found in boot.log.'); 526 | } 527 | } catch (error) { 528 | console.error('Error reading boot.log:', error); 529 | } 530 | } else { 531 | console.log("Cloudflared (bot) not found, cannot determine tunnel domain."); 532 | } 533 | 534 | // 生成 list 和 sub 信息 535 | async function generateLinks(argoDomain) { 536 | try { 537 | return new Promise((resolve) => { 538 | setTimeout(() => { 539 | const VMESS = { v: '2', ps: `${NAME}-vmess`, add: CFIP, port: CFPORT, id: UUID, aid: '0', scy: 'none', net: 'ws', type: 'none', host: argoDomain, path: '/vmess-argo?ed=2560', tls: 'tls', sni: argoDomain, alpn: '' }; 540 | const subTxt = ` 541 | vless://${UUID}@${CFIP}:${CFPORT}?encryption=none&security=tls&sni=${argoDomain}&type=ws&host=${argoDomain}&path=%2Fvless-argo%3Fed%3D2560#${NAME}-vless 542 | 543 | vmess://${Buffer.from(JSON.stringify(VMESS)).toString('base64')} 544 | 545 | trojan://${UUID}@${CFIP}:${CFPORT}?security=tls&sni=${argoDomain}&type=ws&host=${argoDomain}&path=%2Ftrojan-argo%3Fed%3D2560#${NAME}-trojan 546 | `; 547 | // 打印 sub.txt 内容到控制台 548 | console.log('Generated subscription content:'); 549 | console.log(Buffer.from(subTxt).toString('base64')); 550 | fs.writeFileSync(subPath, Buffer.from(subTxt).toString('base64')); 551 | console.log(`${FILE_PATH}/sub.txt saved successfully`); 552 | uploadNodes(); 553 | // 将内容进行 base64 编码并写入 SUB_PATH 路由 554 | app.get(`/${SUB_PATH}`, (req, res) => { 555 | const encodedContent = Buffer.from(subTxt).toString('base64'); 556 | res.set('Content-Type', 'text/plain; charset=utf-8'); 557 | res.send(encodedContent); 558 | }); 559 | resolve(subTxt); 560 | }, 2000); 561 | }); 562 | } catch (error) { 563 | console.error(`Error generating links: ${error.message}`); 564 | } 565 | } 566 | } 567 | 568 | // 自动上传节点或订阅 569 | async function uploadNodes() { 570 | if (UPLOAD_URL && PROJECT_URL) { 571 | const subscriptionUrl = `${PROJECT_URL}/${SUB_PATH}`; 572 | const jsonData = { 573 | subscription: [subscriptionUrl] 574 | }; 575 | try { 576 | console.log(`Attempting to upload subscription: ${subscriptionUrl}`); 577 | const response = await axios.post(`${UPLOAD_URL}/api/add-subscriptions`, jsonData, { 578 | headers: { 579 | 'Content-Type': 'application/json' 580 | } 581 | }); 582 | 583 | if (response.status === 200) { 584 | console.log('Subscription uploaded successfully'); 585 | } else { 586 | console.log(`Subscription upload returned status: ${response.status}`); 587 | } 588 | } catch (error) { 589 | if (error.response) { 590 | if (error.response.status === 400) { 591 | console.log('Subscription already exists'); 592 | } else { 593 | console.error(`Subscription upload failed with status: ${error.response.status}`); 594 | } 595 | } else { 596 | console.error(`Subscription upload failed: ${error.message}`); 597 | } 598 | } 599 | } else if (UPLOAD_URL) { 600 | if (!fs.existsSync(subPath)) { 601 | console.log('No subscription file found to upload nodes'); 602 | return; 603 | } 604 | 605 | try { 606 | const content = fs.readFileSync(subPath, 'utf-8'); 607 | const decoded = Buffer.from(content, 'base64').toString('utf-8'); 608 | const nodes = decoded.split('\n').filter(line => 609 | /(vless|vmess|trojan|hysteria2|tuic):\/\//.test(line) 610 | ); 611 | 612 | if (nodes.length === 0) { 613 | console.log('No valid nodes found in subscription file'); 614 | return; 615 | } 616 | 617 | console.log(`Attempting to upload ${nodes.length} nodes`); 618 | const jsonData = JSON.stringify({ nodes }); 619 | 620 | const response = await axios.post(`${UPLOAD_URL}/api/add-nodes`, jsonData, { 621 | headers: { 'Content-Type': 'application/json' } 622 | }); 623 | 624 | if (response.status === 200) { 625 | console.log('Nodes uploaded successfully'); 626 | } else { 627 | console.log(`Node upload returned status: ${response.status}`); 628 | } 629 | } catch (error) { 630 | console.error(`Node upload failed: ${error.message}`); 631 | } 632 | } else { 633 | console.log('Skipping upload nodes (UPLOAD_URL not configured)'); 634 | } 635 | } 636 | 637 | // 自动访问项目URL 638 | async function addVisitTask() { 639 | if (!AUTO_ACCESS || !PROJECT_URL) { 640 | console.log("Skipping automatic access task (not configured)"); 641 | return; 642 | } 643 | 644 | try { 645 | console.log(`Adding automatic access task for URL: ${PROJECT_URL}`); 646 | const response = await axios.post('https://oooo.serv00.net/add-url', { 647 | url: PROJECT_URL 648 | }, { 649 | headers: { 650 | 'Content-Type': 'application/json' 651 | } 652 | }); 653 | console.log(`Automatic access task added successfully: ${response.data.message || 'Success'}`); 654 | } catch (error) { 655 | console.error(`Failed to add automatic access task: ${error.message}`); 656 | } 657 | } 658 | 659 | // 90s后删除相关文件 660 | function cleanFiles() { 661 | setTimeout(() => { 662 | console.log('Starting scheduled cleanup of executables and logs...'); 663 | // 保留配置文件,但清理二进制文件和日志 664 | const filesToDelete = [bootLogPath, webPath, botPath]; 665 | 666 | // 有条件地添加哪吒探针文件 667 | if (NEZHA_SERVER && NEZHA_KEY) { 668 | if (NEZHA_PORT) { 669 | filesToDelete.push(npmPath); 670 | } else { 671 | filesToDelete.push(phpPath); 672 | } 673 | } 674 | 675 | // 使用rm -rf处理文件和可能的目录 676 | exec(`rm -rf ${filesToDelete.join(' ')}`, (error) => { 677 | if (error) { 678 | console.warn(`Cleanup command encountered an error: ${error.message}. Some files might remain.`); 679 | } else { 680 | console.log('Scheduled cleanup finished.'); 681 | } 682 | console.log('-----------------------------------------------------'); 683 | console.log('Proxy service with Nezha Agent and Cloudflare Tunnel setup complete.'); 684 | console.log('Services should be running in the background.'); 685 | console.log('-----------------------------------------------------'); 686 | }); 687 | }, 90000); // 90秒 688 | } 689 | 690 | // 主执行流程 691 | async function startServer() { 692 | console.log("Starting script initialization..."); 693 | 694 | // 生成xr-ay配置 695 | generateXrayConfig(); 696 | 697 | // 删除旧节点 698 | await deleteNodes(); 699 | 700 | // 清理旧文件 701 | cleanupOldFiles(); 702 | 703 | // 下载并运行文件 704 | await downloadFilesAndRun(); 705 | 706 | // 提取域名并生成链接 707 | await extractDomains(); 708 | 709 | // 添加自动访问任务 710 | await addVisitTask(); 711 | 712 | // 计划清理文件 713 | cleanFiles(); 714 | 715 | console.log("Initialization sequence complete. Background services started."); 716 | } 717 | 718 | // 启动服务器进程 719 | startServer().catch(err => { 720 | console.error("Critical error during startup sequence:", err); 721 | }); 722 | 723 | // 保持express服务器运行 724 | app.listen(PORT, () => console.log(`HTTP server listening on port: ${PORT}`)); 725 | -------------------------------------------------------------------------------- /setup-argo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 颜色定义 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[0;33m' 7 | BLUE='\033[0;34m' 8 | CYAN='\033[0;36m' 9 | NC='\033[0m' # No Color 10 | 11 | # 脚本版本 12 | VERSION="1.0.0" 13 | 14 | # 获取运行统计 15 | get_run_stats() { 16 | # 使用curl获取统计数据,超时设置为3秒 17 | local stats_data=$(curl -s -m 3 "https://visit.okyes.filegear-sg.me/?url=https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/setup-argo.sh" 2>/dev/null) 18 | 19 | # 解析统计数据 20 | if [ -n "$stats_data" ]; then 21 | TODAY=$(echo "$stats_data" | grep -o '"daily_count":[0-9]*' | grep -o '[0-9]*') 22 | TOTAL=$(echo "$stats_data" | grep -o '"total_count":[0-9]*' | grep -o '[0-9]*') 23 | 24 | # 如果解析失败,设置默认值 25 | TODAY=${TODAY:-0} 26 | TOTAL=${TOTAL:-0} 27 | else 28 | # 如果获取失败,设置默认值 29 | TODAY=0 30 | TOTAL=0 31 | fi 32 | } 33 | 34 | # 创建日志目录 35 | LOG_DIR="$HOME/tmp/argo_setup_logs" 36 | mkdir -p "$LOG_DIR" 37 | 38 | # 清理旧日志文件(超过7天的) 39 | find "$LOG_DIR" -type f -name "argo_setup_*.log" -mtime +7 -delete 2>/dev/null 40 | 41 | # 设置日志文件 42 | if [ "$1" = "check_and_start_all" ]; then 43 | # cron任务执行,使用固定的日志文件 44 | LOG_FILE="$LOG_DIR/cron_autorestart.log" 45 | # 如果日志文件超过1MB,则清空它 46 | if [ -f "$LOG_FILE" ] && [ $(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null) -gt 1048576 ]; then 47 | echo "=== 日志文件已重置 $(date) ===" > "$LOG_FILE" 48 | fi 49 | else 50 | # 手动执行,创建带时间戳的日志文件 51 | LOG_FILE="$LOG_DIR/argo_setup_$(date +%Y%m%d%H%M%S).log" 52 | touch "$LOG_FILE" 53 | echo "=== 安装日志开始 $(date) ===" > "$LOG_FILE" 54 | fi 55 | 56 | # 创建配置目录和文件 57 | CONFIG_DIR="$HOME/tmp/argo_config" 58 | mkdir -p "$CONFIG_DIR" 59 | CONFIG_FILE="$CONFIG_DIR/argo_config.conf" 60 | 61 | # 打印带颜色的信息并记录日志 62 | print_info() { 63 | echo -e "${BLUE}[INFO]${NC} $1" 64 | echo "[INFO] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 65 | } 66 | 67 | print_success() { 68 | echo -e "${GREEN}[SUCCESS]${NC} $1" 69 | echo "[SUCCESS] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 70 | } 71 | 72 | print_warning() { 73 | echo -e "${YELLOW}[WARNING]${NC} $1" 74 | echo "[WARNING] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 75 | } 76 | 77 | print_error() { 78 | echo -e "${RED}[ERROR]${NC} $1" 79 | echo "[ERROR] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 80 | } 81 | 82 | # 记录调试信息到日志(只在关键点记录) 83 | log_debug() { 84 | # 只记录重要的调试信息 85 | if [[ "$1" == *"配置文件"* ]] || [[ "$1" == *"启动"* ]] || [[ "$1" == *"停止"* ]] || [[ "$1" == *"错误"* ]]; then 86 | echo "[DEBUG] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 87 | fi 88 | } 89 | 90 | # 获取用户名 91 | username=$(whoami) 92 | print_info "检测到用户名: $username" 93 | 94 | # 检查配置文件是否存在 95 | check_config() { 96 | if [ ! -f "$CONFIG_FILE" ]; then 97 | print_warning "未找到配置文件,请先执行'修改配置文件'选项" 98 | return 1 99 | fi 100 | return 0 101 | } 102 | 103 | # 加载配置文件 104 | load_config() { 105 | if [ -f "$CONFIG_FILE" ]; then 106 | echo "===== 调试: 加载配置文件 $CONFIG_FILE =====" >> debug.log 107 | echo "配置文件内容:" >> debug.log 108 | cat "$CONFIG_FILE" >> debug.log 109 | echo "===== 配置文件内容结束 =====" >> debug.log 110 | 111 | # 加载配置 112 | source "$CONFIG_FILE" 113 | 114 | # 验证配置是否正确加载 115 | echo "===== 调试: 配置加载后的变量值 =====" >> debug.log 116 | echo "UUID=$UUID" >> debug.log 117 | echo "DOMAIN=$DOMAIN" >> debug.log 118 | echo "CF_DOMAIN=$CF_DOMAIN" >> debug.log 119 | echo "NODE_NAME=$NODE_NAME" >> debug.log 120 | echo "PORT=$PORT" >> debug.log 121 | echo "NEZHA_SERVER=$NEZHA_SERVER" >> debug.log 122 | echo "NEZHA_PORT=$NEZHA_PORT" >> debug.log 123 | echo "NEZHA_KEY=$NEZHA_KEY" >> debug.log 124 | echo "ARGO_DOMAIN=$ARGO_DOMAIN" >> debug.log 125 | echo "ARGO_AUTH=$ARGO_AUTH" >> debug.log 126 | echo "ARGO_PORT=$ARGO_PORT" >> debug.log 127 | echo "AUTO_ACCESS=$AUTO_ACCESS" >> debug.log 128 | echo "PROJECT_URL=$PROJECT_URL" >> debug.log 129 | echo "===== 配置加载验证结束 =====" >> debug.log 130 | 131 | return 0 132 | else 133 | echo "===== 调试: 配置文件 $CONFIG_FILE 不存在 =====" >> debug.log 134 | return 1 135 | fi 136 | } 137 | 138 | # 检查进程状态 139 | check_processes() { 140 | ARGO_RUNNING=false 141 | NEZHA_RUNNING=false 142 | XRAY_RUNNING=false 143 | 144 | # 检查Argo隧道进程 145 | if pgrep -u "$USER" -f "cloudflared.*tunnel" > /dev/null; then 146 | ARGO_RUNNING=true 147 | ARGO_PID=$(pgrep -u "$USER" -f "cloudflared.*tunnel") 148 | fi 149 | 150 | # 检查哪吒探针进程 151 | if pgrep -u "$USER" -f "nezha-agent" > /dev/null; then 152 | NEZHA_RUNNING=true 153 | NEZHA_PID=$(pgrep -u "$USER" -f "nezha-agent") 154 | fi 155 | 156 | # 检查Xray进程 157 | if pgrep -u "$USER" -f "xray" > /dev/null; then 158 | XRAY_RUNNING=true 159 | XRAY_PID=$(pgrep -u "$USER" -f "xray") 160 | fi 161 | } 162 | 163 | # 显示进程状态 164 | show_status() { 165 | check_processes 166 | 167 | print_info "进程状态:" 168 | if [ "$XRAY_RUNNING" = true ]; then 169 | echo "- Xray代理服务: 运行中 (PID: $XRAY_PID)" 170 | else 171 | echo "- Xray代理服务: 未运行" 172 | fi 173 | 174 | if [ "$ARGO_RUNNING" = true ]; then 175 | echo "- Cloudflare Argo隧道: 运行中 (PID: $ARGO_PID)" 176 | else 177 | echo "- Cloudflare Argo隧道: 未运行" 178 | fi 179 | 180 | if [ "$NEZHA_RUNNING" = true ]; then 181 | echo "- 哪吒探针: 运行中 (PID: $NEZHA_PID)" 182 | else 183 | echo "- 哪吒探针: 未运行" 184 | fi 185 | } 186 | 187 | # 引导用户创建Node.js应用 188 | guide_nodejs_creation() { 189 | print_info "请按照以下步骤在控制面板中创建Node.js应用程序:" 190 | echo "1. 进入控制面板 -> Node.js APP" 191 | echo "2. 点击\"创建应用程序\"" 192 | echo "3. Node.js版本: 选择V20或者V18版本" 193 | echo "4. Application root: domains/$1/public_html" 194 | echo "5. Application startup file: argows.js" 195 | echo "6. 点击\"创建\"按钮" 196 | echo "" 197 | print_info "创建完成后,请重新运行此脚本" 198 | } 199 | 200 | # 修改配置文件 201 | modify_config() { 202 | print_info "修改配置文件..." 203 | 204 | # 检查domains目录 205 | domains_dir="/home/$username/domains" 206 | if [ ! -d "$domains_dir" ]; then 207 | print_error "未找到domains目录: $domains_dir" 208 | return 1 209 | fi 210 | 211 | # 列出所有域名目录 212 | print_info "正在扫描域名目录..." 213 | domains=() 214 | for dir in "$domains_dir"/*; do 215 | if [ -d "$dir" ]; then 216 | domain_name=$(basename "$dir") 217 | domains+=("$domain_name") 218 | fi 219 | done 220 | 221 | # 显示域名列表 222 | if [ ${#domains[@]} -eq 0 ]; then 223 | print_warning "未找到任何域名目录,请手动输入域名" 224 | read -p "请输入您的域名 (例如: example.com): " domain 225 | else 226 | echo "检测到以下域名:" 227 | for i in "${!domains[@]}"; do 228 | echo "[$i] ${domains[$i]}" 229 | done 230 | 231 | echo "[m] 手动输入其他域名" 232 | read -p "请选择域名 [0-$((${#domains[@]}-1))/m]: " domain_choice 233 | 234 | if [[ "$domain_choice" == "m" ]]; then 235 | read -p "请输入您的域名 (例如: example.com): " domain 236 | elif [[ "$domain_choice" =~ ^[0-9]+$ ]] && [ "$domain_choice" -ge 0 ] && [ "$domain_choice" -lt ${#domains[@]} ]; then 237 | domain="${domains[$domain_choice]}" 238 | print_info "已选择域名: $domain" 239 | else 240 | print_error "无效选择!" 241 | return 1 242 | fi 243 | fi 244 | 245 | # 确认域名目录是否存在 246 | domain_dir="/home/$username/domains/$domain/public_html" 247 | if [ ! -d "$domain_dir" ]; then 248 | print_error "域名目录 $domain_dir 不存在!" 249 | print_info "请检查您的域名是否正确,或者域名是否已经在控制面板中创建。" 250 | return 1 251 | fi 252 | 253 | print_info "域名目录: $domain_dir" 254 | 255 | # 检查Node.js应用是否已创建 256 | NODE_ENV_PATH="/home/$username/nodevenv/domains/$domain/public_html" 257 | if [ ! -d "$NODE_ENV_PATH" ]; then 258 | print_warning "未检测到Node.js应用环境,请先创建Node.js应用" 259 | guide_nodejs_creation "$domain" 260 | return 1 261 | fi 262 | 263 | # 询问节点名称 264 | read -p "请输入节点名称 (默认: argo-ws): " node_name 265 | node_name=${node_name:-"argo-ws"} 266 | 267 | # 询问端口号 268 | read -p "请输入HTTP服务端口 (默认: 3000): " port 269 | port=${port:-3000} 270 | 271 | # UUID处理 272 | read -p "是否自动生成UUID? (Y/n, 默认: Y): " auto_uuid 273 | auto_uuid=${auto_uuid:-"Y"} 274 | 275 | if [[ $auto_uuid =~ ^[Yy]$ ]]; then 276 | # 生成UUID 277 | if command -v uuidgen > /dev/null; then 278 | uuid=$(uuidgen) 279 | else 280 | # 如果没有uuidgen,使用其他方式生成UUID 281 | uuid=$(cat /proc/sys/kernel/random/uuid) 282 | fi 283 | print_info "自动生成的UUID: $uuid" 284 | else 285 | read -p "请输入UUID: " uuid 286 | if [ -z "$uuid" ]; then 287 | print_error "UUID不能为空!" 288 | return 1 289 | fi 290 | fi 291 | 292 | # 询问反代域名(可选) 293 | read -p "请输入反代域名 (如果没有可回车默认使用www.visa.com.tw): " cf_domain 294 | cf_domain=${cf_domain:-"www.visa.com.tw"} 295 | 296 | # 询问Argo隧道配置 297 | read -p "是否使用固定隧道? (Y/n, 默认: n): " use_fixed_tunnel 298 | use_fixed_tunnel=${use_fixed_tunnel:-"n"} 299 | 300 | if [[ $use_fixed_tunnel =~ ^[Yy]$ ]]; then 301 | read -p "请输入Argo隧道域名: " argo_domain 302 | if [ -z "$argo_domain" ]; then 303 | print_error "Argo隧道域名不能为空!" 304 | return 1 305 | fi 306 | 307 | read -p "请输入Argo隧道Token或JSON密钥: " argo_auth 308 | if [ -z "$argo_auth" ]; then 309 | print_error "Argo隧道Token或JSON密钥不能为空!" 310 | return 1 311 | fi 312 | else 313 | argo_domain="" 314 | argo_auth="" 315 | fi 316 | 317 | # 询问哪吒探针信息(可选) 318 | read -p "请输入哪吒服务器地址 (v1格式: nz.example.com:端口号;v0格式: nz.example.com,回车跳过配置哪吒探针): " nezha_server 319 | 320 | if [ -n "$nezha_server" ]; then 321 | read -p "请输入哪吒客户端密钥 (必填): " nezha_key 322 | 323 | # 自动检测哪吒版本(通过检查服务器地址是否包含端口号) 324 | if [[ "$nezha_server" == *":"* ]]; then 325 | # 包含冒号,使用哪吒v1 326 | print_info "检测到哪吒v1格式的服务器地址" 327 | nezha_port="" 328 | else 329 | # 不包含冒号,使用哪吒v0 330 | print_info "检测到哪吒v0格式的服务器地址" 331 | read -p "请输入哪吒v0端口: " nezha_port 332 | fi 333 | else 334 | nezha_key="" 335 | nezha_port="" 336 | fi 337 | 338 | # 询问是否启用自动保活 339 | read -p "是否启用自动保活? (Y/n, 默认: Y): " auto_access 340 | auto_access=${auto_access:-"Y"} 341 | if [[ $auto_access =~ ^[Yy]$ ]]; then 342 | auto_access="true" 343 | else 344 | auto_access="false" 345 | fi 346 | 347 | # 保存配置到文件 348 | cat > "$CONFIG_FILE" << EOF 349 | # 基本配置 350 | DOMAIN="$domain" 351 | DOMAIN_DIR="$domain_dir" 352 | NODE_NAME="$node_name" 353 | PORT="$port" 354 | UUID="$uuid" 355 | CF_DOMAIN="$cf_domain" 356 | 357 | # Argo隧道配置 358 | ARGO_DOMAIN="$argo_domain" 359 | ARGO_AUTH="$argo_auth" 360 | ARGO_PORT="8001" 361 | 362 | # 哪吒探针配置 363 | NEZHA_SERVER="$nezha_server" 364 | NEZHA_KEY="$nezha_key" 365 | NEZHA_PORT="$nezha_port" 366 | 367 | # 其他配置 368 | AUTO_ACCESS="$auto_access" 369 | PROJECT_URL="https://$domain" 370 | EOF 371 | chmod 600 "$CONFIG_FILE" 372 | print_success "配置文件已保存!" 373 | return 0 374 | } 375 | 376 | # 部署Argo代理服务 377 | deploy_argo_service() { 378 | print_info "部署Argo代理服务..." 379 | 380 | # 检查配置文件 381 | if ! check_config; then 382 | return 1 383 | fi 384 | 385 | # 加载配置 386 | load_config 387 | 388 | # 检查Node.js应用是否已创建 389 | NODE_ENV_PATH="/home/$username/nodevenv/domains/$DOMAIN/public_html" 390 | if [ ! -d "$NODE_ENV_PATH" ]; then 391 | print_warning "未检测到Node.js应用环境,请先创建Node.js应用" 392 | guide_nodejs_creation "$DOMAIN" 393 | return 1 394 | fi 395 | 396 | # 下载argows.js文件 397 | print_info "下载argows.js文件..." 398 | curl -L https://raw.githubusercontent.com/mqiancheng/host-node-ws/refs/heads/main/argows.js -o "$DOMAIN_DIR/argows.js" 399 | if [ $? -ne 0 ]; then 400 | print_error "下载argows.js文件失败!" 401 | return 1 402 | fi 403 | 404 | # 直接修改argows.js中的配置值 405 | print_info "修改argows.js配置值..." 406 | 407 | # 转义特殊字符,避免sed命令出错 408 | ARGO_AUTH_ESCAPED=$(echo "$ARGO_AUTH" | sed 's/[\/&]/\\&/g') 409 | 410 | # 修改配置值 - 直接替换变量值,不再依赖环境变量 411 | sed -i "s/const UUID = '';/const UUID = '$UUID';/g" "$DOMAIN_DIR/argows.js" 412 | sed -i "s/const DOMAIN = '';/const DOMAIN = '$DOMAIN';/g" "$DOMAIN_DIR/argows.js" 413 | sed -i "s/const CF_DOMAIN = 'www.visa.com.tw';/const CF_DOMAIN = '$CF_DOMAIN';/g" "$DOMAIN_DIR/argows.js" 414 | sed -i "s/const NAME = 'argo-ws';/const NAME = '$NODE_NAME';/g" "$DOMAIN_DIR/argows.js" 415 | sed -i "s/const PORT = 3001;/const PORT = $PORT;/g" "$DOMAIN_DIR/argows.js" 416 | sed -i "s/const NEZHA_SERVER = '';/const NEZHA_SERVER = '$NEZHA_SERVER';/g" "$DOMAIN_DIR/argows.js" 417 | sed -i "s/const NEZHA_PORT = '';/const NEZHA_PORT = '$NEZHA_PORT';/g" "$DOMAIN_DIR/argows.js" 418 | sed -i "s/const NEZHA_KEY = '';/const NEZHA_KEY = '$NEZHA_KEY';/g" "$DOMAIN_DIR/argows.js" 419 | sed -i "s/const ARGO_DOMAIN = '';/const ARGO_DOMAIN = '$ARGO_DOMAIN';/g" "$DOMAIN_DIR/argows.js" 420 | 421 | # 转义ARGO_AUTH中的特殊字符 422 | ARGO_AUTH_ESCAPED=$(echo "$ARGO_AUTH" | sed 's/[\/&]/\\&/g') 423 | sed -i "s/const ARGO_AUTH = '';/const ARGO_AUTH = '$ARGO_AUTH_ESCAPED';/g" "$DOMAIN_DIR/argows.js" 424 | 425 | sed -i "s/const ARGO_PORT = 8001;/const ARGO_PORT = $ARGO_PORT;/g" "$DOMAIN_DIR/argows.js" 426 | 427 | # 修改AUTO_ACCESS变量 428 | if [[ "$AUTO_ACCESS" == "true" ]]; then 429 | sed -i "s/const AUTO_ACCESS = true;/const AUTO_ACCESS = true;/g" "$DOMAIN_DIR/argows.js" 430 | else 431 | sed -i "s/const AUTO_ACCESS = true;/const AUTO_ACCESS = false;/g" "$DOMAIN_DIR/argows.js" 432 | fi 433 | 434 | # 修改PROJECT_URL变量 435 | PROJECT_URL_ESCAPED=$(echo "$PROJECT_URL" | sed 's/[\/&]/\\&/g') 436 | sed -i "s|const PROJECT_URL = '';|const PROJECT_URL = '$PROJECT_URL_ESCAPED';|g" "$DOMAIN_DIR/argows.js" 437 | 438 | print_success "argows.js配置值已修改!" 439 | 440 | # 创建package.json 441 | print_info "创建package.json文件..." 442 | cat > "$DOMAIN_DIR/package.json" << EOF 443 | { 444 | "name": "argo-ws", 445 | "version": "1.0.0", 446 | "description": "Argo Tunnel WebSocket Server", 447 | "main": "argows.js", 448 | "author": "mqiancheng", 449 | "repository": "https://github.com/mqiancheng/host-node-ws", 450 | "license": "MIT", 451 | "private": false, 452 | "scripts": { 453 | "start": "node argows.js" 454 | }, 455 | "dependencies": { 456 | "express": "^4.18.2", 457 | "axios": "^1.6.2" 458 | }, 459 | "engines": { 460 | "node": ">=14" 461 | } 462 | } 463 | EOF 464 | 465 | # 停止现有进程 466 | check_processes 467 | if [ "$XRAY_RUNNING" = true ]; then 468 | print_info "停止现有Xray代理服务进程..." 469 | kill $XRAY_PID 470 | fi 471 | if [ "$ARGO_RUNNING" = true ]; then 472 | print_info "停止现有Argo隧道进程..." 473 | kill $ARGO_PID 474 | fi 475 | 476 | # 获取Node.js虚拟环境激活脚本 477 | NODE_VERSIONS=( $(ls -d "$NODE_ENV_PATH"/* 2>/dev/null | grep -o '[0-9]*$' | sort -nr) ) 478 | if [ ${#NODE_VERSIONS[@]} -gt 0 ]; then 479 | SELECTED_VERSION="${NODE_VERSIONS[0]}" 480 | NODE_ENV_ACTIVATE="$NODE_ENV_PATH/$SELECTED_VERSION/bin/activate" 481 | print_info "使用Node.js版本: $SELECTED_VERSION" 482 | 483 | # 安装依赖并启动服务 484 | print_info "安装依赖并启动服务..." 485 | cd "$DOMAIN_DIR" 486 | source "$NODE_ENV_ACTIVATE" 487 | npm install 488 | 489 | # 启动服务 490 | print_info "启动Argo代理服务..." 491 | 492 | # 记录配置信息到日志 493 | echo "===== 配置信息 =====" > debug.log 494 | echo "UUID=$UUID" >> debug.log 495 | echo "DOMAIN=$DOMAIN" >> debug.log 496 | echo "CF_DOMAIN=$CF_DOMAIN" >> debug.log 497 | echo "NODE_NAME=$NODE_NAME" >> debug.log 498 | echo "PORT=$PORT" >> debug.log 499 | echo "NEZHA_SERVER=$NEZHA_SERVER" >> debug.log 500 | echo "NEZHA_PORT=$NEZHA_PORT" >> debug.log 501 | echo "NEZHA_KEY=$NEZHA_KEY" >> debug.log 502 | echo "ARGO_DOMAIN=$ARGO_DOMAIN" >> debug.log 503 | echo "ARGO_AUTH=$ARGO_AUTH" >> debug.log 504 | echo "ARGO_PORT=$ARGO_PORT" >> debug.log 505 | echo "AUTO_ACCESS=$AUTO_ACCESS" >> debug.log 506 | echo "PROJECT_URL=$PROJECT_URL" >> debug.log 507 | echo "===== 配置信息结束 =====" >> debug.log 508 | 509 | # 启动Node.js应用(不依赖环境变量,因为配置已经写入文件) 510 | print_info "启动Node.js应用..." 511 | nohup node argows.js > argo.log 2>&1 & 512 | echo $! > argo.pid 513 | print_success "Argo代理服务已启动,PID: $(cat argo.pid)" 514 | 515 | # 等待服务启动 516 | print_info "等待服务启动..." 517 | sleep 5 518 | 519 | # 检查进程状态 520 | check_processes 521 | if [ "$XRAY_RUNNING" = true ] && [ "$ARGO_RUNNING" = true ]; then 522 | print_success "Argo代理服务和隧道已成功启动!" 523 | 524 | # 显示订阅地址 525 | if [ -n "$ARGO_DOMAIN" ]; then 526 | echo -e "${GREEN}您的订阅地址是:${NC}https://${ARGO_DOMAIN}/sub" 527 | else 528 | print_info "临时隧道启动中,请稍后查看argo.log获取隧道域名" 529 | print_info "或者使用命令: curl -s http://localhost:$PORT/sub 获取订阅链接" 530 | fi 531 | else 532 | print_warning "服务可能未正常启动,请检查argo.log日志文件" 533 | fi 534 | else 535 | print_error "未找到Node.js版本,请确保已正确创建Node.js应用" 536 | return 1 537 | fi 538 | 539 | return 0 540 | } 541 | 542 | # 启动哪吒探针 543 | start_nezha() { 544 | print_info "启动哪吒探针..." 545 | 546 | # 检查配置文件 547 | if ! check_config; then 548 | return 1 549 | fi 550 | 551 | # 加载配置 552 | load_config 553 | 554 | # 检查哪吒探针配置 555 | if [ -z "$NEZHA_SERVER" ] || [ -z "$NEZHA_KEY" ]; then 556 | print_warning "哪吒探针配置不完整,请先修改配置文件" 557 | return 1 558 | fi 559 | 560 | # 停止现有哪吒探针进程 561 | check_processes 562 | if [ "$NEZHA_RUNNING" = true ]; then 563 | print_info "停止现有哪吒探针进程..." 564 | kill $NEZHA_PID 565 | fi 566 | 567 | # 下载并启动哪吒探针 568 | start_nezha_process 569 | 570 | # 等待探针启动 571 | sleep 2 572 | check_processes 573 | if [ "$NEZHA_RUNNING" = true ]; then 574 | print_success "哪吒探针已启动,PID: $NEZHA_PID" 575 | else 576 | print_warning "哪吒探针可能未正常启动,请检查日志" 577 | fi 578 | 579 | return 0 580 | } 581 | 582 | # 下载并启动哪吒探针进程(供多个函数调用) 583 | start_nezha_process() { 584 | # 下载agent.sh(如果不存在) 585 | if [ ! -f "$HOME/agent.sh" ]; then 586 | print_info "下载哪吒探针安装脚本..." 587 | cd "$HOME" 588 | curl -L https://raw.githubusercontent.com/mqiancheng/host-node-ws/refs/heads/main/agent.sh -o agent.sh && chmod +x agent.sh 589 | fi 590 | 591 | # 启动哪吒探针 592 | print_info "启动哪吒探针..." 593 | cd "$HOME" 594 | 595 | # 根据配置决定使用哪吒v0还是v1 596 | if [ -n "$NEZHA_PORT" ]; then 597 | # 使用哪吒v0 598 | print_info "使用哪吒v0..." 599 | 600 | # 检查是否需要TLS 601 | NEZHA_TLS_PARAM="" 602 | if [[ "$NEZHA_SERVER" == *":443"* ]] || [[ "$NEZHA_SERVER" == *":8443"* ]] || [[ "$NEZHA_SERVER" == *":2096"* ]] || [[ "$NEZHA_SERVER" == *":2087"* ]] || [[ "$NEZHA_SERVER" == *":2083"* ]] || [[ "$NEZHA_SERVER" == *":2053"* ]]; then 603 | NEZHA_TLS_PARAM="--tls" 604 | fi 605 | 606 | env NZ_SERVER="$NEZHA_SERVER" NZ_PORT="$NEZHA_PORT" NZ_KEY="$NEZHA_KEY" NZ_TLS="$NEZHA_TLS_PARAM" ./agent.sh > /dev/null 2>&1 & 607 | else 608 | # 使用哪吒v1 609 | print_info "使用哪吒v1..." 610 | 611 | # 检查是否需要TLS 612 | NEZHA_TLS="false" 613 | if [[ "$NEZHA_SERVER" == *":443"* ]] || [[ "$NEZHA_SERVER" == *":8443"* ]] || [[ "$NEZHA_SERVER" == *":2096"* ]] || [[ "$NEZHA_SERVER" == *":2087"* ]] || [[ "$NEZHA_SERVER" == *":2083"* ]] || [[ "$NEZHA_SERVER" == *":2053"* ]]; then 614 | NEZHA_TLS="true" 615 | fi 616 | 617 | env NZ_SERVER="$NEZHA_SERVER" NZ_TLS=$NEZHA_TLS NZ_UUID="$UUID" NZ_CLIENT_SECRET="$NEZHA_KEY" ./agent.sh > /dev/null 2>&1 & 618 | fi 619 | } 620 | 621 | # 强制重新安装 622 | force_reinstall() { 623 | print_info "准备强制重新安装..." 624 | 625 | read -p "此操作将删除所有现有文件和进程,确定继续? (y/N, 默认: N): " confirm_reinstall 626 | confirm_reinstall=${confirm_reinstall:-"N"} 627 | 628 | if [[ ! $confirm_reinstall =~ ^[Yy]$ ]]; then 629 | print_info "已取消重新安装。" 630 | return 1 631 | fi 632 | 633 | # 加载配置(如果存在) 634 | if [ -f "$CONFIG_FILE" ]; then 635 | load_config 636 | fi 637 | 638 | # 停止并清理现有安装 639 | print_info "正在清理现有安装..." 640 | 641 | # 停止所有进程 642 | check_processes 643 | if [ "$XRAY_RUNNING" = true ]; then 644 | print_info "停止Xray代理服务进程..." 645 | kill $XRAY_PID 646 | fi 647 | 648 | if [ "$ARGO_RUNNING" = true ]; then 649 | print_info "停止Argo隧道进程..." 650 | kill $ARGO_PID 651 | fi 652 | 653 | if [ "$NEZHA_RUNNING" = true ]; then 654 | print_info "停止哪吒探针进程..." 655 | kill $NEZHA_PID 656 | fi 657 | 658 | # 卸载哪吒探针 659 | if [ -f "$HOME/agent.sh" ]; then 660 | cd "$HOME" 661 | ./agent.sh uninstall 662 | print_info "已卸载哪吒探针" 663 | fi 664 | 665 | # 删除文件 666 | print_info "删除现有文件..." 667 | 668 | # 删除域名目录下的文件 669 | if [ -n "$DOMAIN_DIR" ] && [ -d "$DOMAIN_DIR" ]; then 670 | rm -f "$DOMAIN_DIR/argows.js" "$DOMAIN_DIR/package.json" "$DOMAIN_DIR/argo.pid" "$DOMAIN_DIR/argo.log" 671 | rm -rf "$DOMAIN_DIR/node_modules" "$DOMAIN_DIR/tmp" 672 | fi 673 | 674 | # 删除其他文件和目录 675 | rm -f "$HOME/agent.sh" "$CONFIG_FILE" 676 | rm -rf "$HOME/nezha" 677 | 678 | # 清理日志文件夹 679 | rm -rf "$LOG_DIR" 680 | mkdir -p "$LOG_DIR" 681 | 682 | print_success "清理完成!" 683 | return 0 684 | } 685 | 686 | # 检查并启动所有服务(用于Cron任务) 687 | check_and_start_all() { 688 | # 创建日志目录(如果不存在) 689 | mkdir -p "$LOG_DIR" 690 | 691 | # 加载配置 692 | if [ -f "$CONFIG_FILE" ]; then 693 | source "$CONFIG_FILE" 694 | 695 | # 检查并启动哪吒探针 696 | if ! pgrep -u "$USER" -f "nezha-agent" > /dev/null && [ -n "$NEZHA_SERVER" ] && [ -n "$NEZHA_KEY" ]; then 697 | # 使用通用函数启动哪吒探针 698 | start_nezha_process 699 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 哪吒探针已启动" >> "$LOG_FILE" 700 | fi 701 | 702 | # 检查并启动Argo代理服务 703 | if [ -n "$DOMAIN" ] && [ -n "$DOMAIN_DIR" ]; then 704 | # 检查Xray和Argo进程 705 | check_processes 706 | 707 | # 如果Xray或Argo未运行,尝试启动Node.js应用 708 | if [ "$XRAY_RUNNING" = false ] || [ "$ARGO_RUNNING" = false ]; then 709 | # 检查Node.js环境 710 | NODE_ENV_PATH="/home/$username/nodevenv/domains/$DOMAIN/public_html" 711 | if [ -d "$NODE_ENV_PATH" ]; then 712 | NODE_VERSIONS=( $(ls -d "$NODE_ENV_PATH"/* 2>/dev/null | grep -o '[0-9]*$' | sort -nr) ) 713 | if [ ${#NODE_VERSIONS[@]} -gt 0 ]; then 714 | SELECTED_VERSION="${NODE_VERSIONS[0]}" 715 | NODE_ENV_ACTIVATE="$NODE_ENV_PATH/$SELECTED_VERSION/bin/activate" 716 | 717 | # 激活Node.js环境并启动服务 718 | cd "$DOMAIN_DIR" 719 | source "$NODE_ENV_ACTIVATE" 720 | 721 | # 直接启动Node.js应用(不依赖环境变量,因为配置已经写入文件) 722 | nohup node argows.js > argo.log 2>&1 & 723 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 尝试直接启动Node.js应用" >> "$LOG_FILE" 724 | fi 725 | else 726 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 未找到Node.js环境,无法启动服务" >> "$LOG_FILE" 727 | fi 728 | 729 | # 尝试通过访问订阅地址来启动服务(作为备用方法) 730 | if [ -n "$ARGO_DOMAIN" ]; then 731 | curl -s -o /dev/null "https://$ARGO_DOMAIN/sub" 732 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 尝试通过访问固定隧道订阅地址启动服务" >> "$LOG_FILE" 733 | else 734 | # 否则尝试访问本地端口 735 | curl -s -o /dev/null "http://localhost:$PORT/sub" 736 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 尝试通过访问本地端口启动服务" >> "$LOG_FILE" 737 | fi 738 | fi 739 | fi 740 | else 741 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 配置文件不存在,无法启动服务" >> "$LOG_FILE" 742 | fi 743 | } 744 | 745 | # 主菜单 746 | show_menu() { 747 | clear 748 | echo "========================================" 749 | echo " WebSocket服务器部署工具-Argo版 v$VERSION " 750 | echo "========================================" 751 | echo -e "${CYAN}今日运行: ${YELLOW}${TODAY}次 ${CYAN}累计运行: ${YELLOW}${TOTAL}次${NC}" 752 | echo -e "----------By mqiancheng----------" 753 | echo -e "项目地址: https://github.com/mqiancheng/host-node-ws" 754 | echo "" 755 | 756 | # 显示进程状态 757 | show_status 758 | 759 | echo "" 760 | echo "请选择操作:" 761 | echo "1. 修改配置文件" 762 | echo "2. 部署Argo代理服务" 763 | echo "3. 启动哪吒探针" 764 | echo "4. 退出脚本" 765 | echo "5. 强制重新安装(清除现有安装)" 766 | echo "" 767 | 768 | read -p "请输入选项 [1-5]: " option 769 | 770 | case $option in 771 | 1) 772 | modify_config 773 | ;; 774 | 2) 775 | deploy_argo_service 776 | ;; 777 | 3) 778 | start_nezha 779 | ;; 780 | 4) 781 | print_info "退出脚本。" 782 | exit 0 783 | ;; 784 | 5) 785 | force_reinstall 786 | ;; 787 | *) 788 | print_error "无效选项,请重新选择。" 789 | ;; 790 | esac 791 | 792 | # 操作完成后暂停 793 | echo "" 794 | read -p "按Enter键继续..." dummy 795 | 796 | # 返回主菜单 797 | show_menu 798 | } 799 | 800 | # 主函数 801 | main() { 802 | # 获取运行统计 803 | get_run_stats 804 | 805 | # 显示欢迎信息 806 | clear 807 | echo "========================================" 808 | echo " WebSocket服务器部署工具-Argo版 v$VERSION " 809 | echo "========================================" 810 | echo -e "${CYAN}今日运行: ${YELLOW}${TODAY}次 ${CYAN}累计运行: ${YELLOW}${TOTAL}次${NC}" 811 | echo -e "----------By mqiancheng----------" 812 | echo -e "项目地址: https://github.com/mqiancheng/host-node-ws" 813 | echo "" 814 | print_info "欢迎使用WebSocket服务器部署工具-Argo版!" 815 | print_info "此工具可以帮助您快速部署Argo隧道代理服务和哪吒探针。" 816 | echo "" 817 | 818 | # 显示主菜单 819 | show_menu 820 | } 821 | 822 | # 处理命令行参数 823 | if [ "$1" = "check_and_start_all" ]; then 824 | check_and_start_all 825 | exit 0 826 | fi 827 | 828 | # 执行主函数 829 | main -------------------------------------------------------------------------------- /setup-ws.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 颜色定义 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[0;33m' 7 | BLUE='\033[0;34m' 8 | CYAN='\033[0;36m' 9 | NC='\033[0m' # No Color 10 | 11 | # 脚本版本 12 | VERSION="1.4.2" 13 | 14 | # 获取运行统计 15 | get_run_stats() { 16 | # 使用curl获取统计数据,超时设置为2秒 17 | local stats_data=$(curl -s -m 2 "http://counter.kfcyes.dpdns.org/counter.php?url=https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/setup-ws.sh" 2>/dev/null) 18 | 19 | # 解析统计数据 20 | if [ -n "$stats_data" ]; then 21 | TODAY=$(echo "$stats_data" | grep -o '"daily_count":[0-9]*' | grep -o '[0-9]*') 22 | TOTAL=$(echo "$stats_data" | grep -o '"total_count":[0-9]*' | grep -o '[0-9]*') 23 | 24 | # 如果解析失败,设置默认值 25 | TODAY=${TODAY:-0} 26 | TOTAL=${TOTAL:-0} 27 | else 28 | # 如果获取失败,设置默认值 29 | TODAY=0 30 | TOTAL=0 31 | fi 32 | } 33 | 34 | # 创建日志目录 35 | LOG_DIR="$HOME/tmp/ws_setup_logs" 36 | mkdir -p "$LOG_DIR" 37 | 38 | # 清理旧日志文件(超过7天的) 39 | find "$LOG_DIR" -type f -name "ws_setup_*.log" -mtime +7 -delete 2>/dev/null 40 | 41 | # 设置日志文件 42 | if [ "$1" = "check_and_start_all" ]; then 43 | # cron任务执行,使用固定的日志文件 44 | LOG_FILE="$LOG_DIR/cron_autorestart.log" 45 | # 如果日志文件超过1MB,则清空它 46 | if [ -f "$LOG_FILE" ] && [ $(stat -c%s "$LOG_FILE" 2>/dev/null || stat -f%z "$LOG_FILE" 2>/dev/null) -gt 1048576 ]; then 47 | echo "=== 日志文件已重置 $(date) ===" > "$LOG_FILE" 48 | fi 49 | else 50 | # 手动执行,创建带时间戳的日志文件 51 | LOG_FILE="$LOG_DIR/ws_setup_$(date +%Y%m%d%H%M%S).log" 52 | touch "$LOG_FILE" 53 | echo "=== 安装日志开始 $(date) ===" > "$LOG_FILE" 54 | fi 55 | 56 | # 创建配置目录和文件 57 | CONFIG_DIR="$HOME/tmp/ws_config" 58 | mkdir -p "$CONFIG_DIR" 59 | CONFIG_FILE="$CONFIG_DIR/ws_config.conf" 60 | 61 | # 打印带颜色的信息并记录日志 62 | print_info() { 63 | echo -e "${BLUE}[INFO]${NC} $1" 64 | echo "[INFO] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 65 | } 66 | 67 | print_success() { 68 | echo -e "${GREEN}[SUCCESS]${NC} $1" 69 | echo "[SUCCESS] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 70 | } 71 | 72 | print_warning() { 73 | echo -e "${YELLOW}[WARNING]${NC} $1" 74 | echo "[WARNING] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 75 | } 76 | 77 | print_error() { 78 | echo -e "${RED}[ERROR]${NC} $1" 79 | echo "[ERROR] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 80 | } 81 | 82 | # 记录调试信息到日志(只在关键点记录) 83 | log_debug() { 84 | # 只记录重要的调试信息 85 | if [[ "$1" == *"配置文件"* ]] || [[ "$1" == *"启动"* ]] || [[ "$1" == *"停止"* ]] || [[ "$1" == *"错误"* ]]; then 86 | echo "[DEBUG] $(date +%H:%M:%S) $1" >> "$LOG_FILE" 87 | fi 88 | } 89 | 90 | # 获取用户名 91 | username=$(whoami) 92 | print_info "检测到用户名: $username" 93 | 94 | # 检查配置文件是否存在 95 | check_config() { 96 | if [ ! -f "$CONFIG_FILE" ]; then 97 | print_warning "未找到配置文件,请先执行'修改配置文件'选项" 98 | return 1 99 | fi 100 | return 0 101 | } 102 | 103 | # 加载配置文件 104 | load_config() { 105 | if [ -f "$CONFIG_FILE" ]; then 106 | source "$CONFIG_FILE" 107 | return 0 108 | else 109 | return 1 110 | fi 111 | } 112 | 113 | # 检查进程状态 114 | check_processes() { 115 | NODE_RUNNING=false 116 | NEZHA_RUNNING=false 117 | 118 | # 简化的WebSocket服务进程检测 119 | if ps aux | grep "lsnode:" | grep -v grep > /dev/null; then 120 | NODE_RUNNING=true 121 | NODE_PID=$(ps aux | grep "lsnode:" | grep -v grep | awk '{print $2}') 122 | NODE_TYPE="lsnode" 123 | # 检测通用node index.js进程 124 | elif ps aux | grep "node index.js" | grep -v grep > /dev/null; then 125 | NODE_RUNNING=true 126 | NODE_PID=$(ps aux | grep "node index.js" | grep -v grep | awk '{print $2}') 127 | NODE_TYPE="node" 128 | fi 129 | 130 | # 简化的哪吒探针进程检测 131 | if pgrep -u "$USER" -f "nezha-agent" > /dev/null; then 132 | NEZHA_RUNNING=true 133 | NEZHA_PID=$(pgrep -u "$USER" -f "nezha-agent") 134 | fi 135 | } 136 | 137 | # 显示进程状态 138 | show_status() { 139 | check_processes 140 | 141 | print_info "进程状态:" 142 | if [ "$NODE_RUNNING" = true ]; then 143 | echo "- WebSocket服务: 运行中 ($NODE_TYPE进程, PID: $NODE_PID)" 144 | else 145 | echo "- WebSocket服务: 未运行" 146 | fi 147 | 148 | if [ "$NEZHA_RUNNING" = true ]; then 149 | echo "- 哪吒探针: 运行中 (PID: $NEZHA_PID)" 150 | else 151 | echo "- 哪吒探针: 未运行" 152 | fi 153 | } 154 | 155 | # 引导用户创建Node.js应用 156 | guide_nodejs_creation() { 157 | print_info "请按照以下步骤在控制面板中创建Node.js应用程序:" 158 | echo "1. 进入控制面板 -> Node.js APP" 159 | echo "2. 点击\"创建应用程序\"" 160 | echo "3. Node.js版本: 选择V20或者V18版本" 161 | echo "4. Application root: domains/$1/public_html" 162 | echo "5. Application startup file: index.js" 163 | echo "6. 点击\"创建\"按钮" 164 | echo "" 165 | print_info "创建完成后,请重新运行此脚本" 166 | } 167 | 168 | # 修改配置文件 169 | modify_config() { 170 | print_info "修改配置文件..." 171 | 172 | # 检查domains目录 173 | domains_dir="/home/$username/domains" 174 | if [ ! -d "$domains_dir" ]; then 175 | print_error "未找到domains目录: $domains_dir" 176 | return 1 177 | fi 178 | 179 | # 列出所有域名目录 180 | print_info "正在扫描域名目录..." 181 | domains=() 182 | for dir in "$domains_dir"/*; do 183 | if [ -d "$dir" ]; then 184 | domain_name=$(basename "$dir") 185 | domains+=("$domain_name") 186 | fi 187 | done 188 | 189 | # 显示域名列表 190 | if [ ${#domains[@]} -eq 0 ]; then 191 | print_error "未找到任何域名目录,请先在控制面板中创建域名" 192 | return 1 193 | else 194 | echo "检测到以下域名:" 195 | for i in "${!domains[@]}"; do 196 | echo "[$i] ${domains[$i]}" 197 | done 198 | 199 | read -p "请选择域名 [0-$((${#domains[@]}-1))]: " domain_choice 200 | 201 | if [[ "$domain_choice" =~ ^[0-9]+$ ]] && [ "$domain_choice" -ge 0 ] && [ "$domain_choice" -lt ${#domains[@]} ]; then 202 | domain="${domains[$domain_choice]}" 203 | print_info "已选择域名: $domain" 204 | else 205 | print_error "无效选择!" 206 | return 1 207 | fi 208 | fi 209 | 210 | # 确认域名目录是否存在 211 | domain_dir="/home/$username/domains/$domain/public_html" 212 | if [ ! -d "$domain_dir" ]; then 213 | print_error "域名目录 $domain_dir 不存在!" 214 | print_info "请检查您的域名是否正确,或者域名是否已经在控制面板中创建。" 215 | return 1 216 | fi 217 | 218 | print_info "域名目录: $domain_dir" 219 | 220 | # 检查Node.js应用是否已创建 221 | NODE_ENV_PATH="/home/$username/nodevenv/domains/$domain/public_html" 222 | if [ ! -d "$NODE_ENV_PATH" ]; then 223 | print_warning "未检测到Node.js应用环境,请先创建Node.js应用" 224 | guide_nodejs_creation "$domain" 225 | return 1 226 | fi 227 | 228 | # 询问节点名称 229 | read -p "请输入节点名称 (默认: hostvps): " node_name 230 | node_name=${node_name:-"hostvps"} 231 | 232 | # 询问端口号 233 | read -p "请输入监听端口 (默认: 3000,必须更换): " port 234 | port=${port:-3000} 235 | 236 | # UUID处理 237 | read -p "是否自动生成UUID? (Y/n, 默认: Y): " auto_uuid 238 | auto_uuid=${auto_uuid:-"Y"} 239 | 240 | if [[ $auto_uuid =~ ^[Yy]$ ]]; then 241 | # 生成UUID 242 | if command -v uuidgen > /dev/null; then 243 | uuid=$(uuidgen) 244 | else 245 | # 如果没有uuidgen,使用其他方式生成UUID 246 | uuid=$(cat /proc/sys/kernel/random/uuid) 247 | fi 248 | print_info "自动生成的UUID: $uuid" 249 | else 250 | read -p "请输入UUID: " uuid 251 | if [ -z "$uuid" ]; then 252 | print_error "UUID不能为空!" 253 | return 1 254 | fi 255 | fi 256 | 257 | # 询问反代域名(可选) 258 | read -p "请输入反代域名 (如果没有可回车默认使用www.visa.com.tw): " cf_domain 259 | cf_domain=${cf_domain:-"www.visa.com.tw"} 260 | 261 | # 询问哪吒探针信息(可选) 262 | read -p "请输入哪吒服务器地址 (v1格式: nz.example.com:端口号;v0格式: nz.example.com,回车跳过配置哪吒探针): " nezha_server 263 | 264 | if [ -n "$nezha_server" ]; then 265 | read -p "请输入哪吒客户端密钥 (必填): " nezha_key 266 | 267 | # 自动检测哪吒版本(通过检查服务器地址是否包含端口号) 268 | if [[ "$nezha_server" == *":"* ]]; then 269 | # 包含冒号,使用哪吒v1 270 | print_info "检测到哪吒v1格式的服务器地址" 271 | nezha_port="" 272 | 273 | # 检查是否需要TLS 274 | NEZHA_TLS="false" 275 | if [[ "$nezha_server" == *":443"* ]] || [[ "$nezha_server" == *":8443"* ]] || [[ "$nezha_server" == *":2096"* ]] || [[ "$nezha_server" == *":2087"* ]] || [[ "$nezha_server" == *":2083"* ]] || [[ "$nezha_server" == *":2053"* ]]; then 276 | print_info "检测到TLS端口,将使用TLS连接" 277 | nezha_tls=true 278 | else 279 | # 询问是否使用TLS 280 | read -p "是否使用TLS连接哪吒服务器? (Y/n, 默认: Y): " use_tls 281 | use_tls=${use_tls:-"Y"} 282 | if [[ $use_tls =~ ^[Yy]$ ]]; then 283 | nezha_tls=true 284 | else 285 | nezha_tls=false 286 | fi 287 | fi 288 | else 289 | # 不包含冒号,使用哪吒v0 290 | print_info "检测到哪吒v0格式的服务器地址" 291 | read -p "请输入哪吒v0端口: " nezha_port 292 | 293 | # 检查是否需要TLS 294 | if [[ "$nezha_port" == "443" ]] || [[ "$nezha_port" == "8443" ]] || [[ "$nezha_port" == "2096" ]] || [[ "$nezha_port" == "2087" ]] || [[ "$nezha_port" == "2083" ]] || [[ "$nezha_port" == "2053" ]]; then 295 | print_info "检测到TLS端口,将使用TLS连接" 296 | nezha_tls="--tls" 297 | else 298 | # 询问是否使用TLS 299 | read -p "是否使用TLS连接哪吒服务器? (Y/n, 默认: N): " use_tls 300 | use_tls=${use_tls:-"N"} 301 | if [[ $use_tls =~ ^[Yy]$ ]]; then 302 | nezha_tls="--tls" 303 | else 304 | nezha_tls="" 305 | fi 306 | fi 307 | fi 308 | else 309 | nezha_key="" 310 | nezha_port="" 311 | nezha_tls=true 312 | fi 313 | 314 | # 保存配置到文件 315 | cat > "$CONFIG_FILE" << EOF 316 | # WebSocket服务配置 317 | DOMAIN="$domain" 318 | DOMAIN_DIR="$domain_dir" 319 | NODE_NAME="$node_name" 320 | PORT="$port" 321 | UUID="$uuid" 322 | CF_DOMAIN="$cf_domain" 323 | 324 | # 哪吒探针配置 325 | NEZHA_SERVER="$nezha_server" 326 | NEZHA_KEY="$nezha_key" 327 | NEZHA_PORT="$nezha_port" 328 | NEZHA_TLS="$nezha_tls" 329 | EOF 330 | chmod 600 "$CONFIG_FILE" 331 | print_success "配置文件已保存!" 332 | return 0 333 | } 334 | 335 | # 启动WebSocket代理服务 336 | start_websocket() { 337 | print_info "启动WebSocket代理服务..." 338 | 339 | # 检查配置文件 340 | if ! check_config; then 341 | return 1 342 | fi 343 | 344 | # 加载配置 345 | load_config 346 | 347 | # 检查Node.js应用是否已创建 348 | NODE_ENV_PATH="/home/$username/nodevenv/domains/$DOMAIN/public_html" 349 | if [ ! -d "$NODE_ENV_PATH" ]; then 350 | print_error "未检测到Node.js应用环境,请先创建Node.js应用" 351 | guide_nodejs_creation "$DOMAIN" 352 | return 1 353 | fi 354 | 355 | 356 | 357 | # 创建index.js文件 358 | print_info "创建index.js文件..." 359 | cat > "$DOMAIN_DIR/index.js" << EOF 360 | const http = require('http'); 361 | const fs = require('fs'); 362 | const { Buffer } = require('buffer'); 363 | const net = require('net'); 364 | const { WebSocket, createWebSocketStream } = require('ws'); 365 | 366 | // 配置参数 367 | const UUID = process.env.UUID || '$UUID'; // UUID用于验证连接 368 | const DOMAIN = process.env.DOMAIN || '$DOMAIN'; // 域名 369 | const CF_DOMAIN = process.env.CF_DOMAIN || '$CF_DOMAIN'; // 反代域名 370 | const SUB_PATH = process.env.SUB_PATH || 'sub'; // 获取节点的订阅路径 371 | const NAME = process.env.NAME || '$NODE_NAME'; // 节点名称 372 | const PORT = process.env.PORT || $PORT; // http和ws服务端口 373 | 374 | const httpServer = http.createServer((req, res) => { 375 | // 记录请求信息,帮助调试 376 | console.log(\`收到请求: \${req.method} \${req.url}\`); 377 | 378 | // 处理/sub路径,不区分大小写 379 | if (req.url === '/') { 380 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 381 | res.end('It works!\\nNodeJS ' + process.versions.node + '\\n'); 382 | } else if (req.url.toLowerCase() === \`/\${SUB_PATH.toLowerCase()}\`) { 383 | console.log('处理订阅请求...'); 384 | console.log(\`UUID: \${UUID}, DOMAIN: \${DOMAIN}, NAME: \${NAME}\`); 385 | 386 | const nodeName = NAME || 'NodeWS'; 387 | 388 | // 生成VLESS URL 389 | const vlessURL = \`vless://\${UUID}@\${CF_DOMAIN}:443?encryption=none&security=tls&sni=\${DOMAIN}&type=ws&host=\${DOMAIN}&path=%2F#\${nodeName}\`; 390 | console.log('生成的VLESS URL:', vlessURL); 391 | 392 | // 转换为Base64 393 | const base64Content = Buffer.from(vlessURL).toString('base64'); 394 | 395 | console.log('Base64编码后的URL:'); 396 | console.log(base64Content); 397 | 398 | res.writeHead(200, { 399 | 'Content-Type': 'text/plain', 400 | 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', 401 | 'Pragma': 'no-cache', 402 | 'Expires': '0' 403 | }); 404 | res.end(base64Content + '\\n'); 405 | } else if (req.url === '/status') { 406 | // 添加状态检查端点 407 | const { exec } = require('child_process'); 408 | exec('ps aux | grep -v grep | grep "node\\\\|nezha-agent"', (error, stdout) => { 409 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 410 | res.end(\`服务状态:\\n\${stdout}\\n\`); 411 | }); 412 | } else if (req.url === '/debug') { 413 | // 添加调试端点 414 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 415 | res.end(\`调试信息: 416 | UUID: \${UUID} 417 | DOMAIN: \${DOMAIN} 418 | SUB_PATH: \${SUB_PATH} 419 | NAME: \${NAME} 420 | PORT: \${PORT} 421 | \`); 422 | } else { 423 | res.writeHead(404, { 'Content-Type': 'text/plain' }); 424 | res.end('Not Found\\n'); 425 | } 426 | }); 427 | 428 | const wss = new WebSocket.Server({ server: httpServer }); 429 | const uuid = UUID.replace(/-/g, ""); 430 | wss.on('connection', ws => { 431 | ws.once('message', msg => { 432 | const [VERSION] = msg; 433 | const id = msg.slice(1, 17); 434 | if (!id.every((v, i) => v == parseInt(uuid.substr(i * 2, 2), 16))) return; 435 | let i = msg.slice(17, 18).readUInt8() + 19; 436 | const port = msg.slice(i, i += 2).readUInt16BE(0); 437 | const ATYP = msg.slice(i, i += 1).readUInt8(); 438 | const host = ATYP == 1 ? msg.slice(i, i += 4).join('.') : 439 | (ATYP == 2 ? new TextDecoder().decode(msg.slice(i + 1, i += 1 + msg.slice(i, i + 1).readUInt8())) : 440 | (ATYP == 3 ? msg.slice(i, i += 16).reduce((s, b, i, a) => (i % 2 ? s.concat(a.slice(i - 1, i + 1)) : s), []).map(b => b.readUInt16BE(0).toString(16)).join(':') : '')); 441 | ws.send(new Uint8Array([VERSION, 0])); 442 | const duplex = createWebSocketStream(ws); 443 | net.connect({ host, port }, function () { 444 | this.write(msg.slice(i)); 445 | duplex.on('error', () => { }).pipe(this).on('error', () => { }).pipe(duplex); 446 | }).on('error', () => { }); 447 | }).on('error', () => { }); 448 | }); 449 | 450 | httpServer.listen(PORT, () => { 451 | console.log(\`Server is running on port \${PORT}\`); 452 | }); 453 | EOF 454 | 455 | # 创建package.json 456 | print_info "创建package.json文件..." 457 | cat > "$DOMAIN_DIR/package.json" << EOF 458 | { 459 | "name": "node-ws", 460 | "version": "1.0.0", 461 | "description": "WebSocket Server", 462 | "main": "index.js", 463 | "author": "mqiancheng", 464 | "repository": "https://github.com/mqiancheng/host-node-ws", 465 | "license": "MIT", 466 | "private": false, 467 | "scripts": { 468 | "start": "node index.js" 469 | }, 470 | "dependencies": { 471 | "ws": "^8.14.2" 472 | }, 473 | "engines": { 474 | "node": ">=14" 475 | } 476 | } 477 | EOF 478 | 479 | # 停止现有进程 480 | check_processes 481 | if [ "$NODE_RUNNING" = true ]; then 482 | print_info "停止现有WebSocket服务进程..." 483 | kill $NODE_PID 484 | fi 485 | 486 | # 获取Node.js虚拟环境激活脚本 487 | NODE_VERSIONS=( $(ls -d "$NODE_ENV_PATH"/* 2>/dev/null | grep -o '[0-9]*$' | sort -nr) ) 488 | if [ ${#NODE_VERSIONS[@]} -gt 0 ]; then 489 | SELECTED_VERSION="${NODE_VERSIONS[0]}" 490 | NODE_ENV_ACTIVATE="$NODE_ENV_PATH/$SELECTED_VERSION/bin/activate" 491 | print_info "使用Node.js版本: $SELECTED_VERSION" 492 | 493 | # 安装依赖并启动服务 494 | print_info "安装依赖并启动服务..." 495 | cd "$DOMAIN_DIR" 496 | source "$NODE_ENV_ACTIVATE" 497 | npm install 498 | 499 | # 如果已有进程在运行,先停止它 500 | if [ -f "node.pid" ] && ps -p $(cat node.pid) > /dev/null 2>&1; then 501 | print_info "停止已运行的WebSocket服务 (PID: $(cat node.pid))..." 502 | kill -9 $(cat node.pid) > /dev/null 2>&1 503 | sleep 1 504 | fi 505 | 506 | # 启动Node.js应用 507 | nohup node index.js > node.log 2>&1 & 508 | echo $! > node.pid 509 | 510 | # 等待服务启动 511 | sleep 2 512 | 513 | # 检查服务是否成功启动 514 | if ps -p $(cat node.pid) > /dev/null 2>&1; then 515 | # 检查日志文件中是否有错误信息 516 | if grep -q "EADDRINUSE" "$DOMAIN_DIR/node.log" || grep -q "address already in use" "$DOMAIN_DIR/node.log"; then 517 | print_error "WebSocket服务启动失败: 端口 $PORT 已被占用" 518 | 519 | 520 | # 提示用户输入新的端口号 521 | echo 522 | read -p "请输入新的监听端口号: " new_port 523 | 524 | # 验证端口号是否有效 525 | if [[ ! "$new_port" =~ ^[0-9]+$ ]] || [ "$new_port" -lt 1 ] || [ "$new_port" -gt 65535 ]; then 526 | print_error "无效的端口号,请输入1-65535之间的数字" 527 | return 1 528 | fi 529 | 530 | # 更新配置文件中的端口号 531 | sed -i "s/^PORT=\"$PORT\"/PORT=\"$new_port\"/" "$CONFIG_FILE" 532 | 533 | if [ $? -eq 0 ]; then 534 | print_success "监听端口已修改为 $new_port,请重新启动WebSocket代理服务" 535 | else 536 | print_error "修改配置文件失败,请手动修改配置文件中的PORT值" 537 | fi 538 | 539 | return 1 540 | elif grep -q "Error:" "$DOMAIN_DIR/node.log" || grep -q "error:" "$DOMAIN_DIR/node.log"; then 541 | print_error "WebSocket服务启动失败,发现错误" 542 | echo -e "${YELLOW}请查看日志文件: $DOMAIN_DIR/node.log${NC}" 543 | return 1 544 | fi 545 | 546 | print_success "WebSocket服务已启动,PID: $(cat node.pid)" 547 | echo -e "${GREEN}您的VLESS订阅地址是:${NC}https://${DOMAIN}/sub" 548 | else 549 | print_error "WebSocket服务启动失败,进程未运行" 550 | # 检查端口占用错误 551 | if [ -f "$DOMAIN_DIR/node.log" ] && (grep -q "EADDRINUSE" "$DOMAIN_DIR/node.log" || grep -q "address already in use" "$DOMAIN_DIR/node.log"); then 552 | print_error "检测到端口 $PORT 已被占用" 553 | 554 | 555 | # 提示用户输入新的端口号 556 | echo 557 | read -p "请输入新的监听端口号: " new_port 558 | 559 | # 验证端口号是否有效 560 | if [[ ! "$new_port" =~ ^[0-9]+$ ]] || [ "$new_port" -lt 1 ] || [ "$new_port" -gt 65535 ]; then 561 | print_error "无效的端口号,请输入1-65535之间的数字" 562 | return 1 563 | fi 564 | 565 | # 更新配置文件中的端口号 566 | sed -i "s/^PORT=\"$PORT\"/PORT=\"$new_port\"/" "$CONFIG_FILE" 567 | 568 | if [ $? -eq 0 ]; then 569 | print_success "监听端口已修改为 $new_port,请重新启动WebSocket代理服务" 570 | else 571 | print_error "修改配置文件失败,请手动修改配置文件中的PORT值" 572 | fi 573 | fi 574 | 575 | return 1 576 | fi 577 | else 578 | print_error "未找到Node.js版本,请确保已正确创建Node.js应用" 579 | return 1 580 | fi 581 | 582 | return 0 583 | } 584 | 585 | # 启动哪吒探针 586 | start_nezha() { 587 | print_info "启动哪吒探针..." 588 | 589 | # 检查配置文件 590 | if ! check_config; then 591 | return 1 592 | fi 593 | 594 | # 加载配置 595 | load_config 596 | 597 | # 检查哪吒探针配置 598 | if [ -z "$NEZHA_SERVER" ] || [ -z "$NEZHA_KEY" ]; then 599 | print_warning "哪吒探针配置不完整,请先修改配置文件" 600 | return 1 601 | fi 602 | 603 | # 停止现有哪吒探针进程 604 | check_processes 605 | if [ "$NEZHA_RUNNING" = true ]; then 606 | print_info "停止现有哪吒探针进程..." 607 | kill $NEZHA_PID 608 | fi 609 | 610 | # 下载并启动哪吒探针 611 | start_nezha_process 612 | 613 | # 等待探针启动 614 | sleep 2 615 | check_processes 616 | if [ "$NEZHA_RUNNING" = true ]; then 617 | print_success "哪吒探针已启动,PID: $NEZHA_PID" 618 | else 619 | print_warning "哪吒探针可能未正常启动,请检查日志" 620 | fi 621 | 622 | return 0 623 | } 624 | 625 | # 下载并启动哪吒探针进程(供多个函数调用) 626 | start_nezha_process() { 627 | # 下载agent.sh(如果不存在) 628 | if [ ! -f "$HOME/agent.sh" ]; then 629 | print_info "下载哪吒探针安装脚本..." 630 | cd "$HOME" 631 | curl -L https://raw.githubusercontent.com/mqiancheng/host-node-ws/refs/heads/main/agent.sh -o agent.sh && chmod +x agent.sh 632 | fi 633 | 634 | # 启动哪吒探针 635 | print_info "启动哪吒探针..." 636 | cd "$HOME" 637 | 638 | # 统一使用环境变量方式启动哪吒探针 639 | print_info "启动哪吒探针..." 640 | # 设置所有可能的环境变量,让agent.sh自行判断使用哪些 641 | env NZ_SERVER="$NEZHA_SERVER" NZ_PORT="$NEZHA_PORT" NZ_KEY="$NEZHA_KEY" NZ_TLS="$NEZHA_TLS" NZ_UUID="$UUID" NZ_CLIENT_SECRET="$NEZHA_KEY" ./agent.sh > /dev/null 2>&1 & 642 | } 643 | 644 | # 强制重新安装 645 | force_reinstall() { 646 | print_info "准备强制重新安装..." 647 | 648 | read -p "此操作将删除所有现有文件和进程,确定继续? (y/N, 默认: N): " confirm_reinstall 649 | confirm_reinstall=${confirm_reinstall:-"N"} 650 | 651 | if [[ ! $confirm_reinstall =~ ^[Yy]$ ]]; then 652 | print_info "已取消重新安装。" 653 | return 1 654 | fi 655 | 656 | # 加载配置(如果存在) 657 | if [ -f "$CONFIG_FILE" ]; then 658 | load_config 659 | fi 660 | 661 | # 停止并清理现有安装 662 | print_info "正在清理现有安装..." 663 | 664 | # 停止所有进程 665 | check_processes 666 | if [ "$NODE_RUNNING" = true ]; then 667 | print_info "停止WebSocket服务进程..." 668 | kill $NODE_PID 669 | fi 670 | 671 | if [ "$NEZHA_RUNNING" = true ]; then 672 | print_info "停止哪吒探针进程..." 673 | kill $NEZHA_PID 674 | fi 675 | 676 | # 卸载哪吒探针 677 | if [ -f "$HOME/agent.sh" ]; then 678 | cd "$HOME" 679 | ./agent.sh uninstall 680 | print_info "已卸载哪吒探针" 681 | fi 682 | 683 | # 删除文件 684 | print_info "删除现有文件..." 685 | 686 | # 删除域名目录下的文件 687 | if [ -n "$DOMAIN_DIR" ] && [ -d "$DOMAIN_DIR" ]; then 688 | rm -f "$DOMAIN_DIR/index.js" "$DOMAIN_DIR/package.json" "$DOMAIN_DIR/node.pid" "$DOMAIN_DIR/node.log" 689 | fi 690 | 691 | # 删除其他文件和目录 692 | rm -f "$HOME/agent.sh" "$CONFIG_FILE" 693 | rm -rf "$HOME/nezha" 694 | 695 | # 清理日志文件夹 696 | rm -rf "$LOG_DIR" 697 | mkdir -p "$LOG_DIR" 698 | 699 | print_success "清理完成!" 700 | return 0 701 | } 702 | 703 | # 主菜单 704 | show_menu() { 705 | clear 706 | echo "========================================" 707 | echo " 基础WebSocket代理服务 v$VERSION " 708 | echo "========================================" 709 | echo -e "${CYAN}今日运行: ${YELLOW}${TODAY}次 ${CYAN}累计运行: ${YELLOW}${TOTAL}次${NC}" 710 | echo -e "----------By mqiancheng----------" 711 | echo -e "项目地址: https://github.com/mqiancheng/host-node-ws" 712 | echo "" 713 | 714 | # 显示进程状态 715 | show_status 716 | 717 | echo "" 718 | echo "请选择操作:" 719 | echo "1. 修改配置文件" 720 | echo "2. 启动WebSocket代理服务" 721 | echo "3. 启动哪吒探针" 722 | echo "5. 强制重新安装(清除现有安装)" 723 | echo "0. 退出脚本" 724 | echo "" 725 | 726 | read -p "请输入选项 [0-5]: " option 727 | log_debug "用户选择操作: $option" 728 | 729 | case $option in 730 | 0) 731 | print_info "退出脚本。" 732 | log_debug "用户选择退出脚本" 733 | exit 0 734 | ;; 735 | 1) 736 | modify_config 737 | ;; 738 | 2) 739 | start_websocket 740 | ;; 741 | 3) 742 | start_nezha 743 | ;; 744 | 5) 745 | force_reinstall 746 | ;; 747 | *) 748 | print_error "无效选项,请重新选择。" 749 | log_debug "用户输入了无效选项: $option" 750 | ;; 751 | esac 752 | 753 | # 操作完成后暂停 754 | echo "" 755 | read -p "按Enter键继续..." dummy 756 | 757 | # 返回主菜单 758 | show_menu 759 | } 760 | 761 | # 主函数 762 | main() { 763 | # 获取运行统计 764 | get_run_stats 765 | 766 | # 显示欢迎信息 767 | clear 768 | echo "========================================" 769 | echo " WebSocket服务器部署工具 v$VERSION " 770 | echo "========================================" 771 | echo -e "${CYAN}今日运行: ${YELLOW}${TODAY}次 ${CYAN}累计运行: ${YELLOW}${TOTAL}次${NC}" 772 | echo -e "----------By mqiancheng----------" 773 | echo -e "项目地址: https://github.com/mqiancheng/host-node-ws" 774 | echo "" 775 | print_info "欢迎使用WebSocket服务器部署工具!" 776 | print_info "此工具可以帮助您快速部署WebSocket服务和哪吒探针。" 777 | echo "" 778 | 779 | # 显示主菜单 780 | show_menu 781 | } 782 | 783 | # 检查并启动所有服务(用于Cron任务) 784 | check_and_start_all() { 785 | # 创建日志目录和固定的cron日志文件 786 | CRON_LOG="$HOME/tmp/ws_setup_logs/cron_autorestart.log" 787 | mkdir -p "$HOME/tmp/ws_setup_logs" 788 | 789 | # 如果日志文件超过1MB,则清空它 790 | if [ -f "$CRON_LOG" ] && [ $(stat -c%s "$CRON_LOG" 2>/dev/null || stat -f%z "$CRON_LOG" 2>/dev/null) -gt 1048576 ]; then 791 | echo "=== 日志文件已重置 $(date) ===" > "$CRON_LOG" 792 | fi 793 | 794 | # 加载配置 795 | if [ -f "$HOME/tmp/ws_config/ws_config.conf" ]; then 796 | source "$HOME/tmp/ws_config/ws_config.conf" 797 | 798 | # 检查并启动哪吒探针 799 | if ! pgrep -u "$USER" -f "nezha-agent" > /dev/null && [ -n "$NEZHA_SERVER" ] && [ -n "$NEZHA_KEY" ]; then 800 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 哪吒探针未运行,正在启动..." >> "$CRON_LOG" 801 | 802 | # 确保agent.sh存在 803 | if [ ! -f "$HOME/agent.sh" ]; then 804 | cd "$HOME" 805 | curl -L https://raw.githubusercontent.com/mqiancheng/host-node-ws/refs/heads/main/agent.sh -o agent.sh && chmod +x agent.sh 806 | fi 807 | 808 | # 直接启动哪吒探针 809 | cd "$HOME" 810 | env NZ_SERVER="$NEZHA_SERVER" NZ_PORT="$NEZHA_PORT" NZ_KEY="$NEZHA_KEY" NZ_TLS="$NEZHA_TLS" NZ_UUID="$UUID" NZ_CLIENT_SECRET="$NEZHA_KEY" ./agent.sh > /dev/null 2>&1 & 811 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 哪吒探针启动命令已执行" >> "$CRON_LOG" 812 | fi 813 | 814 | # 检查并启动WebSocket服务 815 | if [ -n "$DOMAIN" ] && [ -n "$DOMAIN_DIR" ]; then 816 | # 使用简化的进程检测 817 | NODE_RUNNING=false 818 | if ps aux | grep "lsnode:" | grep -v grep > /dev/null || ps aux | grep "node index.js" | grep -v grep > /dev/null; then 819 | NODE_RUNNING=true 820 | fi 821 | 822 | # 如果WebSocket服务未运行,尝试通过curl访问订阅地址来启动它 823 | if [ "$NODE_RUNNING" = false ]; then 824 | # 先检查日志文件中是否有端口占用错误 825 | if [ -f "$DOMAIN_DIR/node.log" ] && (grep -q "EADDRINUSE" "$DOMAIN_DIR/node.log" || grep -q "address already in use" "$DOMAIN_DIR/node.log"); then 826 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 检测到端口 $PORT 已被占用,无法启动WebSocket服务" >> "$CRON_LOG" 827 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 请手动修改配置文件使用其他端口" >> "$CRON_LOG" 828 | else 829 | curl -s -o /dev/null "https://$DOMAIN/sub" 830 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 尝试通过访问订阅地址启动WebSocket服务" >> "$CRON_LOG" 831 | fi 832 | fi 833 | fi 834 | else 835 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 配置文件不存在,无法启动服务" >> "$CRON_LOG" 836 | fi 837 | } 838 | 839 | # 处理命令行参数 840 | if [ "$1" = "check_and_start_all" ]; then 841 | check_and_start_all 842 | exit 0 843 | fi 844 | 845 | # 执行主函数 846 | main -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 颜色定义 4 | RED='\033[0;31m' 5 | GREEN='\033[0;32m' 6 | YELLOW='\033[0;33m' 7 | BLUE='\033[0;34m' 8 | CYAN='\033[0;36m' 9 | NC='\033[0m' # No Color 10 | 11 | # 脚本版本 12 | VERSION="1.1.0" 13 | 14 | # 全局变量 15 | HAS_UPDATES=false 16 | UPDATE_LIST="" 17 | WS_UPDATE_INFO="" 18 | ARGO_UPDATE_INFO="" 19 | SETUP_UPDATE_INFO="" 20 | 21 | # 获取运行统计 22 | get_run_stats() { 23 | # 使用curl获取统计数据,超时设置为2秒,但不显示输出 24 | local stats_data=$(curl -s -m 2 "http://counter.kfcyes.dpdns.org/counter.php?url=https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/setup.sh" 2>/dev/null) 25 | 26 | # 解析统计数据 27 | if [ -n "$stats_data" ]; then 28 | TODAY=$(echo "$stats_data" | grep -o '"daily_count":[0-9]*' | grep -o '[0-9]*') 29 | TOTAL=$(echo "$stats_data" | grep -o '"total_count":[0-9]*' | grep -o '[0-9]*') 30 | 31 | # 如果解析失败,设置默认值 32 | TODAY=${TODAY:-0} 33 | TOTAL=${TOTAL:-0} 34 | else 35 | # 如果获取失败,设置默认值 36 | TODAY=0 37 | TOTAL=0 38 | fi 39 | } 40 | 41 | # 打印带颜色的信息 42 | print_info() { 43 | echo -e "${BLUE}[INFO]${NC} $1" 44 | } 45 | 46 | print_success() { 47 | echo -e "${GREEN}[SUCCESS]${NC} $1" 48 | } 49 | 50 | print_warning() { 51 | echo -e "${YELLOW}[WARNING]${NC} $1" 52 | } 53 | 54 | print_error() { 55 | echo -e "${RED}[ERROR]${NC} $1" 56 | } 57 | 58 | # 获取脚本的版本号 59 | get_script_version() { 60 | local script_file=$1 61 | if [ -f "$script_file" ]; then 62 | local version=$(grep -o 'VERSION="[0-9.]*"' "$script_file" | grep -o '[0-9.]*') 63 | echo "$version" 64 | else 65 | echo "0.0.0" 66 | fi 67 | } 68 | 69 | # 获取GitHub上脚本的最新版本号 70 | get_github_version() { 71 | local script_file=$1 72 | # 使用-s参数静默下载,不显示进度条或错误信息 73 | local github_content=$(curl -s "https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/$script_file" 2>/dev/null) 74 | if [ -n "$github_content" ]; then 75 | local version=$(echo "$github_content" | grep -o 'VERSION="[0-9.]*"' | grep -o '[0-9.]*') 76 | echo "$version" 77 | else 78 | echo "0.0.0" 79 | fi 80 | } 81 | 82 | # 比较版本号,如果版本2大于版本1,返回1,否则返回0 83 | compare_versions() { 84 | local version1=$1 85 | local version2=$2 86 | 87 | if [ "$version1" = "$version2" ]; then 88 | return 0 89 | fi 90 | 91 | local IFS=. 92 | local i ver1=($version1) ver2=($version2) 93 | 94 | # 填充短版本号 95 | for ((i=${#ver1[@]}; i<${#ver2[@]}; i++)); do 96 | ver1[i]=0 97 | done 98 | for ((i=${#ver2[@]}; i<${#ver1[@]}; i++)); do 99 | ver2[i]=0 100 | done 101 | 102 | # 比较版本号 103 | for ((i=0; i<${#ver1[@]}; i++)); do 104 | if [[ -z ${ver2[i]} ]]; then 105 | # 如果ver2短,则ver1大 106 | return 0 107 | fi 108 | if ((10#${ver1[i]} > 10#${ver2[i]})); then 109 | return 0 110 | fi 111 | if ((10#${ver1[i]} < 10#${ver2[i]})); then 112 | return 1 113 | fi 114 | done 115 | 116 | return 0 117 | } 118 | 119 | # 下载脚本 120 | download_script() { 121 | local script_file=$1 122 | print_info "下载 $script_file..." 123 | # 使用-s参数静默下载,不显示进度条 124 | curl -s -L "https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/$script_file" -o "$script_file" 125 | if [ $? -eq 0 ]; then 126 | chmod +x "$script_file" 127 | print_success "成功下载 $script_file" 128 | return 0 129 | else 130 | print_error "下载 $script_file 失败" 131 | return 1 132 | fi 133 | } 134 | 135 | # 下载指定的脚本 136 | download_specific_script() { 137 | local script_name=$1 138 | 139 | if [ ! -f "$script_name" ]; then 140 | print_info "正在下载 $script_name..." 141 | download_script "$script_name" || return 1 142 | fi 143 | 144 | # 确保脚本有执行权限 145 | chmod +x "$script_name" 146 | 147 | return 0 148 | } 149 | 150 | # 检查脚本更新 151 | check_script_updates() { 152 | # 使用已经检测到的更新信息 153 | if [ "$HAS_UPDATES" = true ]; then 154 | print_info "发现以下脚本有更新:" 155 | echo -e "$UPDATE_LIST" 156 | 157 | read -p "是否更新这些脚本? (Y/n): " update_choice 158 | update_choice=${update_choice:-Y} 159 | 160 | if [[ $update_choice =~ ^[Yy]$ ]]; then 161 | # 更新脚本 162 | if [ -f "setup-ws.sh" ] && [ -n "$WS_UPDATE_INFO" ]; then 163 | download_script "setup-ws.sh" 164 | fi 165 | 166 | if [ -f "setup-argo.sh" ] && [ -n "$ARGO_UPDATE_INFO" ]; then 167 | download_script "setup-argo.sh" 168 | fi 169 | 170 | # 更新setup.sh自身 171 | if [ -n "$SETUP_UPDATE_INFO" ]; then 172 | download_script "setup.sh" 173 | print_success "脚本更新完成,请重新运行setup.sh" 174 | exit 0 175 | else 176 | print_success "脚本更新完成" 177 | fi 178 | else 179 | print_info "跳过脚本更新" 180 | fi 181 | else 182 | print_success "所有脚本已是最新版本" 183 | fi 184 | } 185 | 186 | # 统计用户选择 187 | record_choice() { 188 | local choice=$1 189 | # 使用>/dev/null屏蔽所有输出 190 | curl -s -m 2 "http://counter.kfcyes.dpdns.org/counter.php?url=https://raw.githubusercontent.com/mqiancheng/host-node-ws/main/setup.sh&choice=$choice" >/dev/null 2>&1 & 191 | } 192 | 193 | # 检查脚本更新状态 194 | check_update_status() { 195 | local has_updates=false 196 | local update_list="" 197 | 198 | # 检查setup-ws.sh 199 | if [ -f "setup-ws.sh" ]; then 200 | local local_version=$(get_script_version "setup-ws.sh") 201 | local github_version=$(get_github_version "setup-ws.sh") 202 | 203 | compare_versions "$local_version" "$github_version" 204 | if [ $? -eq 1 ]; then 205 | has_updates=true 206 | WS_UPDATE_INFO="setup-ws.sh: $local_version -> $github_version" 207 | update_list="$update_list\n- $WS_UPDATE_INFO" 208 | fi 209 | fi 210 | 211 | # 检查setup-argo.sh 212 | if [ -f "setup-argo.sh" ]; then 213 | local local_version=$(get_script_version "setup-argo.sh") 214 | local github_version=$(get_github_version "setup-argo.sh") 215 | 216 | compare_versions "$local_version" "$github_version" 217 | if [ $? -eq 1 ]; then 218 | has_updates=true 219 | ARGO_UPDATE_INFO="setup-argo.sh: $local_version -> $github_version" 220 | update_list="$update_list\n- $ARGO_UPDATE_INFO" 221 | fi 222 | fi 223 | 224 | # 检查setup.sh自身 225 | local self_local_version="$VERSION" 226 | local self_github_version=$(get_github_version "setup.sh") 227 | 228 | compare_versions "$self_local_version" "$self_github_version" 229 | if [ $? -eq 1 ]; then 230 | has_updates=true 231 | SETUP_UPDATE_INFO="setup.sh: $self_local_version -> $self_github_version" 232 | update_list="$update_list\n- $SETUP_UPDATE_INFO" 233 | fi 234 | 235 | # 设置全局更新状态 236 | if [ "$has_updates" = true ]; then 237 | HAS_UPDATES=true 238 | UPDATE_LIST="$update_list" 239 | else 240 | HAS_UPDATES=false 241 | UPDATE_LIST="" 242 | fi 243 | } 244 | 245 | # 主函数 246 | main() { 247 | # 获取运行统计 248 | get_run_stats 249 | 250 | # 检查脚本更新状态 251 | HAS_UPDATES=false 252 | UPDATE_LIST="" 253 | check_update_status 254 | 255 | # 显示欢迎信息 256 | clear 257 | echo "========================================" 258 | echo " WebSocket服务器部署工具 v$VERSION " 259 | echo "========================================" 260 | echo -e "${CYAN}今日运行: ${YELLOW}${TODAY}次 ${CYAN}累计运行: ${YELLOW}${TOTAL}次${NC}" 261 | echo -e "----------By mqiancheng----------" 262 | echo -e "项目地址: https://github.com/mqiancheng/host-node-ws" 263 | echo "" 264 | 265 | # 显示选择菜单 266 | echo "请选择要部署的服务类型:" 267 | echo "1. 基础WebSocket代理服务" 268 | echo " - 简单易用,适合基本代理需求" 269 | echo " - 仅支持VLESS协议" 270 | echo "" 271 | echo -e "${YELLOW}注意: Argo隧道WebSocket代理服务正在测试中,暂时不可用${NC}" 272 | echo "" 273 | 274 | # 根据是否有更新显示不同的选项3 275 | if [ "$HAS_UPDATES" = true ]; then 276 | echo -e "3. ${GREEN}检查脚本更新 [有可用更新!]${NC}" 277 | echo " - 发现以下脚本有更新:" 278 | echo -e "$UPDATE_LIST" 279 | else 280 | echo "3. 检查脚本更新" 281 | fi 282 | echo "" 283 | echo "0. 退出脚本" 284 | echo "" 285 | 286 | read -p "请输入选项 [1]: " choice 287 | choice=${choice:-1} 288 | 289 | case $choice in 290 | 0) 291 | # 用户选择退出 292 | print_info "退出脚本..." 293 | exit 0 294 | ;; 295 | 1) 296 | # 统计用户选择了WebSocket版本 297 | record_choice "ws" 298 | print_info "正在准备WebSocket部署工具..." 299 | 300 | # 下载所需脚本 301 | if download_specific_script "setup-ws.sh"; then 302 | print_info "正在启动WebSocket部署工具..." 303 | sleep 1 304 | exec ./setup-ws.sh 305 | else 306 | print_error "无法下载所需脚本,请检查网络连接或手动下载" 307 | exit 1 308 | fi 309 | ;; 310 | # 隐藏的Argo选项,暂时不可用 311 | 9) 312 | # 统计用户选择了Argo版本 313 | record_choice "argo" 314 | print_info "正在准备Argo隧道WebSocket部署工具..." 315 | 316 | # 下载所需脚本 317 | if download_specific_script "setup-argo.sh"; then 318 | print_info "正在启动Argo隧道WebSocket部署工具..." 319 | sleep 1 320 | exec ./setup-argo.sh 321 | else 322 | print_error "无法下载所需脚本,请检查网络连接或手动下载" 323 | exit 1 324 | fi 325 | ;; 326 | 3) 327 | # 统计用户选择了检查更新 328 | record_choice "update" 329 | print_info "正在检查脚本更新..." 330 | check_script_updates 331 | 332 | # 更新完成后,重新显示主菜单 333 | read -p "按Enter键返回主菜单..." dummy 334 | exec $0 335 | ;; 336 | *) 337 | print_error "无效选项,请选择0-1或3之间的数字" 338 | exit 1 339 | ;; 340 | esac 341 | } 342 | 343 | # 检查并启动所有服务(供cron任务调用) 344 | check_and_start_all() { 345 | # 创建日志目录和固定的cron日志文件 346 | CRON_LOG="$HOME/tmp/ws_setup_logs/cron_autorestart.log" 347 | mkdir -p "$HOME/tmp/ws_setup_logs" 348 | 349 | # 如果日志文件超过1MB,则清空它 350 | if [ -f "$CRON_LOG" ] && [ $(stat -c%s "$CRON_LOG" 2>/dev/null || stat -f%z "$CRON_LOG" 2>/dev/null) -gt 1048576 ]; then 351 | echo "=== 日志文件已重置 $(date) ===" > "$CRON_LOG" 352 | fi 353 | 354 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 执行定时检查..." >> "$CRON_LOG" 355 | 356 | # 调用WebSocket脚本的check_and_start_all函数 357 | if [ -f "$HOME/setup-ws.sh" ]; then 358 | bash "$HOME/setup-ws.sh" check_and_start_all 359 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] 已调用WebSocket脚本的check_and_start_all函数" >> "$CRON_LOG" 360 | else 361 | echo "[$(date '+%Y-%m-%d %H:%M:%S')] WebSocket脚本不存在,无法启动服务" >> "$CRON_LOG" 362 | fi 363 | } 364 | 365 | # 处理命令行参数 366 | if [ "$1" = "check_and_start_all" ]; then 367 | check_and_start_all 368 | exit 0 369 | fi 370 | 371 | # 执行主函数 372 | main --------------------------------------------------------------------------------