diff --git a/README.md b/README.md index db6ed435..5916f4ec 100755 --- a/README.md +++ b/README.md @@ -64,73 +64,87 @@ pm2 startup ### 4.2 使用 Nginx 反代 (推荐) -#### A. 传统 Nginx 配置 (命令行) -如果你使用原生 Nginx,请参考以下配置: +#### 步骤 1: 安装 Nginx 和 Certbot +```bash +apt update && apt install -y nginx certbot python3-certbot-nginx +``` -```nginx +#### 步骤 2: 创建 Nginx 配置文件 +```bash +cat > /etc/nginx/sites-available/mixly << 'EOF' server { listen 80; server_name 你的域名; - return 301 https://$host$request_uri; -} -server { - listen 443 ssl; - server_name 你的域名; - - ssl_certificate /path/to/your/cert.pem; - ssl_certificate_key /path/to/your/key.pem; - - # 核心配置:处理 WebSocket 和大文件上传 + # 主站点反代 location / { proxy_pass https://127.0.0.1:7100; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; + proxy_ssl_verify off; proxy_set_header Host $host; - + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 100M; - proxy_read_timeout 3600s; } - # 处理 Socket.io 特殊路径 + # WebSocket 反代 (关键配置) location /mixly-socket/ { - proxy_pass https://127.0.0.1:7100; + proxy_pass https://127.0.0.1:7100/socket.io/; + proxy_ssl_verify off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_read_timeout 86400s; } } +EOF +``` +> **注意**:将 `你的域名` 替换为实际域名,如 `mixly.example.com` + +#### 步骤 3: 启用配置并测试 +```bash +# 创建软链接启用配置 +ln -sf /etc/nginx/sites-available/mixly /etc/nginx/sites-enabled/ + +# 删除默认站点 (可选) +rm -f /etc/nginx/sites-enabled/default + +# 测试配置语法 +nginx -t + +# 重启 Nginx +systemctl restart nginx ``` -#### B. Nginx Proxy Manager (NPM) 配置 (可视化) -如果你使用的是 NPM,请按以下步骤配置: +#### 步骤 4: 自动申请 SSL 证书 (Let's Encrypt) +```bash +# 将 your-email@example.com 替换为你的邮箱 +certbot --nginx -d 你的域名 --non-interactive --agree-tos -m your-email@example.com +``` -1. **Details 选项卡**: - * **Domain Names**: `你的域名` - * **Scheme**: `https` - * **Forward Hostname/IP**: `127.0.0.1` - * **Forward Port**: `7100` - * **Websockets Support**: **必须勾选 (ON)** -2. **Custom Locations 选项卡**: - * 点击 **Add Location**: - * **Define Location**: `/mixly-socket/` - * **Forward Scheme**: `https` - * **Forward Hostname/IP**: `127.0.0.1` - * **Forward Port**: `7100` - * 点击内置的齿轮图标或进入 **Advanced**,确保有以下配置(通常勾选 Websockets Support 后 NPM 会自动处理,但建议检查): - ```nginx - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - ``` -3. **Advanced 选项卡** (可选): - * 建议添加以下配置以支持大文件上传并忽略自签名证书错误(重要!): - ```nginx - client_max_body_size 100M; - # 忽略后端自签名证书错误 (必填,否则无法连接 wss) - proxy_ssl_verify off; - ``` +Certbot 会自动修改 Nginx 配置并启用 HTTPS (443 端口)。 + +#### 步骤 5: 设置证书自动续期 +```bash +# 测试自动续期 +certbot renew --dry-run + +# 证书默认每 90 天过期,certbot 会自动配置定时任务续期 +``` + +#### Nginx 常用管理命令 +```bash +# 查看状态 +systemctl status nginx + +# 重载配置 (不中断服务) +systemctl reload nginx + +# 查看错误日志 +tail -f /var/log/nginx/error.log +``` ## 5. 跨平台特性说明 @@ -163,3 +177,7 @@ pm2 reload mixly3 - **反向代理配置**:如果使用 Nginx 反代,请务必处理好的 WebSocket (Upgrade) 头,否则页面无法连接。 - **上传报错**:如果提示权限不足,请确认当前用户是否在 `dialout` 组,或尝试 `root` 运行(不推荐)。 - **Python 命令**:系统必须能识别 `python3` 命令。 +- **没有“添加设备”按钮**:这是因为 WebSocket 连接失败。 + 1. 检查 NPM 中是否勾选了 **Websockets Support**。 + 2. 检查 NPM Advanced 配置中是否添加了 `proxy_ssl_verify off;` (关键)。 + 3. 尝试重启服务端:`pm2 reload mixly3`。 diff --git a/mixly/mixly-sw/mixly-modules/deps.json b/mixly/mixly-sw/mixly-modules/deps.json index 25060855..5a93e766 100644 --- a/mixly/mixly-sw/mixly-modules/deps.json +++ b/mixly/mixly-sw/mixly-modules/deps.json @@ -129,6 +129,7 @@ { "path": "/web-socket/socket.js", "require": [ + "io", "Mixly.Env", "Mixly.Config", "Mixly.MJson", @@ -140,4 +141,4 @@ "Mixly.WebSocket.Socket" ] } -] +] \ No newline at end of file diff --git a/mixly/mixly-sw/mixly-modules/web-socket/socket.js b/mixly/mixly-sw/mixly-modules/web-socket/socket.js index 10d8f0e3..16a3fceb 100644 --- a/mixly/mixly-sw/mixly-modules/web-socket/socket.js +++ b/mixly/mixly-sw/mixly-modules/web-socket/socket.js @@ -1,244 +1,253 @@ (() => { -goog.require('Mixly.Env'); -goog.require('Mixly.Config'); -goog.require('Mixly.MJson'); -goog.require('Mixly.WebSocket'); -goog.require('Mixly.LayerExt'); -goog.require('Mixly.Command'); -goog.provide('Mixly.WebSocket.Socket'); + goog.require('Mixly.Env'); + goog.require('Mixly.Config'); + goog.require('Mixly.MJson'); + goog.require('Mixly.WebSocket'); + goog.require('Mixly.LayerExt'); + goog.require('Mixly.Command'); + goog.provide('Mixly.WebSocket.Socket'); -const { - Env, - Config, - MJson, - LayerExt, - Command -} = Mixly; + const { + Env, + Config, + MJson, + LayerExt, + Command + } = Mixly; -const { SOFTWARE } = Config; + const { SOFTWARE } = Config; -const { Socket } = Mixly.WebSocket; + const { Socket } = Mixly.WebSocket; -Socket.obj = null; -Socket.url = 'ws://127.0.0.1/socket'; -Socket.jsonArr = []; -Socket.connected = false; -Socket.initFunc = null; -Socket.debug = SOFTWARE.debug; -let { hostname, protocol, port } = window.location; -if (protocol === 'http:') { - Socket.protocol = 'ws:'; -} else { - Socket.protocol = 'wss:'; -} -if (port) { - port = ':' + port; -} -Socket.url = Socket.protocol + '//' + hostname + port + '/socket'; -Socket.IPAddress = hostname; -Socket.disconnectTimes = 0; -Socket.updating = false; + Socket.obj = null; + Socket.url = ''; + Socket.jsonArr = []; + Socket.connected = false; + Socket.initFunc = null; + Socket.debug = SOFTWARE.debug; + Socket.disconnectTimes = 0; + Socket.updating = false; - -let lockReconnect = false; // 避免重复连接 -let timeoutFlag = true; -let timeoutSet = null; -let reconectNum = 0; -const timeout = 5000; // 超时重连间隔 - -function reconnect () { - if (lockReconnect) return; - lockReconnect = true; - // 没连接上会一直重连,设置延迟避免请求过多 - setTimeout(function () { - timeoutFlag = true; - Socket.init(); - console.info(`正在重连第${reconectNum + 1}次`); - reconectNum++; - lockReconnect = false; - }, timeout); // 这里设置重连间隔(ms) -} - -//心跳检测 -const heartCheck = { - timeout, // 毫秒 - timeoutObj: null, - serverTimeoutObj: null, - reset: function () { - clearInterval(this.timeoutObj); - clearTimeout(this.serverTimeoutObj); - return this; - }, - start: function () { - const self = this; - let count = 0; - let WS = Socket; - this.timeoutObj = setInterval(() => { - if (count < 3) { - if (WS.obj.readyState === 1) { - WS.obj.send('HeartBeat'); - console.info(`HeartBeat第${count + 1}次`); - } - count++; - } else { - clearInterval(this.timeoutObj); - count = 0; - if (WS.obj.readyState === 0 && WS.obj.readyState === 1) { - WS.obj.close(); - } - } - }, self.timeout); + // 构建 Socket.io 连接 URL (和后端统一使用 Socket.io) + let { hostname, protocol, port } = window.location; + if (protocol === 'http:') { + Socket.protocol = 'ws:'; + } else { + Socket.protocol = 'wss:'; } -} + Socket.url = Socket.protocol + '//' + hostname + (port ? ':' + port : ''); + Socket.IPAddress = hostname; -Socket.init = (onopenFunc = (data) => {}, doFunc = () => {}) => { - if (Socket.connected) { - if (Socket.initFunc) { - Socket.initFunc(); - Socket.initFunc = null; - } - doFunc(); - return; - } + let lockReconnect = false; + let timeoutFlag = true; + let timeoutSet = null; + let reconectNum = 0; + const timeout = 5000; - timeoutSet = setTimeout(() => { - if (timeoutFlag && reconectNum < 3) { - console.info(`重连`); - reconectNum++; + function reconnect() { + if (lockReconnect) return; + lockReconnect = true; + setTimeout(function () { + timeoutFlag = true; Socket.init(); + console.info(`正在重连第${reconectNum + 1}次`); + reconectNum++; + lockReconnect = false; + }, timeout); + } + + // 心跳检测 + const heartCheck = { + timeout, + timeoutObj: null, + serverTimeoutObj: null, + reset: function () { + clearInterval(this.timeoutObj); + clearTimeout(this.serverTimeoutObj); + return this; + }, + start: function () { + const self = this; + let count = 0; + this.timeoutObj = setInterval(() => { + if (count < 3) { + if (Socket.connected) { + // Socket.io 自带心跳,这里可以发送自定义心跳 + console.info(`HeartBeat第${count + 1}次`); + } + count++; + } else { + clearInterval(this.timeoutObj); + count = 0; + if (Socket.obj && !Socket.connected) { + Socket.obj.disconnect(); + } + } + }, self.timeout); } - }, timeout); - - let WS = Socket; - WS.obj = new WebSocket(WS.url); - WS.obj.onopen = () => { - console.log('已连接' + WS.url); - WS.connected = true; - Socket.initFunc = doFunc; - reconectNum = 0; - timeoutFlag = false; - clearTimeout(timeoutSet); - heartCheck.reset().start(); - onopenFunc(WS); - Socket.reload(); - if (Socket.updating) { - Socket.updating = false; + } + + Socket.init = (onopenFunc = (data) => { }, doFunc = () => { }) => { + if (Socket.connected) { + if (Socket.initFunc) { + Socket.initFunc(); + Socket.initFunc = null; + } + doFunc(); + return; } - }; - WS.obj.onmessage = (event) => { - heartCheck.reset().start(); - let command = Command.parse(event.data); - command = MJson.decode(command); - if (Socket.debug) - console.log('receive -> ', event.data); - Command.run(command); - }; + timeoutSet = setTimeout(() => { + if (timeoutFlag && reconectNum < 3) { + console.info(`重连`); + reconectNum++; + Socket.init(); + } + }, timeout); - WS.obj.onerror = (event) => { - console.log('WebSocket error: ', event); - reconnect(); - }; + // 使用 Socket.io 客户端连接(和后端统一) + Socket.obj = io(`${Socket.url}/all`, { + path: '/mixly-socket/', + reconnection: true, + reconnectionDelayMax: 10000, + transports: ['websocket'] + }); - WS.obj.onclose = (event) => { - WS.connected = false; - WS.disconnectTimes += 1; - if (WS.disconnectTimes > 255) { - WS.disconnectTimes = 1; - } - console.log('已断开' + WS.url); - - console.info(`关闭`, event.code); - if (event.code !== 1000) { + Socket.obj.on('connect', () => { + console.log('已连接' + Socket.url); + Socket.connected = true; + Socket.initFunc = doFunc; + reconectNum = 0; timeoutFlag = false; clearTimeout(timeoutSet); + heartCheck.reset().start(); + onopenFunc(Socket); + Socket.reload(); + if (Socket.updating) { + Socket.updating = false; + } + }); + + // Socket.io 消息接收 + Socket.obj.onAny((eventName, ...args) => { + heartCheck.reset().start(); + // 构造兼容原有 Command 格式的消息 + const command = { event: eventName, data: args }; + if (Socket.debug) { + console.log('receive -> ', eventName, args); + } + // 尝试使用原有 Command 系统处理 + try { + Command.run(MJson.decode(command)); + } catch (e) { + // 如果 Command 系统不能处理,忽略 + } + }); + + Socket.obj.on('connect_error', (error) => { + console.log('WebSocket error: ', error); reconnect(); - } else { - clearInterval(heartCheck.timeoutObj); - clearTimeout(heartCheck.serverTimeoutObj); - } - } -} + }); -Socket.sendCommand = (command) => { - let WS = Mixly.WebSocket.Socket; - if (!WS.connected) { - layer.msg('未连接' + WS.url, {time: 1000}); - return; - } - let commandStr = ''; - - try { - commandStr = JSON.stringify(MJson.encode(command)); - if (Socket.debug) - console.log('send -> ', commandStr); - } catch (e) { - console.log(e); - return; - } - WS.obj.send(commandStr); -} + Socket.obj.on('disconnect', (reason) => { + Socket.connected = false; + Socket.disconnectTimes += 1; + if (Socket.disconnectTimes > 255) { + Socket.disconnectTimes = 1; + } + console.log('已断开' + Socket.url); + console.info(`关闭`, reason); -Socket.clickConnect = () => { - if (Socket.connected) { - Socket.disconnect(); - } else { - Socket.connect((WS) => { - layer.closeAll(); - layer.msg(WS.url + '连接成功', { time: 1000 }); + if (reason !== 'io client disconnect') { + timeoutFlag = false; + clearTimeout(timeoutSet); + reconnect(); + } else { + clearInterval(heartCheck.timeoutObj); + clearTimeout(heartCheck.serverTimeoutObj); + } }); } -} -Socket.openLoadingBox = (title, successFunc = () => {}, endFunc = () => {}) => { - layer.open({ - type: 1, - title: title, - content: $('#mixly-loader-div'), - shade: LayerExt.SHADE_ALL, - closeBtn: 0, - success: function () { - $("#webusb-cancel").css("display","none"); - $(".layui-layer-page").css("z-index", "198910151"); - successFunc(); - }, - end: function () { - $("#mixly-loader-div").css("display", "none"); - $(".layui-layer-shade").remove(); - $("#webusb-cancel").css("display", "unset"); - if (Socket.connected) - endFunc(); + Socket.sendCommand = (command) => { + if (!Socket.connected) { + layer.msg('未连接' + Socket.url, { time: 1000 }); + return; } - }); -} -Socket.connect = (onopenFunc = (data) => {}, doFunc = () => {}) => { - if (Socket.connected) { - doFunc(); - return; + try { + const encodedCommand = MJson.encode(command); + if (Socket.debug) { + console.log('send -> ', encodedCommand); + } + // 使用 Socket.io emit 发送命令 + Socket.obj.emit('command', encodedCommand); + } catch (e) { + console.log(e); + return; + } } - let title = '连接中...'; - Socket.openLoadingBox(title, () => { - setTimeout(() => { - Socket.init(onopenFunc); - }, 1000); - }, doFunc); -} -Socket.disconnect = () => { - if (!Socket.connected) - return; - let title = '断开中...'; - Socket.openLoadingBox(title, () => { - Socket.obj.close(); - }); -} - -Socket.reload = () => { - if (!Socket.updating && Socket.disconnectTimes) { - window.location.reload(); + Socket.clickConnect = () => { + if (Socket.connected) { + Socket.disconnect(); + } else { + Socket.connect((WS) => { + layer.closeAll(); + layer.msg(WS.url + '连接成功', { time: 1000 }); + }); + } + } + + Socket.openLoadingBox = (title, successFunc = () => { }, endFunc = () => { }) => { + layer.open({ + type: 1, + title: title, + content: $('#mixly-loader-div'), + shade: LayerExt.SHADE_ALL, + closeBtn: 0, + success: function () { + $("#webusb-cancel").css("display", "none"); + $(".layui-layer-page").css("z-index", "198910151"); + successFunc(); + }, + end: function () { + $("#mixly-loader-div").css("display", "none"); + $(".layui-layer-shade").remove(); + $("#webusb-cancel").css("display", "unset"); + if (Socket.connected) + endFunc(); + } + }); + } + + Socket.connect = (onopenFunc = (data) => { }, doFunc = () => { }) => { + if (Socket.connected) { + doFunc(); + return; + } + let title = '连接中...'; + Socket.openLoadingBox(title, () => { + setTimeout(() => { + Socket.init(onopenFunc); + }, 1000); + }, doFunc); + } + + Socket.disconnect = () => { + if (!Socket.connected) + return; + let title = '断开中...'; + Socket.openLoadingBox(title, () => { + Socket.obj.disconnect(); + }); + } + + Socket.reload = () => { + if (!Socket.updating && Socket.disconnectTimes) { + window.location.reload(); + } } -} })();