Files

219 lines
7.3 KiB
JavaScript
Raw Permalink Normal View History

#!/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);
});