219 lines
7.3 KiB
JavaScript
219 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { request } = require("playwright");
|
|
|
|
const BASE_URL = (process.env.MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000").replace(/\/+$/, "");
|
|
const ROOT_PATH = process.env.MNOTE_WOLAI_PDF_SMOKE_ROOT
|
|
|| "/mnt/Data1T/Mnote_data/users/liaibo/workspaces/my-space";
|
|
const LIMIT = Number(process.env.MNOTE_WOLAI_PDF_SMOKE_LIMIT || 10);
|
|
const USERNAME = process.env.MNOTE_WOLAI_PDF_SMOKE_USER || "mnote.e2e@example.com";
|
|
const PASSWORD = process.env.MNOTE_WOLAI_PDF_SMOKE_PASSWORD || "MnoteE2E123!";
|
|
const OUT_DIR = path.join(process.cwd(), "tmp", "task802-pdf-attachments-restore-smoke");
|
|
const RESULT_PATH = path.join(OUT_DIR, "result.json");
|
|
|
|
function fileUrl(localPath) {
|
|
return `file://${localPath}`;
|
|
}
|
|
|
|
function encodeLocalIdSegment(value) {
|
|
const bytes = Buffer.from(String(value || ""), "utf8");
|
|
let encoded = "";
|
|
for (const byte of bytes) {
|
|
const character = String.fromCharCode(byte);
|
|
if (
|
|
(byte >= 48 && byte <= 57)
|
|
|| (byte >= 65 && byte <= 90)
|
|
|| (byte >= 97 && byte <= 122)
|
|
|| character === "."
|
|
|| character === "_"
|
|
|| character === "-"
|
|
) {
|
|
encoded += character;
|
|
} else {
|
|
encoded += `~${byte.toString(16).toUpperCase().padStart(2, "0")}`;
|
|
}
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
function markdownDocumentId(relativePath) {
|
|
return `local-md:${encodeLocalIdSegment(relativePath)}`;
|
|
}
|
|
|
|
function walkMarkdownFiles(dir, out = []) {
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.name === ".assets" || entry.name === ".git" || entry.name === "node_modules") {
|
|
continue;
|
|
}
|
|
const fullPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
walkMarkdownFiles(fullPath, out);
|
|
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
out.push(fullPath);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function rootRelativePath(filePath) {
|
|
return path.relative(ROOT_PATH, filePath).split(path.sep).join("/");
|
|
}
|
|
|
|
function normalizeHref(href) {
|
|
return String(href || "")
|
|
.trim()
|
|
.replace(/^<|>$/g, "")
|
|
.replace(/\\([\\`*_{}\[\]()#+\-.!|>])/g, "$1");
|
|
}
|
|
|
|
function pdfHrefs(markdown) {
|
|
const refs = [];
|
|
const source = String(markdown || "");
|
|
const linkPattern = /\[[^\]]*?\.pdf[^\]]*?\]\(\s*(?:<([^>]+?\.pdf)>|([^)\s]+?\.pdf))(?:\s+["'][^"']*["'])?\s*\)/gi;
|
|
for (const match of source.matchAll(linkPattern)) {
|
|
const href = normalizeHref(match[1] || match[2] || "");
|
|
if (!href || /^https?:\/\//i.test(href) || href.startsWith("#") || href.startsWith("mailto:")) {
|
|
continue;
|
|
}
|
|
refs.push(href);
|
|
}
|
|
return refs;
|
|
}
|
|
|
|
function collectExistingPdfAttachments() {
|
|
const candidates = [];
|
|
for (const markdownPath of walkMarkdownFiles(ROOT_PATH)) {
|
|
const markdown = fs.readFileSync(markdownPath, "utf8");
|
|
const markdownRelativePath = rootRelativePath(markdownPath);
|
|
for (const href of pdfHrefs(markdown)) {
|
|
if (!href.includes(".assets/")) {
|
|
continue;
|
|
}
|
|
const assetRelativePath = path.posix.normalize(path.posix.join(
|
|
path.posix.dirname(markdownRelativePath),
|
|
href,
|
|
));
|
|
const assetPath = path.join(ROOT_PATH, ...assetRelativePath.split("/"));
|
|
if (!fs.existsSync(assetPath)) {
|
|
continue;
|
|
}
|
|
const bytes = fs.statSync(assetPath).size;
|
|
if (bytes <= 0) {
|
|
continue;
|
|
}
|
|
candidates.push({
|
|
markdownRelativePath,
|
|
href,
|
|
assetRelativePath,
|
|
assetPath,
|
|
bytes,
|
|
});
|
|
if (candidates.length >= LIMIT) {
|
|
return candidates;
|
|
}
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
async function signIn(api) {
|
|
const response = await api.post("/api/auth", {
|
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
data: {
|
|
action: "auth:signIn",
|
|
args: {
|
|
provider: "password",
|
|
params: {
|
|
account: USERNAME,
|
|
password: PASSWORD,
|
|
flow: "signIn",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
assert.equal(response.status(), 200, `登录失败: ${response.status()} ${await response.text()}`);
|
|
}
|
|
|
|
function flattenBlockDocumentBlocks(blockDocument) {
|
|
const direct = Array.isArray(blockDocument?.blocks) ? blockDocument.blocks : [];
|
|
return direct;
|
|
}
|
|
|
|
function pageHasMediaSource(pageAggregate, href, assetRelativePath) {
|
|
const content = Array.isArray(pageAggregate?.body?.content) ? pageAggregate.body.content : [];
|
|
if (content.some((block) => block?.type === "media" && block?.props?.sourcePath === href)) {
|
|
return true;
|
|
}
|
|
const blocks = flattenBlockDocumentBlocks(pageAggregate?.body?.blockDocument);
|
|
return blocks.some((block) => (
|
|
block?.type === "media"
|
|
&& (block?.attrs?.sourcePath === href || block?.attrs?.sourcePath === assetRelativePath)
|
|
));
|
|
}
|
|
|
|
async function main() {
|
|
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
const candidates = collectExistingPdfAttachments();
|
|
assert.ok(candidates.length >= Math.min(5, LIMIT), `可验证 PDF 附件不足: ${candidates.length}`);
|
|
|
|
const api = await request.newContext({
|
|
baseURL: BASE_URL,
|
|
extraHTTPHeaders: { accept: "application/json" },
|
|
});
|
|
try {
|
|
await signIn(api);
|
|
const checked = [];
|
|
for (const candidate of candidates) {
|
|
const documentId = markdownDocumentId(candidate.markdownRelativePath);
|
|
const aggregateUrl = new URL(`/api/page-aggregate/${encodeURIComponent(documentId)}`, BASE_URL);
|
|
aggregateUrl.searchParams.set("sourceKind", "local_folder");
|
|
aggregateUrl.searchParams.set("rootUri", fileUrl(ROOT_PATH));
|
|
const response = await api.get(`${aggregateUrl.pathname}${aggregateUrl.search}`);
|
|
assert.equal(
|
|
response.status(),
|
|
200,
|
|
`page aggregate 失败 ${candidate.markdownRelativePath}: ${response.status()} ${await response.text()}`,
|
|
);
|
|
const payload = await response.json();
|
|
const aggregate = payload.result;
|
|
const attachmentRefs = Array.isArray(aggregate?.body?.attachmentRefs)
|
|
? aggregate.body.attachmentRefs
|
|
: [];
|
|
const ref = attachmentRefs.find((item) => (
|
|
item?.relativePath === candidate.assetRelativePath
|
|
|| item?.normalizedHref === candidate.href
|
|
|| item?.rawHref === candidate.href
|
|
));
|
|
assert.ok(ref, `aggregate 缺少 PDF attachmentRef: ${candidate.assetRelativePath}`);
|
|
assert.equal(String(ref.ext || "").toLowerCase(), "pdf", JSON.stringify(ref));
|
|
assert.equal(ref.exists, true, JSON.stringify(ref));
|
|
assert.equal(ref.authorized, true, JSON.stringify(ref));
|
|
assert.equal(ref.fileSize, candidate.bytes, JSON.stringify({ ref, candidate }));
|
|
assert.ok(
|
|
pageHasMediaSource(aggregate, candidate.href, candidate.assetRelativePath),
|
|
`aggregate 未还原 PDF media block: ${candidate.assetRelativePath}`,
|
|
);
|
|
checked.push({
|
|
markdownRelativePath: candidate.markdownRelativePath,
|
|
assetRelativePath: candidate.assetRelativePath,
|
|
bytes: candidate.bytes,
|
|
label: ref.label,
|
|
});
|
|
}
|
|
const result = { ok: true, checked };
|
|
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
console.log(JSON.stringify(result, null, 2));
|
|
} finally {
|
|
await api.dispose();
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error && error.stack ? error.stack : error);
|
|
process.exit(1);
|
|
});
|