#!/usr/bin/env node /** * 自定义 Next dev server: * - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。 * - Next App Router 的 Route Handler 无法处理 Upgrade,因此必须在 Node http server 层做透传。 * * 用法(保持与 next dev 类似): * - pnpm dev -p 3000 * - node scripts/dev-server.js -p 3000 * * 依赖环境变量: * - ONLYOFFICE_INTERNAL_URL:默认 http://127.0.0.1:8081 */ const http = require("http"); const net = require("net"); const path = require("path"); const next = require("next"); const { parse: parseUrl } = require("url"); const ONLYOFFICE_PREFIX = "/onlyoffice-server"; function readArgValue(flag) { const idx = process.argv.findIndex((x) => x === flag); if (idx === -1) return null; const v = process.argv[idx + 1]; if (!v || v.startsWith("-")) return null; return v; } function resolvePort() { const fromArg = readArgValue("-p") || readArgValue("--port"); const raw = fromArg || process.env.PORT || "3000"; const n = Number(raw); return Number.isFinite(n) ? Math.max(1, Math.min(65535, Math.floor(n))) : 3000; } function resolveHostname() { return readArgValue("-H") || readArgValue("--hostname") || process.env.HOSTNAME || "0.0.0.0"; } function isOnlyOfficePath(urlString) { try { const u = new URL(urlString, "http://localhost"); return u.pathname === ONLYOFFICE_PREFIX || u.pathname.startsWith(`${ONLYOFFICE_PREFIX}/`); } catch { return false; } } function buildUpstreamRequestHead(req, targetUrl) { const incoming = new URL(req.url || "/", "http://localhost"); const rawPath = incoming.pathname || "/"; const stripped = rawPath === ONLYOFFICE_PREFIX ? "/" : rawPath.slice(ONLYOFFICE_PREFIX.length) || "/"; const basePath = String(targetUrl.pathname || "/").replace(/\/+$/, "") || ""; const upstreamPath = `${basePath}${stripped}`.replace(/\/{2,}/g, "/") + (incoming.search || ""); const lines = []; lines.push(`${req.method || "GET"} ${upstreamPath} HTTP/1.1`); const headers = req.headers || {}; for (const [k, v] of Object.entries(headers)) { if (!v) continue; const key = String(k); if (key.toLowerCase() === "host") continue; if (Array.isArray(v)) { lines.push(`${key}: ${v.join(", ")}`); } else { lines.push(`${key}: ${String(v)}`); } } // 说明:Host 必须指向 ONLYOFFICE_INTERNAL_URL,否则上游可能拒绝 Upgrade。 lines.push(`Host: ${targetUrl.host}`); lines.push(""); lines.push(""); return lines.join("\r\n"); } function proxyOnlyOfficeUpgrade(req, socket, head) { const target = new URL((process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081").replace(/\/+$/, "") + "/"); const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80); const upstream = net.connect({ host: target.hostname, port }, () => { try { const reqHead = buildUpstreamRequestHead(req, target); upstream.write(reqHead); if (head && head.length > 0) upstream.write(head); socket.pipe(upstream); upstream.pipe(socket); } catch (e) { try { socket.destroy(); } catch {} try { upstream.destroy(); } catch {} } }); const onError = (err) => { try { const msg = err && err.message ? String(err.message) : String(err || ""); // eslint-disable-next-line no-console console.log("[dev-server][onlyoffice-ws] proxy error", msg); } catch {} try { socket.destroy(); } catch {} try { upstream.destroy(); } catch {} }; upstream.on("error", onError); socket.on("error", onError); } async function main() { const port = resolvePort(); const hostname = resolveHostname(); const dev = true; const app = next({ dev, dir: path.join(__dirname, "..") }); const handle = app.getRequestHandler(); await app.prepare(); // 说明:Next dev 的 HMR 依赖 WebSocket(/_next/webpack-hmr),需要交给 Next 自己处理 upgrade。 const handleUpgrade = typeof app.getUpgradeHandler === "function" ? app.getUpgradeHandler() : null; const server = http.createServer((req, res) => { try { res.setHeader("x-mnote-dev-server", "1"); res.setHeader("x-mnote-onlyoffice-ws-proxy", "1"); } catch { // ignore } const parsed = parseUrl(req.url || "/", true); handle(req, res, parsed); }); server.on("upgrade", (req, socket, head) => { if (isOnlyOfficePath(req.url || "/")) { try { // eslint-disable-next-line no-console console.log("[dev-server][onlyoffice-ws] upgrade", req.url); } catch {} proxyOnlyOfficeUpgrade(req, socket, head); return; } if (handleUpgrade) { handleUpgrade(req, socket, head); return; } try { socket.destroy(); } catch { // ignore } }); server.listen(port, hostname, () => { // eslint-disable-next-line no-console console.log(`[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081"})`); }); } main().catch((err) => { // eslint-disable-next-line no-console console.error(err instanceof Error ? err.stack : String(err)); process.exit(1); });