From d6a25afa0a8f89806202a63de9ea7c6d7eb49a53 Mon Sep 17 00:00:00 2001 From: Agent Board Date: Mon, 13 Jul 2026 09:00:19 +0800 Subject: [PATCH] fix: move page ai launcher to pi lab --- .../browser/sidebar-page-ai-pi-lab-runtime.js | 32 +++++--- .../browser/sidebar-page-ai-runtime.js | 4 +- .../mnote-web/browser/sidebar-tree-runtime.js | 10 +++ rust/crates/mnote-web/src/routes/web_shell.rs | 2 +- rust/crates/mnote-web/src/ssr/pages/layout.rs | 4 +- .../src/ssr/styles/components/main.css | 4 + scripts/TESTING_REFERENCE.md | 2 +- scripts/desktop-hot.js | 5 +- scripts/desktop-hot.test.js | 9 +++ scripts/task-pi-lab-static-smoke.js | 8 ++ scripts/task490-runtime-surfaces-smoke.js | 73 ++++--------------- 11 files changed, 80 insertions(+), 73 deletions(-) diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js index 750320d8..d6ad70dd 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js @@ -4624,15 +4624,29 @@ function installLauncher() { if (piLabLauncherEl) return; - piLabLauncherEl = el('button', { - type: 'button', - className: 'wolai-page-ai-pi-lab-launcher', - 'data-page-ai-pi-lab-launcher': 'true', - title: 'Pi Lab', - 'aria-label': '打开 Pi Lab', - }, 'π'); - piLabLauncherEl.addEventListener('click', showPiLab); - document.body.appendChild(piLabLauncherEl); + var hostButton = document.querySelector('[data-mnote-action="open-page-ai-pi-lab"], [data-testid="wolai-floating-ai"]'); + if (hostButton instanceof HTMLElement) { + piLabLauncherEl = hostButton; + piLabLauncherEl.setAttribute('data-page-ai-pi-lab-launcher', 'true'); + piLabLauncherEl.setAttribute('data-mnote-action', 'open-page-ai-pi-lab'); + piLabLauncherEl.setAttribute('aria-label', '打开 Pi Lab'); + piLabLauncherEl.setAttribute('title', '打开 Pi Lab'); + piLabLauncherEl.textContent = 'π'; + } else { + piLabLauncherEl = el('button', { + type: 'button', + className: 'wolai-page-ai-pi-lab-launcher', + 'data-page-ai-pi-lab-launcher': 'true', + title: 'Pi Lab', + 'aria-label': '打开 Pi Lab', + }, 'π'); + document.body.appendChild(piLabLauncherEl); + } + piLabLauncherEl.addEventListener('click', function (event) { + event.preventDefault(); + event.stopPropagation(); + showPiLab(); + }); } function wireEvents() { diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js index 50c96b69..53433c19 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js @@ -880,9 +880,9 @@ export function createSidebarPageAiRuntime(context) { function pageAiOpencodeHostEnabled() { try { - return window.localStorage ? window.localStorage.getItem('mnote.page_ai.opencode_host') !== '0' : true; + return window.localStorage ? window.localStorage.getItem('mnote.page_ai.opencode_host') === '1' : false; } catch (_error) { - return true; + return false; } } diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index b1650841..59425de6 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -3191,6 +3191,16 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim return; } + var pageAiPiLabTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai-pi-lab"]'); + if (pageAiPiLabTrigger) { + e.preventDefault(); + if (window.createSidebarPageAiPiLabRuntime) { + window.createSidebarPageAiPiLabRuntime({}); + } + window.postMessage({ source: 'mnote-sidebar', type: 'mnote:pi-lab-show' }, window.location.origin); + return; + } + var navigationPageTrigger = closestAction(e.target, '[data-mnote-action="open-navigation-page"]'); if (navigationPageTrigger) { e.preventDefault(); diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 02ee8753..1abd4228 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -4159,7 +4159,7 @@ mod tests { assert!(!html.contains("[data-node-id=\"${escapedId}\"] .tree-link-title")); assert!(html.contains("data-testid=\"wolai-page-settings-trigger\"")); assert!(html.contains("data-mnote-action=\"open-page-settings\"")); - assert!(html.contains("data-mnote-action=\"open-page-ai\"")); + assert!(html.contains("data-mnote-action=\"open-page-ai-pi-lab\"")); assert!(html.contains("\"titleEndpoint\":\"/api/documents/title\"")); assert!(html.contains("data-testid=\"mnote-document-workspace\"")); assert!(html.contains("data-document-pane=\"true\"")); diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 5795dc3f..1caa1a1c 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -193,7 +193,7 @@ pub fn PageLayout(
- +
@@ -544,6 +544,8 @@ mod tests { html.find(r#"data-testid="mnote-floating-knowledge""#) < html.find(r#"data-testid="wolai-floating-ai""#) ); + assert!(html.contains(r#"data-mnote-action="open-page-ai-pi-lab""#)); + assert!(!html.contains(r#"data-mnote-action="open-page-ai""#)); assert!(!html.contains(r#"data-mnote-action="open-knowledge-rag-settings""#)); assert!(html.contains(r#"data-icon="travel_explore""#)); assert!(crate::ssr::styles::MNOTE_CSS.contains(r#"data-icon="travel_explore""#)); diff --git a/rust/crates/mnote-web/src/ssr/styles/components/main.css b/rust/crates/mnote-web/src/ssr/styles/components/main.css index 03353bf9..5d297c9e 100644 --- a/rust/crates/mnote-web/src/ssr/styles/components/main.css +++ b/rust/crates/mnote-web/src/ssr/styles/components/main.css @@ -2383,6 +2383,10 @@ text-decoration: none; } +.wolai-floating-button--pi { + font-weight: 700; +} + .document-shell[data-page-wide-layout="true"] { width: min(100%, 980px); } diff --git a/scripts/TESTING_REFERENCE.md b/scripts/TESTING_REFERENCE.md index 94c345f3..d42b242b 100644 --- a/scripts/TESTING_REFERENCE.md +++ b/scripts/TESTING_REFERENCE.md @@ -29,7 +29,7 @@ node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js node scripts/task490-runtime-surfaces-smoke.js ``` -其中 `task490-runtime-surfaces-smoke.js` 是轻量浏览器 smoke,验证页面设置、Page AI drawer 打开/关闭、Page AI stop-run abort、slash menu、block handle menu 能在当前 leptos-tiptap 文档页工作;它使用 mock run 覆盖停止按钮,不要求真实模型返回,也不替代更重的页面设置、Pi Lab/Page AI 或编辑器菜单专项脚本。 +其中 `task490-runtime-surfaces-smoke.js` 是轻量浏览器 smoke,验证页面设置、浮动 Pi Lab 入口、旧 OpenHub/opencode 前端不再打开、slash menu、block handle menu 能在当前 leptos-tiptap 文档页工作;它不要求真实模型返回,也不替代更重的 Pi Lab/Page AI 或编辑器菜单专项脚本。 ### 0.1 当前主链优先脚本 diff --git a/scripts/desktop-hot.js b/scripts/desktop-hot.js index c9c383c0..1d39aca4 100644 --- a/scripts/desktop-hot.js +++ b/scripts/desktop-hot.js @@ -6,7 +6,7 @@ * - ENABLE_BACKEND:设为 "1" or "true" 时启用默认 FastAPI 后端 * - BACKEND_CMD:覆盖 FastAPI 启动命令;设置后即视为显式启用后端 * - SKIP_BACKEND:设为 "1" or "true" 可强制跳过 FastAPI 后端 - * - ENABLE_OPENCODE:设为 "1" or "true" 时启用 opencode serve + * - ENABLE_OPENCODE:设为 "1" or "true" 时启用 opencode serve;默认不启动 * - OPENCODE_CMD:覆盖 opencode 启动命令;设置后即视为显式启用 opencode * - SKIP_OPENCODE:设为 "1" or "true" 可强制跳过 opencode * - MNOTE_OPENCODE_XDG_ROOT / MNOTE_OPENCODE_HOME:opencode 专用运行目录 @@ -128,7 +128,7 @@ function shouldStartBackend(env = process.env) { function shouldStartOpencode(env = process.env) { if (isEnabledEnv(env.SKIP_OPENCODE)) return false; if (String(env.OPENCODE_CMD || "").trim()) return true; - return true; + return isEnabledEnv(env.ENABLE_OPENCODE); } function resolveRuntimePlan(env = process.env) { @@ -757,6 +757,7 @@ module.exports = { resolveBackendExecutable, schedulePiLabWarmup, shouldStartBackend, + shouldStartOpencode, stopStaleMnoteWebCargoProcesses, terminatePid, }; diff --git a/scripts/desktop-hot.test.js b/scripts/desktop-hot.test.js index 7e436449..bf4ef51b 100644 --- a/scripts/desktop-hot.test.js +++ b/scripts/desktop-hot.test.js @@ -11,6 +11,7 @@ const { isPortFree, resolveRuntimePlan, shouldStartBackend, + shouldStartOpencode, stopStaleMnoteWebCargoProcesses, } = require("./desktop-hot.js"); @@ -188,6 +189,14 @@ test("默认跳过 FastAPI 后端,只有显式开启时才启动", () => { assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1", SKIP_BACKEND: "1" }), false); }); +test("默认跳过 opencode,只有显式开启时才启动", () => { + assert.equal(shouldStartOpencode({}), false); + assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "1" }), true); + assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "true" }), true); + assert.equal(shouldStartOpencode({ OPENCODE_CMD: "custom-opencode" }), true); + assert.equal(shouldStartOpencode({ ENABLE_OPENCODE: "1", SKIP_OPENCODE: "1" }), false); +}); + test("opencode 默认命令使用 MNote 专用运行目录", () => { const command = buildDefaultOpencodeCommand(18085); assert.match(command, /\/mnt\/Data1T\/Mnote_data\/opencode\/runtime/); diff --git a/scripts/task-pi-lab-static-smoke.js b/scripts/task-pi-lab-static-smoke.js index 7af3a9ff..2b28566f 100644 --- a/scripts/task-pi-lab-static-smoke.js +++ b/scripts/task-pi-lab-static-smoke.js @@ -12,6 +12,8 @@ const repoRoot = path.resolve(__dirname, '..'); // Files to check const files = { runtime: path.join(repoRoot, 'rust/crates/mnote-web/browser/sidebar-page-ai-pi-lab-runtime.js'), + legacyPageAiRuntime: path.join(repoRoot, 'rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js'), + layout: path.join(repoRoot, 'rust/crates/mnote-web/src/ssr/pages/layout.rs'), route: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/page_ai_pi.rs'), mod: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/mod.rs'), webShell: path.join(repoRoot, 'rust/crates/mnote-web/src/routes/web_shell.rs'), @@ -28,6 +30,8 @@ function readFile(p) { } const runtime = readFile(files.runtime); +const legacyPageAiRuntime = readFile(files.legacyPageAiRuntime); +const layout = readFile(files.layout); const route = readFile(files.route); const routesMod = readFile(files.mod); const webShell = readFile(files.webShell); @@ -151,7 +155,11 @@ const checks = [ ['runtime preserves Omniroute/freefirst as selectable model', runtime.includes("'freefirst'") && runtime.includes('modelControlOptions')], ['runtime labels freefirst option as omniroute/freefirst', runtime.includes("name: DEFAULT_MODEL_PROVIDER + '/freefirst'")], ['runtime has Pi Lab floating launcher', runtime.includes('data-page-ai-pi-lab-launcher')], + ['runtime reuses the existing floating AI slot for Pi Lab launcher', runtime.includes('[data-mnote-action="open-page-ai-pi-lab"], [data-testid="wolai-floating-ai"]')], ['runtime hides Pi Lab launcher while drawer is active', runtime.includes('piLabLauncherEl.hidden = !!piLabState.active')], + ['layout maps old floating AI slot to Pi Lab', layout.includes('data-testid="wolai-floating-ai"') && layout.includes('data-mnote-action="open-page-ai-pi-lab"') && layout.includes('{"\\u{03c0}"}')], + ['layout no longer exposes legacy Page AI/OpenHub action from floating button', !layout.includes('data-mnote-action="open-page-ai"> { - const drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); - const trigger = document.querySelector('[data-testid="wolai-floating-ai"]'); - return drawer instanceof HTMLElement - && drawer.hidden === true - && trigger instanceof HTMLElement - && trigger.getAttribute("data-state") === "closed" - && trigger.getAttribute("aria-expanded") === "false"; - }, - null, - { timeout: UI_TIMEOUT_MS }, - ); - - await aiButton.click({ timeout: UI_TIMEOUT_MS }); - await aiDrawer.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); - await page.locator("[data-page-ai-input]").fill("Task490 stop smoke", { timeout: UI_TIMEOUT_MS }); - await page.locator('[data-page-ai-action="send"]').click({ timeout: UI_TIMEOUT_MS }); - await page.waitForFunction( - () => document.documentElement.getAttribute("data-mnote-page-ai-run-status") === "running" - && document.documentElement.getAttribute("data-mnote-page-ai-run-id") === "run_task490_stop", - null, - { timeout: UI_TIMEOUT_MS }, - ); - const stopButton = page.locator('.wolai-page-ai-stop[data-page-ai-action="stop-run"]').first(); - await stopButton.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); - await page.waitForFunction( - () => { - const stop = document.querySelector('.wolai-page-ai-stop[data-page-ai-action="stop-run"]'); - return stop instanceof HTMLButtonElement && stop.disabled === false && stop.getAttribute("aria-disabled") === "false"; - }, - null, - { timeout: UI_TIMEOUT_MS }, - ); - await stopButton.click({ timeout: UI_TIMEOUT_MS }); - await page.waitForFunction( - () => document.documentElement.getAttribute("data-mnote-page-ai-run-status") === "aborted", - null, - { timeout: UI_TIMEOUT_MS }, - ); - await page.locator('[data-page-ai-conversation]').getByText("已请求停止当前 AI run。").waitFor({ - state: "visible", - timeout: UI_TIMEOUT_MS, - }); - assert(capturedPageAi.runs.length === 1, "Page AI stop smoke 应先发起一个 run"); - assert(capturedPageAi.aborts.length === 1, "点击停止应调用 abort API 一次"); - const abortBody = JSON.parse(capturedPageAi.aborts[0] || "{}"); - assert(abortBody.reason === "page_ai_user_stop", "abort API 应记录用户停止原因"); - screenshots.pageAiStopped = await saveScreenshot(page, "02b-page-ai-stopped"); - await closePageAiIfOpen(page); + const piDrawer = page.locator('[data-page-ai-pi-lab="drawer"]').first(); + await piDrawer.waitFor({ state: "visible", timeout: UI_TIMEOUT_MS }); + assert(await page.locator("[data-page-ai-pi-lab-input]").first().isVisible(), "Pi Lab drawer 必须显示输入框"); + const retiredFrontend = await page.evaluate(() => ({ + legacyDrawerOpen: Boolean(document.querySelector('[data-testid="wolai-page-ai-drawer"]:not([hidden])')), + opencodeHost: Boolean(document.querySelector('[data-page-ai-opencode-host="true"]')), + opencodeIframe: Boolean(document.querySelector('[data-page-ai-opencode-iframe]')), + })); + assert(retiredFrontend.legacyDrawerOpen === false, "浮动 Pi 入口不能打开旧 Page AI/OpenHub drawer"); + assert(retiredFrontend.opencodeHost === false, "浮动 Pi 入口不能打开 opencode/OpenHub host 前端"); + assert(retiredFrontend.opencodeIframe === false, "浮动 Pi 入口不能保留 opencode/OpenHub iframe"); + screenshots.pageAi = await saveScreenshot(page, "02-pi-lab"); + await page.locator("[data-page-ai-pi-lab-close]").click({ timeout: UI_TIMEOUT_MS }); + await piDrawer.waitFor({ state: "hidden", timeout: UI_TIMEOUT_MS }); await setBlockHandleFixture(page, UI_TIMEOUT_MS); await editor.click({ timeout: UI_TIMEOUT_MS });