集成 OpenHub 与 WeKnora Page AI
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
#!/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,
|
||||
ensureAuthenticated,
|
||||
getViewerIdentity,
|
||||
} = require("./tree-shell-smoke-helpers");
|
||||
|
||||
const TASK = "task773-page-ai-openhub-browser-smoke";
|
||||
const OUTPUT_DIR = path.join(process.cwd(), "tmp", "7-68-runtime");
|
||||
const SCREENSHOT_PATH = path.join(OUTPUT_DIR, "page-ai-openhub-browser.png");
|
||||
const RESULT_PATH = path.join(OUTPUT_DIR, "page-ai-openhub-browser-result.json");
|
||||
const TEST_EMAIL = "mnote.e2e@example.com";
|
||||
const TEST_PASSWORD = "MnoteE2E123!";
|
||||
const CHROMIUM_EXECUTABLE_PATH = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
|| ["/usr/bin/chromium-browser", "/usr/bin/chromium", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome"]
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
|
||||
class SmokeFailure extends Error {
|
||||
constructor(kind, message, details = {}) {
|
||||
super(message);
|
||||
this.name = "SmokeFailure";
|
||||
this.kind = kind;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
function visibleSelectorScript(selectors) {
|
||||
return selectors.some((selector) => {
|
||||
const nodes = Array.from(document.querySelectorAll(selector));
|
||||
return nodes.some((node) => {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
const rect = node.getBoundingClientRect();
|
||||
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function assertServiceReachable(baseUrl) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`${baseUrl}/health`, { redirect: "manual", signal: AbortSignal.timeout(6_000) });
|
||||
} catch (error) {
|
||||
throw new SmokeFailure("service_unreachable", `MNote 3000 服务不可达:${error.message}`, { baseUrl });
|
||||
}
|
||||
if (!response.ok && response.status !== 303) {
|
||||
throw new SmokeFailure("service_unreachable", `MNote /health 返回异常:HTTP ${response.status}`, { baseUrl });
|
||||
}
|
||||
}
|
||||
|
||||
async function loginWithUiFirst(page, requestContext) {
|
||||
await page.goto(`${BASE_URL}/auth`, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||||
if (!page.url().includes("/auth")) {
|
||||
return getViewerIdentity(requestContext);
|
||||
}
|
||||
|
||||
const quickLogin = page.getByRole("button", { name: "测试账号快速登录" });
|
||||
if (await quickLogin.isVisible({ timeout: 8_000 }).catch(() => false)) {
|
||||
await quickLogin.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (page.url().includes("/auth")) {
|
||||
const account = page.locator('input[name="account"], input[type="email"], input[data-auth-field="account"]').first();
|
||||
const password = page.locator('input[name="password"], input[type="password"]').first();
|
||||
const submit = page.getByRole("button", { name: /^登录$|账号登录|登录$/ }).first();
|
||||
if (!(await account.isVisible({ timeout: 2_000 }).catch(() => false)) || !(await password.isVisible({ timeout: 2_000 }).catch(() => false))) {
|
||||
throw new SmokeFailure("auth_failed", "认证页未出现快速登录,也找不到账号密码输入框", { url: page.url() });
|
||||
}
|
||||
await account.fill(TEST_EMAIL, { timeout: UI_TIMEOUT_MS });
|
||||
await password.fill(TEST_PASSWORD, { timeout: UI_TIMEOUT_MS });
|
||||
await submit.click({ timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForURL((url) => !url.toString().includes("/auth"), { timeout: UI_TIMEOUT_MS, waitUntil: "commit" }).catch(() => undefined);
|
||||
}
|
||||
|
||||
if (page.url().includes("/auth")) {
|
||||
throw new SmokeFailure("auth_failed", "测试账号登录后仍停留在 /auth", { url: page.url() });
|
||||
}
|
||||
|
||||
try {
|
||||
return await getViewerIdentity(requestContext);
|
||||
} catch (error) {
|
||||
throw new SmokeFailure("auth_failed", `登录后 whoami 仍不可用:${error.message}`, { url: page.url() });
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForVisibleAny(page, selectors, label) {
|
||||
try {
|
||||
await page.waitForFunction(visibleSelectorScript, selectors, { timeout: UI_TIMEOUT_MS });
|
||||
} catch (error) {
|
||||
throw new SmokeFailure("selector_missing", `${label} 不可见。候选 selector: ${selectors.join(", ")}`, {
|
||||
selectors,
|
||||
cause: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function openPageAiDrawer(page) {
|
||||
await page.goto(BASE_URL, { waitUntil: "commit", timeout: UI_TIMEOUT_MS });
|
||||
if (page.url().includes("/auth")) {
|
||||
throw new SmokeFailure("auth_failed", "打开主页后被重定向到 /auth,登录态未生效", { url: page.url() });
|
||||
}
|
||||
|
||||
await page.evaluate(() => {
|
||||
try {
|
||||
localStorage.removeItem("mnote.page_ai.openhub_host");
|
||||
localStorage.setItem("mnote.page_ai.openhub_host", "1");
|
||||
} catch {}
|
||||
});
|
||||
await waitForVisibleAny(page, ["[data-testid='wolai-floating-ai']", "[data-testid='wolai-page-ai-drawer']"], "Page AI 入口");
|
||||
|
||||
const drawerVisible = await page.locator("[data-testid='wolai-page-ai-drawer']").first().isVisible().catch(() => false);
|
||||
if (!drawerVisible) {
|
||||
await page.getByTestId("wolai-floating-ai").click({ timeout: UI_TIMEOUT_MS });
|
||||
}
|
||||
await waitForVisibleAny(page, ["[data-testid='wolai-page-ai-drawer']"], "Page AI drawer");
|
||||
}
|
||||
|
||||
async function validateOpenHubQuickActions(page) {
|
||||
const frameHandle = await page.locator("iframe[data-page-ai-openhub-iframe]").elementHandle({ timeout: UI_TIMEOUT_MS });
|
||||
const frame = frameHandle ? await frameHandle.contentFrame() : null;
|
||||
if (!frame) {
|
||||
throw new SmokeFailure("quick_action_failed", "OpenHub iframe frame 不可用", { reason: "iframe_frame_missing" });
|
||||
}
|
||||
const tabButton = frame.locator("[data-mnote-openhub-current-tab-toggle]").first();
|
||||
const folderButton = frame.locator("[data-mnote-openhub-current-folder-toggle]").first();
|
||||
await tabButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
await folderButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
|
||||
const fillVisibleInput = async (value) => frame.evaluate((nextValue) => {
|
||||
const selectors = [
|
||||
"textarea[placeholder*='输入']",
|
||||
"textarea",
|
||||
"[contenteditable='true']",
|
||||
"input[type='text'][placeholder*='输入']",
|
||||
"input[type='text']",
|
||||
".ant-input",
|
||||
];
|
||||
const visible = (node) => {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
const rect = node.getBoundingClientRect();
|
||||
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
for (const selector of selectors) {
|
||||
const nodes = Array.from(document.querySelectorAll(selector)).filter(visible);
|
||||
if (!nodes.length) continue;
|
||||
const node = nodes[nodes.length - 1];
|
||||
node.focus();
|
||||
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), "value");
|
||||
if (descriptor && typeof descriptor.set === "function") {
|
||||
descriptor.set.call(node, nextValue);
|
||||
} else {
|
||||
node.value = nextValue;
|
||||
}
|
||||
} else {
|
||||
node.textContent = nextValue;
|
||||
}
|
||||
node.dispatchEvent(new InputEvent("input", { bubbles: true, data: nextValue, inputType: "insertText" }));
|
||||
node.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, value);
|
||||
const readVisibleInput = async () => frame.evaluate(() => {
|
||||
const selectors = [
|
||||
"textarea[placeholder*='输入']",
|
||||
"textarea",
|
||||
"[contenteditable='true']",
|
||||
"input[type='text'][placeholder*='输入']",
|
||||
"input[type='text']",
|
||||
".ant-input",
|
||||
];
|
||||
const visible = (node) => {
|
||||
if (!(node instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(node);
|
||||
const rect = node.getBoundingClientRect();
|
||||
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
for (const selector of selectors) {
|
||||
const nodes = Array.from(document.querySelectorAll(selector)).filter(visible);
|
||||
if (!nodes.length) continue;
|
||||
const node = nodes[nodes.length - 1];
|
||||
if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) return node.value || "";
|
||||
return node.textContent || "";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const hasInput = await fillVisibleInput("");
|
||||
if (!hasInput) {
|
||||
throw new SmokeFailure("quick_action_failed", "OpenHub iframe 内未找到可见聊天输入框", {});
|
||||
}
|
||||
const prompt = `MNOTE_NATIVE_CONTEXT_SMOKE_${Date.now()}`;
|
||||
let capturedBody = null;
|
||||
await page.route("**/page-ai/openhub/ai/api/query/stream**", async (route) => {
|
||||
capturedBody = JSON.parse(route.request().postData() || "{}");
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { "content-type": "text/event-stream; charset=utf-8" },
|
||||
body: [
|
||||
`data: ${JSON.stringify({ type: "session", conversation_id: "task773-native-context", done: false })}`,
|
||||
"",
|
||||
`data: ${JSON.stringify({ type: "message_complete", done: true })}`,
|
||||
"",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await fillVisibleInput(prompt);
|
||||
await tabButton.click({ timeout: UI_TIMEOUT_MS });
|
||||
const inputTextAfterToggle = (await readVisibleInput()).trim();
|
||||
const tabSelected = await tabButton.evaluate((node) => node.classList.contains("ant-btn-primary") || node.getAttribute("type") === "button" && node.matches(".ant-btn-primary"));
|
||||
const folderSelectedAfterTab = await folderButton.evaluate((node) => node.classList.contains("ant-btn-primary"));
|
||||
await page.keyboard.press("Enter");
|
||||
await page.waitForFunction(() => window.__mnoteOpenHubTask773RequestCaptured === true, undefined, { timeout: 100 }).catch(() => undefined);
|
||||
const started = Date.now();
|
||||
while (!capturedBody && Date.now() - started < UI_TIMEOUT_MS) {
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
const activeEditor = await page.evaluate(() => window.__mnoteDocumentPaneRuntime && typeof window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot === "function"
|
||||
? window.__mnoteDocumentPaneRuntime.getOpenEditorsSnapshot()?.activeEditor || null
|
||||
: null);
|
||||
const result = {
|
||||
ok: Boolean(capturedBody && capturedBody.mnote_context && capturedBody.mnote_context.value),
|
||||
tabButtonVisible: await tabButton.isVisible().catch(() => false),
|
||||
folderButtonVisible: await folderButton.isVisible().catch(() => false),
|
||||
tabSelected,
|
||||
folderSelectedAfterTab,
|
||||
inputTextAfterToggle,
|
||||
requestQuestion: capturedBody && capturedBody.question,
|
||||
mnoteContext: capturedBody && capturedBody.mnote_context,
|
||||
activeEditor,
|
||||
};
|
||||
if (!result.ok) {
|
||||
throw new SmokeFailure("quick_action_failed", "OpenHub 当前 Tab/文件夹上下文未随发送请求进入后台", result);
|
||||
}
|
||||
if (!result.tabSelected || result.folderSelectedAfterTab) {
|
||||
throw new SmokeFailure("quick_action_selection_invalid", "当前 Tab 按钮没有呈现单选选中态", result);
|
||||
}
|
||||
if (result.inputTextAfterToggle !== prompt) {
|
||||
throw new SmokeFailure("quick_action_leaked_to_input", "当前 Tab/文件夹地址不应直接写入用户可见输入框", result);
|
||||
}
|
||||
if (result.mnoteContext.kind !== "tab" || !/^https?:\/\/.+\/documents\//.test(result.mnoteContext.value)) {
|
||||
throw new SmokeFailure("quick_action_tab_context_invalid", "当前 Tab 发送上下文不是 MNote 文档地址", result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function collectState(page) {
|
||||
return page.evaluate(() => {
|
||||
const visible = (node) => {
|
||||
if (!node || node.nodeType !== 1) return false;
|
||||
const ownerWindow = node.ownerDocument && node.ownerDocument.defaultView ? node.ownerDocument.defaultView : window;
|
||||
const style = ownerWindow.getComputedStyle(node);
|
||||
const rect = node.getBoundingClientRect();
|
||||
return !node.hidden && style.display !== "none" && style.visibility !== "hidden" && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const rectOf = (node) => {
|
||||
if (!node || node.nodeType !== 1 || typeof node.getBoundingClientRect !== "function") return null;
|
||||
const rect = node.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
top: Math.round(rect.top),
|
||||
bottom: Math.round(rect.bottom),
|
||||
};
|
||||
};
|
||||
const visibleElements = (selector) => Array.from(document.querySelectorAll(selector)).filter(visible);
|
||||
const visibleAny = (selector) => visibleElements(selector).length > 0;
|
||||
const text = (selector) => (document.querySelector(selector)?.textContent || "").trim();
|
||||
const iframe = document.querySelector("iframe[data-page-ai-openhub-iframe]");
|
||||
const iframeDoc = iframe instanceof HTMLIFrameElement ? iframe.contentDocument : null;
|
||||
const iframeBodyText = (iframeDoc && iframeDoc.body ? iframeDoc.body.textContent || "" : "").trim();
|
||||
const iframeShellKind = iframeDoc && iframeDoc.body ? iframeDoc.body.getAttribute("data-mnote-openhub-ai-shell") || "" : "";
|
||||
const drawer = Array.from(document.querySelectorAll("[data-testid='wolai-page-ai-drawer']")).find(visible) || null;
|
||||
const openhubHost = document.querySelector("[data-page-ai-openhub-host='true']");
|
||||
const outerOpenHubHeaderVisible = visibleAny(".wolai-page-ai-opencode-header")
|
||||
|| visibleAny(".wolai-page-ai-header-copy")
|
||||
|| visibleAny("[data-page-ai-openhub-runtime-status]");
|
||||
const diagnostics = document.querySelector("[data-page-ai-openhub-bootstrap-copy]");
|
||||
const diagnosticsVisible = Boolean(diagnostics && visible(diagnostics));
|
||||
const diagnosticsRect = diagnosticsVisible ? rectOf(diagnostics) : null;
|
||||
const diagnosticsOpen = diagnostics instanceof HTMLDetailsElement ? diagnostics.open : false;
|
||||
const debugChromeRects = diagnosticsVisible ? [{
|
||||
selector: "[data-page-ai-openhub-bootstrap-copy]",
|
||||
text: (diagnostics.textContent || "").replace(/\s+/g, " ").trim().slice(0, 160),
|
||||
rect: diagnosticsRect,
|
||||
open: diagnosticsOpen,
|
||||
}] : [];
|
||||
const debugChromeTotalHeight = diagnosticsRect?.height || 0;
|
||||
const fallbackActionSelector = "[data-page-ai-action='openhub-use-opencode-fallback']";
|
||||
const fallbackActionVisibleInIframe = iframeDoc
|
||||
? Array.from(iframeDoc.querySelectorAll(fallbackActionSelector)).some(visible)
|
||||
: false;
|
||||
const pageText = (document.body.textContent || "").replace(/\s+/g, " ").trim();
|
||||
const loginTextPattern = /(登录\s*OpenHub|OpenHub\s*Login|WeKnora\s*登录|登录\s*WeKnora|Sign in to OpenHub|OpenHub account|WeKnora account)/i;
|
||||
const reactSelectorMarkers = [
|
||||
"[data-openhub-ai-panel]",
|
||||
"[data-testid='openhub-ai-panel']",
|
||||
"[data-testid='openhub-chat']",
|
||||
"[data-openhub-conversation]",
|
||||
"[data-openhub-session-history]",
|
||||
".openhub-ai-panel",
|
||||
".chat-message-list",
|
||||
".ant-layout",
|
||||
".ant-menu",
|
||||
".ant-input",
|
||||
].filter((selector) => Array.from(document.querySelectorAll(selector)).some(visible)
|
||||
|| (iframeDoc && Array.from(iframeDoc.querySelectorAll(selector)).some(visible)));
|
||||
const reactTextMarkers = [
|
||||
"OpenHub 平台",
|
||||
"开始对话",
|
||||
"历史记录",
|
||||
"技能管理",
|
||||
"选择模型",
|
||||
].filter((marker) => iframeBodyText.includes(marker));
|
||||
const conflictEntryPattern = /(文件管理|知识库|时光机|智能体|协作任务|团队状态)/;
|
||||
const quickActionTab = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-current-tab-toggle]") : null;
|
||||
const quickActionFolder = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-current-folder-toggle]") : null;
|
||||
const smokeInput = iframeDoc ? iframeDoc.querySelector("[data-mnote-openhub-smoke-input]") : null;
|
||||
return {
|
||||
url: location.href,
|
||||
title: document.title,
|
||||
bodySnippet: pageText.slice(0, 800),
|
||||
drawerVisible: Boolean(drawer),
|
||||
drawerRect: rectOf(drawer),
|
||||
openhubHost: Boolean(openhubHost),
|
||||
openhubHostRect: rectOf(openhubHost),
|
||||
outerOpenHubHeaderVisible,
|
||||
debugChromeVisible: debugChromeRects.length > 0,
|
||||
debugChromeRects,
|
||||
debugChromeTotalHeight,
|
||||
debugChromeSqueezesContent: diagnosticsOpen || debugChromeTotalHeight > 80,
|
||||
hostChromeVisible: Boolean(Array.from(document.querySelectorAll("[data-page-ai-openhub-bootstrap-copy]")).find(visible)),
|
||||
iframeVisible: iframe instanceof HTMLIFrameElement && visible(iframe),
|
||||
iframeRect: rectOf(iframe),
|
||||
iframeSrc: iframe instanceof HTMLIFrameElement ? iframe.getAttribute("src") || "" : "",
|
||||
iframeBodySnippet: iframeBodyText.slice(0, 800),
|
||||
iframeShellKind,
|
||||
runtimeStatus: text("[data-page-ai-openhub-runtime-status]"),
|
||||
authTruth: text("[data-page-ai-openhub-auth-truth]"),
|
||||
workspaceScope: text("[data-page-ai-openhub-workspace-scope]"),
|
||||
routeGuard: text("[data-page-ai-openhub-route-guard]"),
|
||||
fallback: text("[data-page-ai-openhub-fallback]"),
|
||||
loginPageVisible: loginTextPattern.test(pageText) || loginTextPattern.test(iframeBodyText),
|
||||
staticBoundaryVisible: iframeShellKind === "static-boundary" || /静态占位|static-boundary|最小 host\/bootstrap 占位/.test(iframeBodyText),
|
||||
fallbackActionVisible: visibleAny(fallbackActionSelector) || fallbackActionVisibleInIframe,
|
||||
fallbackActionCount: document.querySelectorAll(fallbackActionSelector).length
|
||||
+ (iframeDoc ? iframeDoc.querySelectorAll(fallbackActionSelector).length : 0),
|
||||
reactAiMarkers: [...reactSelectorMarkers, ...reactTextMarkers.map((marker) => `text:${marker}`)],
|
||||
reactTextMarkers,
|
||||
conflictEntryVisible: conflictEntryPattern.test(iframeBodyText),
|
||||
quickActionTabVisible: Boolean(quickActionTab && visible(quickActionTab)),
|
||||
quickActionFolderVisible: Boolean(quickActionFolder && visible(quickActionFolder)),
|
||||
quickActionTabText: quickActionTab ? (quickActionTab.textContent || "").trim() : "",
|
||||
quickActionFolderText: quickActionFolder ? (quickActionFolder.textContent || "").trim() : "",
|
||||
quickActionTabLabel: quickActionTab ? quickActionTab.getAttribute("aria-label") || "" : "",
|
||||
quickActionFolderLabel: quickActionFolder ? quickActionFolder.getAttribute("aria-label") || "" : "",
|
||||
quickActionTabSelected: quickActionTab ? quickActionTab.classList.contains("ant-btn-primary") : false,
|
||||
quickActionFolderSelected: quickActionFolder ? quickActionFolder.classList.contains("ant-btn-primary") : false,
|
||||
quickActionSmokeInputValue: smokeInput instanceof HTMLTextAreaElement ? smokeInput.value : "",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function assertOpenHubState(state) {
|
||||
if (!state.drawerVisible) {
|
||||
throw new SmokeFailure("selector_missing", "Page AI drawer 未保持可见", state);
|
||||
}
|
||||
if (!state.openhubHost) {
|
||||
throw new SmokeFailure("selector_missing", "Page AI drawer 未切到 OpenHub host", state);
|
||||
}
|
||||
if (!state.iframeVisible || !state.iframeSrc.includes("/page-ai/openhub/ai")) {
|
||||
throw new SmokeFailure("selector_missing", "OpenHub iframe 不可见或 src 未指向 /page-ai/openhub/ai", state);
|
||||
}
|
||||
if (state.outerOpenHubHeaderVisible) {
|
||||
throw new SmokeFailure("outer_header_visible", "OpenHub drawer 仍显示 MNote 外层 OpenHub AI 标题栏", state);
|
||||
}
|
||||
if (!state.iframeRect || state.iframeRect.height < 420) {
|
||||
throw new SmokeFailure("iframe_too_short", "OpenHub iframe 高度不足,可能被顶部 debug/status 区挤压", state);
|
||||
}
|
||||
if (state.debugChromeSqueezesContent) {
|
||||
throw new SmokeFailure("debug_chrome_visible", "OpenHub 顶部 debug/status chrome 仍可见且挤占空间", state);
|
||||
}
|
||||
if (state.fallbackActionVisible || state.fallbackActionCount > 0) {
|
||||
throw new SmokeFailure("legacy_fallback_visible", "OpenHub drawer 仍存在用户可见 opencode fallback 入口", state);
|
||||
}
|
||||
if (state.loginPageVisible) {
|
||||
throw new SmokeFailure("unexpected_upstream_login", "页面出现 OpenHub/WeKnora 登录入口", state);
|
||||
}
|
||||
if (!state.iframeBodySnippet && !state.reactAiMarkers.length) {
|
||||
throw new SmokeFailure("selector_missing", "OpenHub iframe 已出现,但 iframe 内 shell 内容不可见", state);
|
||||
}
|
||||
if (state.conflictEntryVisible) {
|
||||
throw new SmokeFailure("unexpected_conflict_entry", "OpenHub iframe 嵌入态仍显示 MNote 真相冲突入口", state);
|
||||
}
|
||||
if (!state.quickActionTabVisible || !state.quickActionFolderVisible) {
|
||||
throw new SmokeFailure("quick_action_missing", "OpenHub iframe 内缺少当前 Tab/文件夹快捷按钮", state);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
const consoleMessages = [];
|
||||
let browser;
|
||||
let context;
|
||||
let page;
|
||||
let result;
|
||||
let directHostRoute = null;
|
||||
let legacyOpencodeRoute = null;
|
||||
const networkEvents = [];
|
||||
|
||||
try {
|
||||
await assertServiceReachable(BASE_URL);
|
||||
browser = await chromium.launch({
|
||||
headless: process.env.HEADFUL !== "1",
|
||||
...(CHROMIUM_EXECUTABLE_PATH ? { executablePath: CHROMIUM_EXECUTABLE_PATH } : {}),
|
||||
});
|
||||
context = await browser.newContext({ viewport: { width: 1440, height: 960 }, locale: "zh-CN" });
|
||||
page = await context.newPage();
|
||||
page.on("console", (message) => {
|
||||
if (["error", "warning"].includes(message.type())) {
|
||||
consoleMessages.push({ type: message.type(), text: message.text() });
|
||||
}
|
||||
});
|
||||
page.on("response", (response) => {
|
||||
const url = response.url();
|
||||
if (url.includes("/page-ai/openhub")) {
|
||||
networkEvents.push({ url, status: response.status(), contentType: response.headers()["content-type"] || "" });
|
||||
}
|
||||
});
|
||||
|
||||
const viewer = await loginWithUiFirst(page, context.request).catch(async (error) => {
|
||||
if (error instanceof SmokeFailure) throw error;
|
||||
await ensureAuthenticated(page, context.request);
|
||||
return getViewerIdentity(context.request);
|
||||
});
|
||||
legacyOpencodeRoute = await context.request.get(`${BASE_URL}/page-ai/opencode`, { timeout: UI_TIMEOUT_MS }).then(async (response) => ({
|
||||
url: `${BASE_URL}/page-ai/opencode`,
|
||||
status: response.status(),
|
||||
ok: response.ok(),
|
||||
contentType: response.headers()["content-type"] || "",
|
||||
bodySnippet: (await response.text()).slice(0, 240),
|
||||
})).catch((error) => ({
|
||||
url: `${BASE_URL}/page-ai/opencode`,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: error.message,
|
||||
}));
|
||||
if (legacyOpencodeRoute.status !== 410) {
|
||||
throw new SmokeFailure("legacy_fallback_route_enabled", "登录后 /page-ai/opencode legacy fallback 页面仍可访问", legacyOpencodeRoute);
|
||||
}
|
||||
directHostRoute = await context.request.get(`${BASE_URL}/page-ai/openhub/ai`, { timeout: UI_TIMEOUT_MS }).then(async (response) => ({
|
||||
url: `${BASE_URL}/page-ai/openhub/ai`,
|
||||
status: response.status(),
|
||||
ok: response.ok(),
|
||||
contentType: response.headers()["content-type"] || "",
|
||||
bodySnippet: (await response.text()).slice(0, 240),
|
||||
})).catch((error) => ({
|
||||
url: `${BASE_URL}/page-ai/openhub/ai`,
|
||||
status: 0,
|
||||
ok: false,
|
||||
error: error.message,
|
||||
}));
|
||||
await openPageAiDrawer(page);
|
||||
await waitForVisibleAny(
|
||||
page,
|
||||
[
|
||||
"[data-page-ai-openhub-host='true']",
|
||||
"[data-page-ai-openhub-bootstrap-copy]",
|
||||
"iframe[data-page-ai-openhub-iframe]",
|
||||
],
|
||||
"OpenHub host",
|
||||
);
|
||||
await page.waitForFunction(() => {
|
||||
const frame = document.querySelector("iframe[data-page-ai-openhub-iframe]");
|
||||
return frame instanceof HTMLIFrameElement && (frame.getAttribute("src") || "").includes("/page-ai/openhub/ai");
|
||||
}, undefined, { timeout: UI_TIMEOUT_MS });
|
||||
await page.waitForFunction(() => {
|
||||
const frame = document.querySelector("iframe[data-page-ai-openhub-iframe]");
|
||||
return frame instanceof HTMLIFrameElement && (frame.getAttribute("src") || "").includes("mnoteScope=");
|
||||
}, undefined, { timeout: UI_TIMEOUT_MS }).catch(() => undefined);
|
||||
await page.waitForTimeout(800);
|
||||
await page.waitForFunction(() => {
|
||||
const iframe = document.querySelector("iframe[data-page-ai-openhub-iframe]");
|
||||
const doc = iframe instanceof HTMLIFrameElement ? iframe.contentDocument : null;
|
||||
return Boolean(doc && doc.querySelector("[data-mnote-openhub-current-tab-toggle]") && doc.querySelector("[data-mnote-openhub-current-folder-toggle]"));
|
||||
}, undefined, { timeout: UI_TIMEOUT_MS });
|
||||
|
||||
const quickActions = await validateOpenHubQuickActions(page);
|
||||
const state = await collectState(page);
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true });
|
||||
assertOpenHubState(state);
|
||||
const openhubReactConnected = state.reactAiMarkers.length > 0;
|
||||
|
||||
result = {
|
||||
ok: true,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
viewer,
|
||||
screenshot: SCREENSHOT_PATH,
|
||||
directHostRoute,
|
||||
legacyOpencodeRoute,
|
||||
quickActions,
|
||||
state,
|
||||
openhubReactConnected,
|
||||
staticBoundaryOnly: state.staticBoundaryVisible && !openhubReactConnected,
|
||||
reactAiNote: openhubReactConnected
|
||||
? "OpenHub React UI marker 已出现"
|
||||
: "未发现 OpenHub React AI marker;本次只验证 MNote OpenHub host drawer/iframe/shell 边界,不把静态 shell 记为 React AI 已接入",
|
||||
networkEvents,
|
||||
consoleMessages,
|
||||
};
|
||||
} catch (error) {
|
||||
const state = page ? await collectState(page).catch(() => null) : null;
|
||||
if (page) {
|
||||
await page.screenshot({ path: SCREENSHOT_PATH, fullPage: true }).catch(() => undefined);
|
||||
}
|
||||
result = {
|
||||
ok: false,
|
||||
task: TASK,
|
||||
baseUrl: BASE_URL,
|
||||
screenshot: fs.existsSync(SCREENSHOT_PATH) ? SCREENSHOT_PATH : null,
|
||||
failureKind: error instanceof SmokeFailure ? error.kind : "unexpected_error",
|
||||
error: error instanceof Error ? error.stack || error.message : String(error),
|
||||
errorDetails: error instanceof SmokeFailure ? error.details : null,
|
||||
directHostRoute,
|
||||
legacyOpencodeRoute,
|
||||
state,
|
||||
networkEvents,
|
||||
consoleMessages,
|
||||
};
|
||||
} finally {
|
||||
fs.writeFileSync(RESULT_PATH, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
||||
if (context) await context.close().catch(() => undefined);
|
||||
if (browser) await browser.close().catch(() => undefined);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
if (!result.ok) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user