feat: 收口文档桥接与 OnlyOffice/Sidebar 回归
- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器 - 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线 - 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
@@ -4,13 +4,13 @@
|
||||
* - 解决 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
|
||||
*/
|
||||
* 用法(保持与 next dev 类似):
|
||||
* - pnpm dev -p 3000
|
||||
* - node scripts/dev-server.js -p 3000
|
||||
*
|
||||
* 依赖环境变量:
|
||||
* - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时会自动探测 8081/8082
|
||||
*/
|
||||
|
||||
const http = require("http");
|
||||
const net = require("net");
|
||||
@@ -18,8 +18,15 @@ const path = require("path");
|
||||
const next = require("next");
|
||||
const { parse: parseUrl } = require("url");
|
||||
|
||||
const ONLYOFFICE_PREFIX = "/onlyoffice-server";
|
||||
const CONVEX_PREFIX = "/convex";
|
||||
const ONLYOFFICE_PREFIX = "/onlyoffice-server";
|
||||
const CONVEX_PREFIX = "/convex";
|
||||
const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8081";
|
||||
const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js";
|
||||
const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000;
|
||||
|
||||
let cachedOnlyOfficeInternalUrl = "";
|
||||
let cachedOnlyOfficeInternalUrlAt = 0;
|
||||
let pendingOnlyOfficeInternalUrl = null;
|
||||
|
||||
function readArgValue(flag) {
|
||||
const idx = process.argv.findIndex((x) => x === flag);
|
||||
@@ -49,14 +56,92 @@ function isOnlyOfficePath(urlString) {
|
||||
}
|
||||
}
|
||||
|
||||
function isConvexPath(urlString) {
|
||||
try {
|
||||
const u = new URL(urlString, "http://localhost");
|
||||
return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
|
||||
} catch {
|
||||
function isConvexPath(urlString) {
|
||||
try {
|
||||
const u = new URL(urlString, "http://localhost");
|
||||
return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOnlyOfficeInternalUrl(raw) {
|
||||
const value = String(raw || "").trim().replace(/\/+$/, "");
|
||||
if (!value) return "";
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
||||
return url.toString().replace(/\/+$/, "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function listOnlyOfficeInternalUrlCandidates() {
|
||||
const candidates = [];
|
||||
const push = (value) => {
|
||||
const normalized = normalizeOnlyOfficeInternalUrl(value);
|
||||
if (!normalized) return;
|
||||
if (!candidates.includes(normalized)) candidates.push(normalized);
|
||||
};
|
||||
|
||||
push(process.env.ONLYOFFICE_INTERNAL_URL);
|
||||
|
||||
for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || "").split(",")) {
|
||||
push(raw);
|
||||
}
|
||||
|
||||
push(DEFAULT_ONLYOFFICE_INTERNAL_URL);
|
||||
push("http://127.0.0.1:8082");
|
||||
push("http://localhost:8081");
|
||||
push("http://localhost:8082");
|
||||
|
||||
return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL];
|
||||
}
|
||||
|
||||
async function probeOnlyOfficeInternalUrl(candidate) {
|
||||
if (typeof fetch !== "function") return false;
|
||||
try {
|
||||
const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);
|
||||
const response = await fetch(probeUrl, {
|
||||
method: "HEAD",
|
||||
redirect: "follow",
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveOnlyOfficeInternalUrl() {
|
||||
const now = Date.now();
|
||||
if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < ONLYOFFICE_RESOLVE_CACHE_TTL_MS) {
|
||||
return new URL(`${cachedOnlyOfficeInternalUrl}/`);
|
||||
}
|
||||
|
||||
if (!pendingOnlyOfficeInternalUrl) {
|
||||
pendingOnlyOfficeInternalUrl = (async () => {
|
||||
const candidates = listOnlyOfficeInternalUrlCandidates();
|
||||
for (const candidate of candidates) {
|
||||
if (await probeOnlyOfficeInternalUrl(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL;
|
||||
})();
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = await pendingOnlyOfficeInternalUrl;
|
||||
cachedOnlyOfficeInternalUrl = resolved;
|
||||
cachedOnlyOfficeInternalUrlAt = Date.now();
|
||||
return new URL(`${resolved}/`);
|
||||
} finally {
|
||||
pendingOnlyOfficeInternalUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUpstreamRequestHead(req, targetUrl, prefix) {
|
||||
const incoming = new URL(req.url || "/", "http://localhost");
|
||||
@@ -127,9 +212,21 @@ function buildUpstreamRequestHead(req, targetUrl, prefix) {
|
||||
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);
|
||||
async function proxyOnlyOfficeUpgrade(req, socket, head) {
|
||||
let target;
|
||||
try {
|
||||
target = await resolveOnlyOfficeInternalUrl();
|
||||
} catch (err) {
|
||||
try {
|
||||
const msg = err && err.message ? String(err.message) : String(err || "");
|
||||
console.log("[dev-server][onlyoffice-ws] resolve error", msg);
|
||||
} catch {}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);
|
||||
|
||||
const upstream = net.connect({ host: target.hostname, port }, () => {
|
||||
try {
|
||||
@@ -352,13 +449,20 @@ async function main() {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
|
||||
console.log(
|
||||
`[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081"}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
server.listen(port, hostname, () => {
|
||||
resolveOnlyOfficeInternalUrl()
|
||||
.then((onlyofficeTarget) => {
|
||||
console.log(
|
||||
`[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${onlyofficeTarget.toString().replace(/\/$/, "")}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
`[dev-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || DEFAULT_ONLYOFFICE_INTERNAL_URL}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
* - 解决 ONLYOFFICE 在 `/onlyoffice-server/*` 下的 WebSocket Upgrade 需求(socket.io / coauthoring)。
|
||||
* - 解决 Convex 在 HTTPS(frp/nginx)场景下浏览器不能连接 ws:// 的问题:通过同源 `/convex/*` 反代到本机 Convex。
|
||||
*
|
||||
* 用法:
|
||||
* - pnpm build
|
||||
* - pnpm start (默认会执行本脚本)
|
||||
*
|
||||
* 依赖环境变量:
|
||||
* - ONLYOFFICE_INTERNAL_URL:默认 http://127.0.0.1:8081
|
||||
* - CONVEX_INTERNAL_URL:默认 http://127.0.0.1:3210
|
||||
*/
|
||||
* 用法:
|
||||
* - pnpm build
|
||||
* - pnpm start (默认会执行本脚本)
|
||||
*
|
||||
* 依赖环境变量:
|
||||
* - ONLYOFFICE_INTERNAL_URL:可显式指定;未指定或失效时会自动探测 8081/8082
|
||||
* - CONVEX_INTERNAL_URL:默认 http://127.0.0.1:3210
|
||||
*/
|
||||
|
||||
const http = require("http");
|
||||
const net = require("net");
|
||||
@@ -19,8 +19,15 @@ const path = require("path");
|
||||
const next = require("next");
|
||||
const { parse: parseUrl } = require("url");
|
||||
|
||||
const ONLYOFFICE_PREFIX = "/onlyoffice-server";
|
||||
const CONVEX_PREFIX = "/convex";
|
||||
const ONLYOFFICE_PREFIX = "/onlyoffice-server";
|
||||
const CONVEX_PREFIX = "/convex";
|
||||
const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8081";
|
||||
const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js";
|
||||
const ONLYOFFICE_RESOLVE_CACHE_TTL_MS = 30_000;
|
||||
|
||||
let cachedOnlyOfficeInternalUrl = "";
|
||||
let cachedOnlyOfficeInternalUrlAt = 0;
|
||||
let pendingOnlyOfficeInternalUrl = null;
|
||||
|
||||
function readArgValue(flag) {
|
||||
const idx = process.argv.findIndex((x) => x === flag);
|
||||
@@ -50,14 +57,92 @@ function isOnlyOfficePath(urlString) {
|
||||
}
|
||||
}
|
||||
|
||||
function isConvexPath(urlString) {
|
||||
try {
|
||||
const u = new URL(urlString, "http://localhost");
|
||||
return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
|
||||
} catch {
|
||||
function isConvexPath(urlString) {
|
||||
try {
|
||||
const u = new URL(urlString, "http://localhost");
|
||||
return u.pathname === CONVEX_PREFIX || u.pathname.startsWith(`${CONVEX_PREFIX}/`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOnlyOfficeInternalUrl(raw) {
|
||||
const value = String(raw || "").trim().replace(/\/+$/, "");
|
||||
if (!value) return "";
|
||||
try {
|
||||
const url = new URL(value);
|
||||
if (url.protocol !== "http:" && url.protocol !== "https:") return "";
|
||||
return url.toString().replace(/\/+$/, "");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function listOnlyOfficeInternalUrlCandidates() {
|
||||
const candidates = [];
|
||||
const push = (value) => {
|
||||
const normalized = normalizeOnlyOfficeInternalUrl(value);
|
||||
if (!normalized) return;
|
||||
if (!candidates.includes(normalized)) candidates.push(normalized);
|
||||
};
|
||||
|
||||
push(process.env.ONLYOFFICE_INTERNAL_URL);
|
||||
|
||||
for (const raw of String(process.env.ONLYOFFICE_INTERNAL_URL_CANDIDATES || "").split(",")) {
|
||||
push(raw);
|
||||
}
|
||||
|
||||
push(DEFAULT_ONLYOFFICE_INTERNAL_URL);
|
||||
push("http://127.0.0.1:8082");
|
||||
push("http://localhost:8081");
|
||||
push("http://localhost:8082");
|
||||
|
||||
return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL];
|
||||
}
|
||||
|
||||
async function probeOnlyOfficeInternalUrl(candidate) {
|
||||
if (typeof fetch !== "function") return false;
|
||||
try {
|
||||
const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);
|
||||
const response = await fetch(probeUrl, {
|
||||
method: "HEAD",
|
||||
redirect: "follow",
|
||||
cache: "no-store",
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveOnlyOfficeInternalUrl() {
|
||||
const now = Date.now();
|
||||
if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < ONLYOFFICE_RESOLVE_CACHE_TTL_MS) {
|
||||
return new URL(`${cachedOnlyOfficeInternalUrl}/`);
|
||||
}
|
||||
|
||||
if (!pendingOnlyOfficeInternalUrl) {
|
||||
pendingOnlyOfficeInternalUrl = (async () => {
|
||||
const candidates = listOnlyOfficeInternalUrlCandidates();
|
||||
for (const candidate of candidates) {
|
||||
if (await probeOnlyOfficeInternalUrl(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return candidates[0] || DEFAULT_ONLYOFFICE_INTERNAL_URL;
|
||||
})();
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = await pendingOnlyOfficeInternalUrl;
|
||||
cachedOnlyOfficeInternalUrl = resolved;
|
||||
cachedOnlyOfficeInternalUrlAt = Date.now();
|
||||
return new URL(`${resolved}/`);
|
||||
} finally {
|
||||
pendingOnlyOfficeInternalUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildUpstreamRequestHead(req, targetUrl, prefix) {
|
||||
const incoming = new URL(req.url || "/", "http://localhost");
|
||||
@@ -123,9 +208,21 @@ function buildUpstreamRequestHead(req, targetUrl, prefix) {
|
||||
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);
|
||||
async function proxyOnlyOfficeUpgrade(req, socket, head) {
|
||||
let target;
|
||||
try {
|
||||
target = await resolveOnlyOfficeInternalUrl();
|
||||
} catch (err) {
|
||||
try {
|
||||
const msg = err && err.message ? String(err.message) : String(err || "");
|
||||
console.log("[prod-server][onlyoffice-ws] resolve error", msg);
|
||||
} catch {}
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
const port = Number(target.port) || (target.protocol === "https:" ? 443 : 80);
|
||||
|
||||
const upstream = net.connect({ host: target.hostname, port }, () => {
|
||||
try {
|
||||
@@ -316,12 +413,20 @@ async function main() {
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, hostname, () => {
|
||||
console.log(
|
||||
`[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || "http://127.0.0.1:8081"}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
});
|
||||
}
|
||||
server.listen(port, hostname, () => {
|
||||
resolveOnlyOfficeInternalUrl()
|
||||
.then((onlyofficeTarget) => {
|
||||
console.log(
|
||||
`[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${onlyofficeTarget.toString().replace(/\/$/, "")}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
console.log(
|
||||
`[prod-server] ready http://${hostname}:${port} (ONLYOFFICE ws via ${ONLYOFFICE_PREFIX} -> ${process.env.ONLYOFFICE_INTERNAL_URL || DEFAULT_ONLYOFFICE_INTERNAL_URL}; Convex ws via ${CONVEX_PREFIX} -> ${process.env.CONVEX_INTERNAL_URL || "http://127.0.0.1:3210"})`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err instanceof Error ? err.stack : String(err));
|
||||
|
||||
Reference in New Issue
Block a user