2026-05-20 14:20:48 +08:00
#!/usr/bin/env node
"use strict" ;
2026-07-28 17:04:27 +08:00
const { loginViaAuthForm } = require ( './lib/browser-auth-login' );
2026-05-20 14:20:48 +08:00
const assert = require ( "node:assert" );
const fs = require ( "node:fs" );
const os = require ( "node:os" );
const path = require ( "node:path" );
const { chromium } = require ( "playwright" );
const BASE_URL = ( process . env . MNOTE_WEB_SMOKE_BASE_URL || "http://127.0.0.1:3000" ). replace ( /\/+$/ , "" );
const UI_TIMEOUT_MS = Number ( process . env . MNOTE_SMOKE_UI_TIMEOUT_MS || 30_000 );
const CHROMIUM_EXECUTABLE_PATH = process . env . PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|| [ "/usr/bin/google-chrome-stable" , "/usr/bin/google-chrome" , "/snap/bin/chromium" ]
. find (( candidate ) => fs . existsSync ( candidate ));
function fileUrl ( localPath ) {
return `file:// ${ localPath } ` ;
}
function localMdDocumentId ( relativePath ) {
return `local-md: ${ relativePath . replaceAll ( "/" , "~2F" ) } ` ;
}
function documentUrl ( root , relativePath , secondaryRelativePath = "" ) {
const url = new URL ( ` ${ BASE_URL } /documents/ ${ encodeURIComponent ( localMdDocumentId ( relativePath )) } ` );
url . searchParams . set ( "sourceKind" , "local_folder" );
url . searchParams . set ( "rootUri" , fileUrl ( root ));
url . searchParams . set ( "treeView" , "filetree" );
if ( secondaryRelativePath ) {
url . searchParams . set ( "secondaryDocumentId" , localMdDocumentId ( secondaryRelativePath ));
url . searchParams . set ( "secondarySourceKind" , "local_folder" );
url . searchParams . set ( "secondaryRootUri" , fileUrl ( root ));
}
return url . toString ();
}
function writeWorkspaceManifest ( root , ownerId ) {
const metadataDir = path . join ( root , ".mnote" );
fs . mkdirSync ( metadataDir , { recursive : true });
fs . writeFileSync (
path . join ( metadataDir , "workspace.json" ),
` ${ JSON . stringify ({
workspaceId : `local-ws: ${ ownerId } :task457` ,
ownerId ,
createdAt : new Date (). toISOString (),
capabilities : [ "local_files" , "markdown_edit" , "asset_upload" ],
} , null, 2)}\n` ,
"utf8" ,
);
}
async function quickLogin ( page ) {
2026-07-28 17:04:27 +08:00
// 7-76 P0: 标准表单登录(无测试快速登录按钮)
const base =
( typeof BASE_URL !== "undefined" && BASE_URL ) ||
( typeof baseUrl !== "undefined" && baseUrl ) ||
process . env . MNOTE_UI_BASE_URL ||
"http://127.0.0.1:3000" ;
const timeout =
( typeof UI_TIMEOUT_MS !== "undefined" && UI_TIMEOUT_MS ) ||
( typeof TIMEOUT !== "undefined" && TIMEOUT ) ||
30_000 ;
if ( ! String ( page . url () || "" ). includes ( "/auth" )) {
await page . goto ( String ( base ). replace ( /\/+$/ , "" ) + "/auth" , {
waitUntil : "commit" ,
timeout ,
});
2026-05-20 14:20:48 +08:00
}
2026-07-28 17:04:27 +08:00
await loginViaAuthForm ( page , {
baseUrl : base ,
timeoutMs : timeout ,
gotoAuth : false ,
});
await page
. waitForURL (( url ) => ! String ( url ). includes ( "/auth" ), { timeout })
. catch (() => {});
2026-05-20 14:20:48 +08:00
}
async function uploadLocalAsset ( page , root , documentId , fileName , mimeType , bytes , kind ) {
return await page . evaluate (
async ({ rootUri , documentId , fileName , mimeType , bytes , kind }) => {
const form = new FormData ();
form . append ( "rootUri" , rootUri );
form . append ( "documentId" , documentId );
2026-05-28 22:01:44 +08:00
form . append ( "uploadIntent" , "editor.markdown.attach" );
2026-05-20 14:20:48 +08:00
form . append ( "kind" , kind );
form . append ( "file" , new File ([ new Uint8Array ( bytes )], fileName , { type : mimeType }));
const response = await fetch ( "/api/local-folder/assets/upload" , { method : "POST" , body : form });
const payload = await response . json (). catch (() => null );
if ( ! response . ok || ! payload || payload . ok !== true ) {
throw new Error ( `upload_failed_ ${ response . status } : ${ JSON . stringify ( payload ) } ` );
}
return payload . asset ;
},
{ rootUri : fileUrl ( root ), documentId , fileName , mimeType , bytes : Array . from ( bytes ), kind },
);
}
async function main () {
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "mnote-task457-resource-tabs-" ));
const relativePath = "README.md" ;
const documentId = localMdDocumentId ( relativePath );
const sideRelativePath = "Side.md" ;
writeWorkspaceManifest ( root , "user_real" );
fs . writeFileSync ( path . join ( root , relativePath ), "# Page\n\n页面正文保持不变\n" , "utf8" );
fs . writeFileSync ( path . join ( root , sideRelativePath ), "# Side\n\n右侧栏保持打开\n" , "utf8" );
const browser = await chromium . launch ({
headless : true ,
executablePath : CHROMIUM_EXECUTABLE_PATH ,
});
const context = await browser . newContext ({
viewport : { width : 1360 , height : 900 },
extraHTTPHeaders : {
"x-mnote-actor-id" : "user_real" ,
"x-mnote-actor-type" : "user" ,
},
});
const page = await context . newPage ();
try {
await quickLogin ( page );
await page . goto ( documentUrl ( root , relativePath , sideRelativePath ), { waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
await page . locator ( ".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror" ). first (). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await page . locator ( '.document-pane[data-pane-role="secondary"][data-pane-visible="true"]' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
const asset = await uploadLocalAsset (
page ,
root ,
documentId ,
"resource-note.md" ,
"text/markdown" ,
Buffer . from ( "# Resource Note\n\n旧资源正文\n" , "utf8" ),
"attachment" ,
);
const officeAsset = await uploadLocalAsset (
page ,
root ,
documentId ,
"resource-office.docx" ,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" ,
Buffer . from ( "task457 office probe" , "utf8" ),
"attachment" ,
);
await page . goto ( documentUrl ( root , relativePath , sideRelativePath ), { waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
await page . locator ( ".document-pane[data-pane-role=\"primary\"] .editor-surface .ProseMirror" ). first (). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await page . evaluate (({ asset , documentId }) => {
window . dispatchEvent ( new CustomEvent ( "tree.asset.open" , {
detail : {
assetId : asset . id ,
documentId ,
title : asset . file_name || "resource-note.md" ,
assetType : asset . asset_type || "attachment" ,
},
}));
}, { asset , documentId });
await page . locator ( '[data-testid="mnote-resource-tab-host"]:not([hidden])' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await page . locator ( '.document-pane[data-pane-role="secondary"][data-pane-visible="true"]' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await page . locator ( '.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"]' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
2026-05-20 17:47:09 +08:00
const markdownTabState = await page . evaluate (() => {
const pageTab = document . querySelector ( '[data-mnote-main-tab="page"]' );
const tab = document . querySelector ( '.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"]' );
const badge = tab ? . querySelector ( ".mnote-main-tab-badge" );
return {
pageText : pageTab ? . textContent || "" ,
resourceText : tab ? . textContent || "" ,
badgeKind : tab ? . getAttribute ( "data-mnote-tab-badge-kind" ) || "" ,
badgeColor : badge ? getComputedStyle ( badge ). backgroundColor : "" ,
};
});
assert ( ! /\b(description|article|notes)\b/i . test ( markdownTabState . pageText ), `页面 tab 不应显示 Material Symbols 文本: ${ JSON . stringify ( markdownTabState ) } ` );
assert ( ! /\b(description|article|notes)\b/i . test ( markdownTabState . resourceText ), `Markdown tab 不应显示 Material Symbols 文本: ${ JSON . stringify ( markdownTabState ) } ` );
assert . equal ( markdownTabState . badgeKind , "code" , `Markdown tab 应使用黑色 code 色块: ${ JSON . stringify ( markdownTabState ) } ` );
assert ( markdownTabState . badgeColor === "rgb(17, 17, 17)" , `Markdown tab 色块应为黑色: ${ JSON . stringify ( markdownTabState ) } ` );
2026-05-28 22:01:44 +08:00
const openEditorsSnapshot = await page . evaluate (() => window . __mnoteOpenEditorsSnapshot || null );
assert . equal ( openEditorsSnapshot ? . schema , "mnote.open_editors_snapshot.v1" , `应输出 open editors snapshot: ${ JSON . stringify ( openEditorsSnapshot ) } ` );
assert ( openEditorsSnapshot . groups ? . primary , `snapshot 应包含 primary group: ${ JSON . stringify ( openEditorsSnapshot ) } ` );
assert ( openEditorsSnapshot . groups ? . secondary , `snapshot 应包含 secondary group: ${ JSON . stringify ( openEditorsSnapshot ) } ` );
assert ( openEditorsSnapshot . activeObjectIdentity . includes ( "resource-note.md" ), `全局 active 应优先 primary active resource,而不是 secondary page: ${ JSON . stringify ( openEditorsSnapshot ) } ` );
assert ( openEditorsSnapshot . groups . primary . activeObjectIdentity . includes ( "resource-note.md" ), `primary group active 应指向 markdown resource: ${ JSON . stringify ( openEditorsSnapshot . groups . primary ) } ` );
assert ( openEditorsSnapshot . groups . primary . resourceEditors . some (( entry ) => entry . paneRole === "primary" && entry . kind === "markdown" && entry . preview === false && entry . pinned === false ), `primary resource editor 应包含轻量 editor 字段: ${ JSON . stringify ( openEditorsSnapshot . groups . primary ) } ` );
assert ( openEditorsSnapshot . groups . primary . editors . some (( entry ) => entry . kind === "page" && entry . pinned === true ), `primary group 应包含 pinned page editor: ${ JSON . stringify ( openEditorsSnapshot . groups . primary ) } ` );
assert ( openEditorsSnapshot . groups . secondary . editors . some (( entry ) => entry . kind === "page" && entry . paneRole === "secondary" ), `secondary group 应包含 secondary page editor: ${ JSON . stringify ( openEditorsSnapshot . groups . secondary ) } ` );
assert ( openEditorsSnapshot . groups . secondary . editors . some (( entry ) => entry . kind === "page" && entry . documentId === localMdDocumentId ( sideRelativePath ) && entry . workspacePath ? . relativePath === sideRelativePath ), `secondary page editor 应保留自己的 documentId/workspacePath: ${ JSON . stringify ( openEditorsSnapshot . groups . secondary ) } ` );
2026-05-20 14:20:48 +08:00
const editor = page . locator ( '.mnote-resource-tab-panel:not([hidden]) .editor-surface .ProseMirror[contenteditable="true"]' ). first ();
await editor . waitFor ({ state : "visible" , timeout : UI_TIMEOUT_MS });
await editor . click ({ timeout : UI_TIMEOUT_MS });
await page . keyboard . press ( process . platform === "darwin" ? "Meta+End" : "Control+End" );
await page . keyboard . type ( "\n新增资源正文" , { delay : 5 });
await page . waitForFunction (
() => document . querySelector ( '.mnote-resource-tab-panel:not([hidden]) [data-runtime-editor-status="saved"]' ),
null ,
{ timeout : UI_TIMEOUT_MS },
);
const assetPath = path . join ( root , "README" , "resource-note.md" );
const pageMarkdown = fs . readFileSync ( path . join ( root , relativePath ), "utf8" );
const assetMarkdown = fs . readFileSync ( assetPath , "utf8" );
assert ( assetMarkdown . includes ( "新增资源正文" ), assetMarkdown );
assert ( pageMarkdown . includes ( "页面正文保持不变" ), pageMarkdown );
assert ( ! pageMarkdown . includes ( "新增资源正文" ), pageMarkdown );
await page . evaluate (({ officeAsset , documentId }) => {
window . dispatchEvent ( new CustomEvent ( "tree.asset.open" , {
detail : {
assetId : officeAsset . id ,
documentId ,
title : officeAsset . file_name || "resource-office.docx" ,
assetType : officeAsset . asset_type || "attachment" ,
},
}));
}, { officeAsset , documentId });
await page . locator ( '.mnote-main-tab.is-active[data-mnote-tab-kind="office"]' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
2026-05-20 17:47:09 +08:00
const officeTabState = await page . evaluate (() => {
const tab = document . querySelector ( '.mnote-main-tab.is-active[data-mnote-tab-kind="office"]' );
const badge = tab ? . querySelector ( ".mnote-main-tab-badge" );
return {
resourceText : tab ? . textContent || "" ,
badgeKind : tab ? . getAttribute ( "data-mnote-tab-badge-kind" ) || "" ,
badgeColor : badge ? getComputedStyle ( badge ). backgroundColor : "" ,
};
});
assert ( ! /\b(description|article|notes)\b/i . test ( officeTabState . resourceText ), `Office tab 不应显示 Material Symbols 文本: ${ JSON . stringify ( officeTabState ) } ` );
assert . equal ( officeTabState . badgeKind , "word" , `docx tab 应使用 word 色块: ${ JSON . stringify ( officeTabState ) } ` );
assert ( officeTabState . badgeColor === "rgb(79, 125, 243)" , `docx tab 色块应与附件 word 蓝色一致: ${ JSON . stringify ( officeTabState ) } ` );
2026-05-28 22:01:44 +08:00
const openEditorsSnapshotAfterOffice = await page . evaluate (() => window . __mnoteOpenEditorsSnapshot || null );
const activeOfficeEditor = openEditorsSnapshotAfterOffice ? . groups ? . primary ? . resourceEditors ? . find (( entry ) => entry . active && entry . kind === "office" );
assert ( activeOfficeEditor , `打开 Office 后 snapshot 应把 office resource 标为 active: ${ JSON . stringify ( openEditorsSnapshotAfterOffice ? . groups ? . primary ) } ` );
assert . equal ( activeOfficeEditor . documentId , documentId , `Office snapshot 应携带 documentId: ${ JSON . stringify ( activeOfficeEditor ) } ` );
assert ( activeOfficeEditor . workspaceId , `Office snapshot 应携带 workspaceId: ${ JSON . stringify ( activeOfficeEditor ) } ` );
assert . equal ( activeOfficeEditor . editorKind , "office" , `Office snapshot 应携带 editorKind: ${ JSON . stringify ( activeOfficeEditor ) } ` );
assert ( activeOfficeEditor . lastActiveAt > 0 , `Office passive resource 激活后 lastActiveAt 应大于 0: ${ JSON . stringify ( activeOfficeEditor ) } ` );
2026-05-20 14:20:48 +08:00
const officeFrameSrc = await page . locator ( '.mnote-resource-tab-panel:not([hidden]) iframe.mnote-resource-tab-frame' ). first (). getAttribute ( "src" , {
timeout : UI_TIMEOUT_MS ,
});
assert ( officeFrameSrc , "Office resource tab 应创建 iframe" );
const officeFrameUrl = new URL ( officeFrameSrc , BASE_URL );
2026-05-28 22:01:44 +08:00
assert ( officeFrameUrl . pathname === "/office-preview" , `Office resource tab iframe 应指向 /office-preview 轻量预览,实际为 ${ officeFrameSrc } ` );
assert ( officeFrameUrl . searchParams . get ( "assetId" ) === officeAsset . id , "Office preview URL 应携带当前资源 assetId" );
assert ( officeFrameUrl . searchParams . get ( "documentId" ) === documentId , "Office preview URL 应携带当前 documentId" );
assert ( officeFrameUrl . searchParams . get ( "fileName" ) === "resource-office.docx" , "Office preview URL 应携带资源文件名" );
assert ( officeFrameUrl . searchParams . get ( "fileType" ) === "docx" , "Office preview URL 应携带 docx 类型" );
assert ( officeFrameUrl . searchParams . get ( "fileUrl" ) ? . includes ( "/api/local-folder/files/open" ), "Office preview URL 应通过 local-folder file open 回源" );
2026-05-20 14:20:48 +08:00
const [ officePopup ] = await Promise . all ([
page . waitForEvent ( "popup" , { timeout : UI_TIMEOUT_MS }),
page . evaluate (({ officeAsset , documentId }) => {
window . dispatchEvent ( new CustomEvent ( "tree.asset.open" , {
detail : {
assetId : officeAsset . id ,
documentId ,
title : officeAsset . file_name || "resource-office.docx" ,
assetType : officeAsset . asset_type || "attachment" ,
openTarget : "new-window" ,
},
}));
}, { officeAsset , documentId }),
]);
await officePopup . waitForLoadState ( "domcontentloaded" , { timeout : UI_TIMEOUT_MS }). catch (() => undefined );
const officePopupUrl = new URL ( officePopup . url ());
assert ( officePopupUrl . pathname === "/onlyoffice" , `显式 new-window 应打开 /onlyoffice,实际为 ${ officePopup . url () } ` );
assert ( officePopupUrl . searchParams . get ( "assetId" ) === officeAsset . id , "new-window URL 应携带当前资源 assetId" );
assert ( officePopupUrl . searchParams . get ( "mode" ) === "edit" , "显式 new-window 应保留 edit 模式" );
await officePopup . close (). catch (() => undefined );
2026-05-20 19:04:05 +08:00
const officeCloseBtn = page . locator ( '.mnote-main-tab.is-active[data-mnote-tab-kind="office"] .mnote-main-tab-close' );
await officeCloseBtn . click ({ timeout : UI_TIMEOUT_MS });
await page . waitForTimeout ( 200 );
const activeAfterOfficeClose = await page . evaluate (() => {
const activeTab = document . querySelector ( ".mnote-main-tab.is-active" );
return {
kind : activeTab ? . getAttribute ( "data-mnote-tab-kind" ) || "" ,
text : activeTab ? . textContent ? . trim () || "" ,
};
});
assert . equal ( activeAfterOfficeClose . kind , "markdown" , `关闭 office tab 后应回到 markdown tab: ${ JSON . stringify ( activeAfterOfficeClose ) } ` );
assert ( activeAfterOfficeClose . text . includes ( "resource-note.md" ), `markdown tab 标题应包含资源名: ${ JSON . stringify ( activeAfterOfficeClose ) } ` );
2026-05-20 20:48:18 +08:00
const markdownActiveState = await page . evaluate (() => {
const url = new URL ( window . location . href );
const activeResource = url . searchParams . get ( "resourceTab" ) || "" ;
const activeRow = document . querySelector ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]' );
const resourceRows = Array . from ( document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-asset-id]' )). map (( row ) => row . getAttribute ( "data-asset-id" ) || "" );
return {
activeResource ,
activeRowAssetId : activeRow ? . getAttribute ( "data-asset-id" ) || "" ,
resourceRows ,
selectedRows : Array . from ( document . querySelectorAll ( '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-selected="true"]' )). map (( row ) => row . getAttribute ( "data-asset-id" ) || row . getAttribute ( "data-row-id" ) || "" ),
};
});
assert ( markdownActiveState . activeResource . includes ( "resource-note.md" ), `激活 resource tab 应写入 canonical resourceTab URL 状态: ${ JSON . stringify ( markdownActiveState ) } ` );
if ( markdownActiveState . resourceRows . includes ( asset . id )) {
assert . equal ( markdownActiveState . activeRowAssetId , asset . id , `已渲染对应 filetree row 时应标记 data-active: ${ JSON . stringify ( markdownActiveState ) } ` );
}
2026-05-28 22:01:44 +08:00
await page . reload ({ waitUntil : "domcontentloaded" , timeout : UI_TIMEOUT_MS });
await page . locator ( '.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"]' ). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
await page . locator ( '.mnote-resource-tab-panel:not([hidden]) .editor-surface .ProseMirror[contenteditable="true"]' ). first (). waitFor ({
state : "visible" ,
timeout : UI_TIMEOUT_MS ,
});
const restoredMarkdownState = await page . evaluate (() => {
const snapshot = window . __mnoteOpenEditorsSnapshot || null ;
const active = snapshot ? . groups ? . primary ? . resourceEditors ? . find (( entry ) => entry . active && entry . kind === "markdown" );
return {
activeResource : new URL ( window . location . href ). searchParams . get ( "resourceTab" ) || "" ,
activeObjectIdentity : active ? . objectIdentity || "" ,
path : active ? . path || "" ,
workspacePath : active ? . workspacePath || null ,
editorText : document . querySelector ( '.mnote-resource-tab-panel:not([hidden]) .editor-surface .ProseMirror' ) ? . textContent || "" ,
};
});
assert ( restoredMarkdownState . activeResource . includes ( "resource-note.md" ), `刷新后应保留 canonical resourceTab URL 状态: ${ JSON . stringify ( restoredMarkdownState ) } ` );
assert ( restoredMarkdownState . activeObjectIdentity . includes ( "resource-note.md" ), `刷新后应从 OpenEditorsSnapshot 恢复 active markdown resource: ${ JSON . stringify ( restoredMarkdownState ) } ` );
assert . equal ( restoredMarkdownState . path , "README/resource-note.md" , `刷新后 active resource snapshot 应保留 path: ${ JSON . stringify ( restoredMarkdownState ) } ` );
assert . equal ( restoredMarkdownState . workspacePath ? . relativePath , "README/resource-note.md" , `刷新后 active resource 应带 workspacePath.relativePath: ${ JSON . stringify ( restoredMarkdownState ) } ` );
assert ( restoredMarkdownState . editorText . includes ( "新增资源正文" ), `刷新后 resource editor 应读回资源正文: ${ JSON . stringify ( restoredMarkdownState ) } ` );
2026-05-20 19:04:05 +08:00
const mdCloseBtn = page . locator ( '.mnote-main-tab.is-active[data-mnote-tab-kind="markdown"] .mnote-main-tab-close' );
await mdCloseBtn . click ({ timeout : UI_TIMEOUT_MS });
await page . waitForTimeout ( 200 );
const activeAfterAllClosed = await page . evaluate (() => {
const activeTab = document . querySelector ( ".mnote-main-tab.is-active" );
return {
kind : activeTab ? . getAttribute ( "data-mnote-tab-kind" ) || "" ,
resourceTabCount : document . querySelectorAll ( '.mnote-main-tab[data-mnote-tab-kind]:not([data-mnote-tab-kind="page"])' ). length ,
panelHidden : document . querySelector ( "[data-mnote-resource-tab-host]" ) ? . hasAttribute ( "hidden" ),
};
});
assert . equal ( activeAfterAllClosed . kind , "page" , `关闭所有 resource tab 后应回到 page tab: ${ JSON . stringify ( activeAfterAllClosed ) } ` );
assert . equal ( activeAfterAllClosed . resourceTabCount , 0 , `resource tab 应全部关闭: ${ JSON . stringify ( activeAfterAllClosed ) } ` );
assert . equal ( activeAfterAllClosed . panelHidden , true , `resource tab host 应隐藏: ${ JSON . stringify ( activeAfterAllClosed ) } ` );
2026-05-20 20:48:18 +08:00
assert . equal ( new URL ( page . url ()). searchParams . get ( "resourceTab" ), null , "关闭所有 resource tab 后应清除 resourceTab URL 状态" );
2026-05-20 19:04:05 +08:00
2026-05-20 14:20:48 +08:00
console . log ( JSON . stringify ({ ok : true , root , assetPath , assetId : asset . id , officeAssetId : officeAsset . id }, null , 2 ));
} finally {
await context . close (). catch (() => undefined );
await browser . close (). catch (() => undefined );
}
}
main (). catch (( error ) => {
console . error ( error && error . stack ? error . stack : error );
process . exit ( 1 );
});