2026-05-11 08:27:52 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require("node:fs");
|
|
|
|
|
|
const path = require("node:path");
|
|
|
|
|
|
const { chromium } = require("playwright");
|
|
|
|
|
|
const {
|
|
|
|
|
|
BASE_URL,
|
|
|
|
|
|
UI_TIMEOUT_MS,
|
|
|
|
|
|
assert,
|
|
|
|
|
|
createTempDocument,
|
|
|
|
|
|
ensureAuthenticated,
|
|
|
|
|
|
openDocument,
|
|
|
|
|
|
openFilesystemView,
|
|
|
|
|
|
purgeDocument,
|
|
|
|
|
|
} = require("./tree-shell-smoke-helpers");
|
|
|
|
|
|
|
|
|
|
|
|
const DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
|
|
|
|
const PNG_MIME = "image/png";
|
|
|
|
|
|
const TEST_EMAIL = "mnote.e2e@example.com";
|
|
|
|
|
|
const TEST_PASSWORD = "MnoteE2E123!";
|
2026-05-19 08:11:58 +08:00
|
|
|
|
// 说明:该 smoke 仍在验证 Convex media / OnlyOffice 的 cloud upload 兼容入口,不是 local-first 默认上传路径。
|
2026-05-11 08:27:52 +08:00
|
|
|
|
const PROBE_DOCX_PATH =
|
|
|
|
|
|
process.env.MNOTE_ONLYOFFICE_PROBE_DOCX ||
|
|
|
|
|
|
"/tmp/mnote-onlyoffice-manual/mnote-onlyoffice-upload-test.docx";
|
|
|
|
|
|
const SCREENSHOT_DIR =
|
|
|
|
|
|
process.env.MNOTE_UPLOAD_ENTRY_SCREENSHOT_DIR ||
|
|
|
|
|
|
"/mnt/Data1T/mnote/tmp/wolai-editor-parity/task175-rust-upload-entry";
|
|
|
|
|
|
|
|
|
|
|
|
function ensureProbeDocx() {
|
|
|
|
|
|
assert(fs.existsSync(PROBE_DOCX_PATH), `缺少 Office 探测文件:${PROBE_DOCX_PATH}`);
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: "task175-slash-attachment.docx",
|
|
|
|
|
|
mimeType: DOCX_MIME,
|
|
|
|
|
|
buffer: fs.readFileSync(PROBE_DOCX_PATH),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tinyPngBuffer() {
|
|
|
|
|
|
return Buffer.from(
|
|
|
|
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=",
|
|
|
|
|
|
"base64",
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function parseSetCookie(setCookie, origin) {
|
|
|
|
|
|
const [nameValue] = String(setCookie || "").split(";");
|
|
|
|
|
|
const separator = nameValue.indexOf("=");
|
|
|
|
|
|
if (separator <= 0) return null;
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: nameValue.slice(0, separator).trim(),
|
|
|
|
|
|
value: nameValue.slice(separator + 1).trim(),
|
|
|
|
|
|
domain: new URL(origin).hostname,
|
|
|
|
|
|
path: "/",
|
|
|
|
|
|
httpOnly: /;\s*httponly\b/i.test(setCookie),
|
|
|
|
|
|
sameSite: "Lax",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function forceTestAccountLogin(context) {
|
|
|
|
|
|
const response = await context.request.fetch(`${BASE_URL}/api/auth`, {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
data: {
|
|
|
|
|
|
action: "auth:signIn",
|
|
|
|
|
|
args: {
|
|
|
|
|
|
provider: "password",
|
|
|
|
|
|
params: {
|
|
|
|
|
|
email: TEST_EMAIL,
|
|
|
|
|
|
password: TEST_PASSWORD,
|
|
|
|
|
|
flow: "signIn",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
headers: { "content-type": "application/json" },
|
|
|
|
|
|
timeout: UI_TIMEOUT_MS,
|
|
|
|
|
|
});
|
|
|
|
|
|
const payload = await response.json().catch(async () => ({ raw: await response.text() }));
|
|
|
|
|
|
assert(response.ok(), `测试账号登录失败:${response.status()} ${JSON.stringify(payload)}`);
|
|
|
|
|
|
const cookies = response
|
|
|
|
|
|
.headersArray()
|
|
|
|
|
|
.filter((header) => header.name.toLowerCase() === "set-cookie")
|
|
|
|
|
|
.map((header) => parseSetCookie(header.value, BASE_URL))
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
assert(cookies.length > 0, "测试账号登录响应缺少 set-cookie");
|
|
|
|
|
|
await context.addCookies(cookies);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function waitForRuntimeEditor(page) {
|
|
|
|
|
|
const editor = page
|
|
|
|
|
|
.locator('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror[contenteditable="true"]')
|
|
|
|
|
|
.first();
|
|
|
|
|
|
await editor.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
return editor;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function waitForUploadResponse(page, fileName, action) {
|
|
|
|
|
|
const response = await page.waitForResponse(
|
|
|
|
|
|
async (candidate) => {
|
|
|
|
|
|
if (!candidate.url().includes("/api/media/upload") || candidate.request().method() !== "POST") {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
const payload = await candidate.json().catch(() => null);
|
|
|
|
|
|
return Boolean(payload?.asset?.id && (!fileName || payload.asset.file_name === fileName));
|
|
|
|
|
|
},
|
|
|
|
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
|
|
|
|
);
|
|
|
|
|
|
const payload = await response.json();
|
|
|
|
|
|
assert(response.ok(), `${action} 上传失败:${response.status()} ${JSON.stringify(payload)}`);
|
|
|
|
|
|
assert(payload?.asset?.id, `${action} 上传响应缺少 asset.id:${JSON.stringify(payload)}`);
|
|
|
|
|
|
return payload.asset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function waitForAssetRow(page, assetId, action) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
(targetAssetId) =>
|
|
|
|
|
|
Array.from(document.querySelectorAll(`[data-testid="filetree-asset-row"][data-asset-id="${targetAssetId}"]`)).some(
|
|
|
|
|
|
(row) =>
|
|
|
|
|
|
row instanceof HTMLElement &&
|
|
|
|
|
|
window.getComputedStyle(row).display !== "none" &&
|
|
|
|
|
|
window.getComputedStyle(row).visibility !== "hidden" &&
|
|
|
|
|
|
row.getClientRects().length > 0,
|
|
|
|
|
|
),
|
|
|
|
|
|
assetId,
|
|
|
|
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
const debug = await page.evaluate((targetAssetId) => ({
|
|
|
|
|
|
mode: document.documentElement.getAttribute("data-mnote-sidebar-tree-mode"),
|
|
|
|
|
|
lastUpload: document.documentElement.getAttribute("data-mnote-last-upload-asset-id"),
|
|
|
|
|
|
targetCount: document.querySelectorAll(`[data-testid="filetree-asset-row"][data-asset-id="${targetAssetId}"]`).length,
|
|
|
|
|
|
assets: Array.from(document.querySelectorAll('[data-testid="filetree-asset-row"]')).slice(-12).map((row) => ({
|
|
|
|
|
|
id: row.getAttribute("data-asset-id"),
|
|
|
|
|
|
visible: row instanceof HTMLElement && window.getComputedStyle(row).display !== "none" && window.getComputedStyle(row).visibility !== "hidden" && row.getClientRects().length > 0,
|
|
|
|
|
|
text: row.textContent,
|
|
|
|
|
|
})),
|
|
|
|
|
|
body: document.body.innerText.slice(0, 1200),
|
|
|
|
|
|
}), assetId).catch((debugError) => ({ debugError: String(debugError) }));
|
|
|
|
|
|
throw new Error(`${action} 上传后的文件树附件行不可见:${assetId} ${JSON.stringify(debug)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const title = await page.evaluate((targetAssetId) => {
|
|
|
|
|
|
const row = Array.from(document.querySelectorAll(`[data-testid="filetree-asset-row"][data-asset-id="${targetAssetId}"]`)).find(
|
|
|
|
|
|
(candidate) =>
|
|
|
|
|
|
candidate instanceof HTMLElement &&
|
|
|
|
|
|
window.getComputedStyle(candidate).display !== "none" &&
|
|
|
|
|
|
window.getComputedStyle(candidate).visibility !== "hidden" &&
|
|
|
|
|
|
candidate.getClientRects().length > 0,
|
|
|
|
|
|
);
|
|
|
|
|
|
return row?.querySelector(".tree-link-title")?.textContent?.trim() || "";
|
|
|
|
|
|
}, assetId);
|
|
|
|
|
|
assert(title.length > 0, `${action} 上传后的文件树附件行缺少标题`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function waitForEditorOfficeAttachment(page, assetId, fileName, action) {
|
|
|
|
|
|
const attachment = page
|
|
|
|
|
|
.locator(`.editor-surface .ProseMirror a[data-mnote-attachment-link="true"][href*="/onlyoffice"][href*="${assetId}"]`)
|
|
|
|
|
|
.first();
|
|
|
|
|
|
await attachment.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const text = (await attachment.innerText()).trim();
|
|
|
|
|
|
assert(text.includes(fileName), `${action} 正文附件标题不正确:${text}`);
|
|
|
|
|
|
const href = await attachment.getAttribute("href");
|
|
|
|
|
|
assert(href && href.includes("/onlyoffice?"), `${action} 正文 Office 附件 href 应指向 /onlyoffice,实际:${href}`);
|
|
|
|
|
|
assert(
|
|
|
|
|
|
href && href.startsWith("/onlyoffice?"),
|
|
|
|
|
|
`${action} 正文 Office 附件 href 应保存为相对 /onlyoffice 链接,实际:${href}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert(href.includes(`assetId=${encodeURIComponent(assetId)}`), `${action} 正文 Office 附件 href 缺少 assetId:${href}`);
|
|
|
|
|
|
return attachment;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function assertAttachmentActions(page, attachment, action) {
|
|
|
|
|
|
await attachment.hover({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const actions = page.locator('[data-testid="mnote-attachment-actions"]').first();
|
|
|
|
|
|
await actions.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await actions.locator('[data-testid="mnote-attachment-action-menu"]').click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const menu = page.locator('[data-testid="mnote-tree-context-menu"][data-kind="attachment"]').first();
|
|
|
|
|
|
await menu.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const text = await menu.innerText();
|
|
|
|
|
|
for (const label of ["拷贝副本", "删除", "复制链接", "弹窗预览", "右侧预览", "下载", "更换文件", "重命名", "添加说明文字"]) {
|
|
|
|
|
|
assert(text.includes(label), `${action} 附件三点菜单缺少“${label}”:${text}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
await page.keyboard.press("Escape").catch(() => undefined);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function screenshot(page, name) {
|
|
|
|
|
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
|
|
|
|
await page.screenshot({
|
|
|
|
|
|
path: path.join(SCREENSHOT_DIR, `${name}.png`),
|
|
|
|
|
|
fullPage: false,
|
|
|
|
|
|
timeout: UI_TIMEOUT_MS,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadViaSlash(page, itemTestId, filePayload, action) {
|
|
|
|
|
|
const editor = await waitForRuntimeEditor(page);
|
|
|
|
|
|
await editor.click({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await page.keyboard.type("/");
|
|
|
|
|
|
const slash = page.locator('[data-testid="mnote-leptos-tiptap-slash-menu"]').first();
|
|
|
|
|
|
await slash.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
const slashText = await slash.innerText();
|
|
|
|
|
|
assert(slashText.includes("媒体与附件"), `slash 菜单缺少媒体与附件分组:${slashText}`);
|
|
|
|
|
|
|
|
|
|
|
|
const item = page.locator(`[data-testid="${itemTestId}"]`).first();
|
|
|
|
|
|
await item.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await item.scrollIntoViewIfNeeded({ timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await screenshot(page, `slash-${itemTestId}`);
|
|
|
|
|
|
|
|
|
|
|
|
const [fileChooser] = await Promise.all([
|
|
|
|
|
|
page.waitForEvent("filechooser", { timeout: UI_TIMEOUT_MS }),
|
|
|
|
|
|
item.click({ timeout: UI_TIMEOUT_MS }),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const uploadResponse = waitForUploadResponse(page, filePayload.name, action);
|
|
|
|
|
|
await fileChooser.setFiles(filePayload);
|
|
|
|
|
|
const asset = await uploadResponse;
|
|
|
|
|
|
await waitForAssetRow(page, asset.id, action);
|
|
|
|
|
|
return asset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function dispatchFileDrop(page, selector, filePayload, action) {
|
|
|
|
|
|
const uploadPromise = waitForUploadResponse(page, filePayload.name, action);
|
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
|
({ selector, fileName, mimeType, bytes }) => {
|
|
|
|
|
|
const target = Array.from(document.querySelectorAll(selector)).find(
|
|
|
|
|
|
(candidate) => candidate instanceof HTMLElement && candidate.getClientRects().length > 0,
|
|
|
|
|
|
) || document.querySelector(selector);
|
|
|
|
|
|
if (!(target instanceof HTMLElement)) {
|
|
|
|
|
|
throw new Error(`拖放目标不存在:${selector}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
const dataTransfer = new DataTransfer();
|
|
|
|
|
|
dataTransfer.items.add(new File([new Uint8Array(bytes)], fileName, { type: mimeType }));
|
|
|
|
|
|
target.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true, dataTransfer }));
|
|
|
|
|
|
target.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true, dataTransfer }));
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
selector,
|
|
|
|
|
|
fileName: filePayload.name,
|
|
|
|
|
|
mimeType: filePayload.mimeType,
|
|
|
|
|
|
bytes: Array.from(filePayload.buffer),
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
const asset = await uploadPromise;
|
|
|
|
|
|
await waitForAssetRow(page, asset.id, action);
|
|
|
|
|
|
return asset;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
|
|
const browser = await chromium.launch({ headless: process.env.HEADFUL !== "1" });
|
|
|
|
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 960 } });
|
|
|
|
|
|
const page = await context.newPage();
|
|
|
|
|
|
const networkNotes = [];
|
|
|
|
|
|
page.on("response", async (response) => {
|
|
|
|
|
|
if (!/\/api\/(media\/upload|tree\/filetree\/upload-target-preflight)/.test(response.url())) return;
|
|
|
|
|
|
const text = await response.text().catch(() => "");
|
|
|
|
|
|
networkNotes.push({
|
|
|
|
|
|
status: response.status(),
|
|
|
|
|
|
url: response.url(),
|
|
|
|
|
|
body: text.slice(0, 1000),
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
page.on("pageerror", (error) => {
|
|
|
|
|
|
networkNotes.push({ type: "pageerror", message: error.message });
|
|
|
|
|
|
});
|
|
|
|
|
|
page.on("console", (message) => {
|
|
|
|
|
|
if (["error", "warning"].includes(message.type())) {
|
|
|
|
|
|
networkNotes.push({ type: message.type(), message: message.text().slice(0, 1000) });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
page.on("dialog", async (dialog) => {
|
|
|
|
|
|
networkNotes.push({ type: "dialog", message: dialog.message() });
|
|
|
|
|
|
await dialog.dismiss().catch(() => undefined);
|
|
|
|
|
|
});
|
|
|
|
|
|
let target = null;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
await forceTestAccountLogin(context);
|
|
|
|
|
|
await ensureAuthenticated(page, context.request);
|
|
|
|
|
|
target = await createTempDocument(context.request, null);
|
|
|
|
|
|
await openDocument(page, target.workspaceId, target.documentId);
|
|
|
|
|
|
await openFilesystemView(page);
|
|
|
|
|
|
|
|
|
|
|
|
const slashAttachment = ensureProbeDocx();
|
|
|
|
|
|
const slashImage = {
|
|
|
|
|
|
name: "task175-slash-image.png",
|
|
|
|
|
|
mimeType: PNG_MIME,
|
|
|
|
|
|
buffer: tinyPngBuffer(),
|
|
|
|
|
|
};
|
|
|
|
|
|
const treeDropAttachment = {
|
|
|
|
|
|
name: "task175-filetree-drop.docx",
|
|
|
|
|
|
mimeType: DOCX_MIME,
|
|
|
|
|
|
buffer: slashAttachment.buffer,
|
|
|
|
|
|
};
|
|
|
|
|
|
const editorDropImage = {
|
|
|
|
|
|
name: "task175-editor-drop.png",
|
|
|
|
|
|
mimeType: PNG_MIME,
|
|
|
|
|
|
buffer: tinyPngBuffer(),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const attachmentAsset = await uploadViaSlash(
|
|
|
|
|
|
page,
|
|
|
|
|
|
"slash-item-upload-attachment",
|
|
|
|
|
|
slashAttachment,
|
|
|
|
|
|
"slash 上传附件",
|
|
|
|
|
|
);
|
|
|
|
|
|
const editorAttachment = await waitForEditorOfficeAttachment(
|
|
|
|
|
|
page,
|
|
|
|
|
|
attachmentAsset.id,
|
|
|
|
|
|
slashAttachment.name,
|
|
|
|
|
|
"slash 上传附件",
|
|
|
|
|
|
);
|
|
|
|
|
|
await assertAttachmentActions(page, editorAttachment, "slash 上传附件");
|
|
|
|
|
|
await page.reload({ waitUntil: "domcontentloaded", timeout: UI_TIMEOUT_MS });
|
|
|
|
|
|
await waitForRuntimeEditor(page);
|
|
|
|
|
|
await waitForEditorOfficeAttachment(page, attachmentAsset.id, slashAttachment.name, "刷新后正文附件");
|
|
|
|
|
|
await openFilesystemView(page);
|
|
|
|
|
|
const imageAsset = await uploadViaSlash(page, "slash-item-image", slashImage, "slash 上传图片");
|
|
|
|
|
|
|
|
|
|
|
|
const docRowSelector = `[data-testid="filetree-doc-row"][data-document-id="${target.documentId}"]`;
|
|
|
|
|
|
const droppedAttachment = await dispatchFileDrop(page, docRowSelector, treeDropAttachment, "文件树拖入附件");
|
|
|
|
|
|
|
|
|
|
|
|
const editorStageSelector = '[data-testid="mnote-leptos-tiptap-editor-stage"]';
|
|
|
|
|
|
const droppedImage = await dispatchFileDrop(page, editorStageSelector, editorDropImage, "主编辑区拖入图片");
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
(fileName) => {
|
|
|
|
|
|
const images = Array.from(document.querySelectorAll(".editor-surface .ProseMirror img[src]"));
|
|
|
|
|
|
return images.some((image) => image.getAttribute("alt") === fileName || image.getAttribute("title") === fileName);
|
|
|
|
|
|
},
|
|
|
|
|
|
editorDropImage.name,
|
|
|
|
|
|
{ timeout: UI_TIMEOUT_MS },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
ok: true,
|
|
|
|
|
|
workspaceId: target.workspaceId,
|
|
|
|
|
|
documentId: target.documentId,
|
|
|
|
|
|
uploadedAssetIds: [
|
|
|
|
|
|
attachmentAsset.id,
|
|
|
|
|
|
imageAsset.id,
|
|
|
|
|
|
droppedAttachment.id,
|
|
|
|
|
|
droppedImage.id,
|
|
|
|
|
|
],
|
|
|
|
|
|
screenshotDir: SCREENSHOT_DIR,
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (networkNotes.length) {
|
|
|
|
|
|
console.error(`上传入口调试信息:${JSON.stringify(networkNotes.slice(-20), null, 2)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
if (target?.documentId) {
|
|
|
|
|
|
await purgeDocument(context.request, target.documentId).catch((error) => {
|
|
|
|
|
|
console.warn(`清理临时页面失败:${error instanceof Error ? error.message : String(error)}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
await context.close().catch(() => undefined);
|
|
|
|
|
|
await browser.close().catch(() => undefined);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch((error) => {
|
|
|
|
|
|
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|