feat: 收口文档桥接与 OnlyOffice/Sidebar 回归
- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器 - 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线 - 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user