feat: 收口文档桥接与 OnlyOffice/Sidebar 回归

- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器

- 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线

- 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
lix-2026
2026-04-15 03:06:29 +08:00
parent 84a8454fa9
commit b33ffb99e7
51 changed files with 3260 additions and 379 deletions
@@ -0,0 +1,93 @@
const DEFAULT_ONLYOFFICE_INTERNAL_URL = "http://127.0.0.1:8081";
const ONLYOFFICE_PROBE_PATH = "/web-apps/apps/api/documents/api.js";
const RESOLVE_CACHE_TTL_MS = 30_000;
const DEFAULT_ONLYOFFICE_INTERNAL_URL_CANDIDATES = [
DEFAULT_ONLYOFFICE_INTERNAL_URL,
"http://127.0.0.1:8082",
"http://localhost:8081",
"http://localhost:8082",
];
let cachedOnlyOfficeInternalUrl = "";
let cachedOnlyOfficeInternalUrlAt = 0;
let pendingOnlyOfficeInternalUrl: Promise<string> | null = null;
const normalizeOnlyOfficeInternalUrl = (raw?: string | null) => {
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 "";
}
};
const getOnlyOfficeInternalUrlCandidates = () => {
const candidates: string[] = [];
const push = (value?: string | null) => {
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);
}
for (const value of DEFAULT_ONLYOFFICE_INTERNAL_URL_CANDIDATES) {
push(value);
}
return candidates.length > 0 ? candidates : [DEFAULT_ONLYOFFICE_INTERNAL_URL];
};
const probeOnlyOfficeInternalUrl = async (candidate: string) => {
try {
const probeUrl = new URL(ONLYOFFICE_PROBE_PATH, `${candidate}/`);
const response = await fetch(probeUrl, {
method: "HEAD",
redirect: "follow",
cache: "no-store",
signal: AbortSignal.timeout(2_500),
});
return response.ok;
} catch {
return false;
}
};
export const resolveOnlyOfficeInternalUrl = async () => {
const now = Date.now();
if (cachedOnlyOfficeInternalUrl && now - cachedOnlyOfficeInternalUrlAt < RESOLVE_CACHE_TTL_MS) {
return cachedOnlyOfficeInternalUrl;
}
if (pendingOnlyOfficeInternalUrl) {
return pendingOnlyOfficeInternalUrl;
}
pendingOnlyOfficeInternalUrl = (async () => {
const candidates = getOnlyOfficeInternalUrlCandidates();
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 resolved;
} finally {
pendingOnlyOfficeInternalUrl = null;
}
};