@@ -3263,7 +3265,7 @@ pub fn AiManagementPage(
@@ -3308,7 +3310,7 @@ pub fn AiManagementPage(
diff --git a/rust/crates/mnote-web/src/ssr/styles/components/page-ai.css b/rust/crates/mnote-web/src/ssr/styles/components/page-ai.css
index 8bb5b57a..a9303436 100644
--- a/rust/crates/mnote-web/src/ssr/styles/components/page-ai.css
+++ b/rust/crates/mnote-web/src/ssr/styles/components/page-ai.css
@@ -2221,71 +2221,10 @@ button.wolai-page-ai-history-main span {
overflow: hidden;
}
-.wolai-page-ai-drawer[data-page-ai-openhub-host="true"] .wolai-page-ai-panel {
- display: flex;
- flex-direction: column;
- gap: 0;
- padding: 0;
- border: 0;
- overflow: hidden;
-}
-
-.wolai-page-ai-drawer[data-page-ai-openhub-host="true"] .wolai-page-ai-resize-handle {
- left: -12px;
-}
-
.wolai-page-ai-opencode-header {
flex: 0 0 auto;
}
-.wolai-page-ai-openhub-diagnostics {
- flex: 0 0 auto;
- max-height: 32px;
- overflow: hidden;
- border-bottom: 1px solid rgba(27, 28, 28, 0.08);
- background: rgba(247, 247, 245, 0.78);
- color: rgba(27, 28, 28, 0.58);
- font-size: 12px;
-}
-
-.wolai-page-ai-openhub-diagnostics[hidden] {
- display: none !important;
-}
-
-.wolai-page-ai-openhub-diagnostics summary {
- min-height: 32px;
- display: flex;
- align-items: center;
- padding: 0 12px;
- cursor: pointer;
- list-style: none;
-}
-
-.wolai-page-ai-openhub-diagnostics summary::-webkit-details-marker {
- display: none;
-}
-
-.wolai-page-ai-openhub-diagnostics:not([open]) > :not(summary) {
- display: none !important;
-}
-
-.wolai-page-ai-openhub-diagnostics[open] {
- max-height: min(36vh, 220px);
- overflow: auto;
- padding-bottom: 10px;
-}
-
-.wolai-page-ai-openhub-diagnostics[open] summary {
- margin-bottom: 6px;
-}
-
-.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-row,
-.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-empty,
-.wolai-page-ai-openhub-diagnostics .wolai-page-ai-opencode-badges {
- margin-right: 12px;
- margin-left: 12px;
-}
-
.wolai-page-ai-opencode-chrome {
display: flex;
flex: 0 0 auto;
diff --git a/scripts/TESTING_REFERENCE.md b/scripts/TESTING_REFERENCE.md
index 57b496d6..94c345f3 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 覆盖停止按钮,不要求真实模型返回,也不替代更重的页面设置、OpenHub/Page AI 或编辑器菜单专项脚本。
+其中 `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 或编辑器菜单专项脚本。
### 0.1 当前主链优先脚本
@@ -37,7 +37,7 @@ node scripts/task490-runtime-surfaces-smoke.js
- Rust SSR 文档页 / Page Aggregate:local-first 默认优先 `task167-local-markdown-title-body-options-no-convex-smoke.js`;Page Aggregate browser conversion / compat fallback 改动补跑 `task522-page-aggregate-compat-fallback-contract.js`;cloud/control-plane 文档可补跑 `task110-page-title-single-truth-smoke.js`、`task-page-aggregate-body-sync-smoke.js`、`task-page-aggregate-options-sync-smoke.js`、`task-page-aggregate-refresh-persistence-smoke.js`
- leptos-tiptap runtime surface:`task490-runtime-surfaces-smoke.js`;需要验证保存回读可补跑 `task121-rust-web-editor-island-hydration-smoke.js`,但它仍使用 `/api/tree/commands create` 准备文档,不作为无 Convex 默认基线;需要验证浮层互斥和更多菜单状态时再跑 `task158-e30-menu-state-smoke.js`
- local-first 本地工作区:`task164-desktop-hot-local-folder-main-entry-smoke.js`、`task166-local-first-managed-workspace-no-convex-smoke.js`、`task167-local-markdown-title-body-options-no-convex-smoke.js`、`task436-local-markdown-open-document-external-change-smoke.js`、`task443-local-markdown-asset-upload-smoke.js`、`task451-local-markdown-conflict-resolution-ui-smoke.js`、`task452-local-search-index-browser-smoke.js`、`task453-local-folder-page-ai-changed-files-smoke.js`;WorkspacePath / ObjectIdentity runtime 消费统一改动补跑 `task524-workspace-object-identity-matrix-smoke.js`。`task452` 只覆盖普通本地搜索、settings API、tag/backlink API 和旧 local-index UI 不复活;资料库问答、PDF/Office/image source ingestion 与引用回跳改跑 WeKnora / provider-neutral knowledge-rag 脚本。
-- WeKnora / OpenHub 资料库:`task781-knowledge-bases-registry-smoke.js`、`task785-weknora-create-kb-folder-ingest-search-smoke.js`、`task787-weknora-default-lightrag-legacy-smoke.js`、`task772-openhub-weknora-kb-ui-smoke.js` 覆盖默认 provider、知识库列表、摄取、搜索、引用桥接与 legacy LightRAG guard。旧 `task529/530/531/534` LightRAG 脚本只作为 legacy fallback / 历史对照,不进入默认回归。旧 `task526-local-folder-ocr-api-smoke.js` / OCR sidecar 链路已退役并软归档;`/api/search/documents` 不作为 WeKnora、LightRAG legacy、LiteParse、OCR sidecar 或 evidence.sqlite 的资料库问答 fallback。
+- LightRAG / Knowledge RAG 资料库:`task781-knowledge-bases-registry-smoke.js`、`task785-weknora-create-kb-folder-ingest-search-smoke.js`、`task787-weknora-default-lightrag-legacy-smoke.js` 覆盖默认 provider、知识库列表、摄取、搜索、引用桥接与 legacy LightRAG guard。旧 `task529/530/531/534` LightRAG 脚本只作为 legacy fallback / 历史对照,不进入默认回归。旧 OpenHub 专项资料库 smoke 已软归档到 `recycle/20260712-pi-ts-openhub-retirement/`;旧 `task526-local-folder-ocr-api-smoke.js` / OCR sidecar 链路已退役并软归档;`/api/search/documents` 不作为 WeKnora、LightRAG legacy、LiteParse、OCR sidecar 或 evidence.sqlite 的资料库问答 fallback。
- local Markdown conflict regression:`task510-local-markdown-conflict-regression-group.js` 串联真实冲突、连续上传、新建页面、外部恢复、多 tab CAS 与上传 409 孤儿策略;可用 `MNOTE_CONFLICT_REGRESSION_TASKS=a.js,b.js` 做定点子集。
- tree realtime / live cache:`task446-tree-rename-dual-browser-live-smoke.js`、`task447-tree-move-order-dual-browser-live-smoke.js`、`task448-tree-resync-recovery-dual-browser-smoke.js`、`task449-tree-sse-reconnect-snapshot-recovery-smoke.js`
- 资源对象与 mindmap:`task455-local-folder-mindmap-clean-smoke.js`、`task456-resource-object-shell-sync-smoke.js`、`task166-mindmap-phase6-block-smoke.js`、`task167-mindmap-kmind-parity-smoke.js`、`task168-mindmap-put-validator-smoke.js`;Page AI mindmap skill / 资源生成改动补跑 `task503-mindmap-skill-capability-smoke.js`,真实 mindmap resource tab / Page AI target 改动补跑 `task525-page-ai-mindmap-resource-target-smoke.js`
@@ -123,7 +123,7 @@ node scripts/task490-runtime-surfaces-smoke.js
- `task129-wolai-aline-baseline-smoke.js`
- `task130` 到 `task137` 一系列 `wolai-aline-*`
- `task160-wolai-page-settings-shell-smoke.js`
-- 页面 AI 旧 `/api/ai-agent/run` 视觉 smoke 已移入本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`;当前页面 AI 验证优先使用 OpenHub / opencode / Page AI smoke 与 `task-page-block-ai-tools-smoke.js`。Hermes 相关 smoke 只作为 legacy guard / 历史对照。
+- 页面 AI 旧 `/api/ai-agent/run` 视觉 smoke 已移入本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/`;当前页面 AI 验证优先使用 Pi Lab / opencode / Page AI smoke 与 `task-page-block-ai-tools-smoke.js`。Hermes 相关 smoke 只作为 legacy guard / 历史对照。
这一层最重要的原则不是“文案命中”,而是:
@@ -193,7 +193,7 @@ node scripts/task097-homepage-entry-smoke.js
- local-first:优先使用 `task164-desktop-hot-local-folder-main-entry-smoke.js`、`task166-local-first-managed-workspace-no-convex-smoke.js`、`task167-local-markdown-title-body-options-no-convex-smoke.js`、`task455-local-folder-mindmap-clean-smoke.js`
- 页面设置:`task160`、`task164-page-options-visible-effect-smoke.js`
- 双栏:`task165`
-- AI:优先使用 OpenHub / opencode / Page AI 当前主线 smoke、`task-page-block-ai-tools-smoke.js` 和 `task-hermes-page-ai-retirement-guard.js` 这类 legacy guard;Hermes 页面 AI smoke 只作为历史对照。Page AI history / skills / ChatOnly 相关改动补跑 `task503-mindmap-skill-capability-smoke.js`、`task504-page-ai-history-agent-filter-smoke.js`、`task512-chatonly-doubao-sync-smoke.js`、`task513-chatonly-provider-sync-smoke.js`;OnlyOffice live bridge 改动补跑 `task515-onlyoffice-live-scope-http-smoke.js`、`task516-onlyoffice-bridge-multisession-browser-smoke.js`、`task517-onlyoffice-bridge-plugin-direct-smoke.js`、`task518-onlyoffice-real-iframe-session-scope-smoke.js`,其中 `task516/517` 覆盖 bridge session lifecycle 与 `docKey/pageOrigin` 元数据;Page AI 真实 target picker / Office live session 改动补跑 `task523-page-ai-onlyoffice-real-target-session-smoke.js`;`task155`、`task156`、`task161` 等旧 `/api/ai-agent/run` smoke 只保留在本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/` 做历史对照
+- AI:优先使用 Pi Lab / opencode / Page AI 当前主线 smoke、`task-page-block-ai-tools-smoke.js` 和 `task-hermes-page-ai-retirement-guard.js` 这类 legacy guard;Hermes 页面 AI smoke 只作为历史对照。Page AI history / skills / ChatOnly 相关改动补跑 `task503-mindmap-skill-capability-smoke.js`、`task504-page-ai-history-agent-filter-smoke.js`、`task512-chatonly-doubao-sync-smoke.js`、`task513-chatonly-provider-sync-smoke.js`;OnlyOffice live bridge 改动补跑 `task515-onlyoffice-live-scope-http-smoke.js`、`task516-onlyoffice-bridge-multisession-browser-smoke.js`、`task517-onlyoffice-bridge-plugin-direct-smoke.js`、`task518-onlyoffice-real-iframe-session-scope-smoke.js`,其中 `task516/517` 覆盖 bridge session lifecycle 与 `docKey/pageOrigin` 元数据;Page AI 真实 target picker / Office live session 改动补跑 `task523-page-ai-onlyoffice-real-target-session-smoke.js`;`task155`、`task156`、`task161` 等旧 `/api/ai-agent/run` smoke 只保留在本机 gitignored `recycle/scripts/retired-ai-agent-run-smokes/` 做历史对照
原则:
@@ -439,7 +439,7 @@ const controlPlaneEnv = buildControlPlaneTestEnv(dataRoot, process.env);
- `rust/crates/` 下的 Rust 代码通过 `ControlPlaneStore` trait 访问数据库是正常路径。
- `scripts/task-control-plane-admin-libsql-roundtrip-smoke.js` 使用 `cargo run --bin control-plane-admin`,走 Rust admin CLI。
- `sqlite` fallback、admin CLI、legacy evidence/local_search 不在脚本纪律约束范围内。
-- OpenHub 自身 SQLite 不绑定本轮 control-plane Turso 切换;OpenHub 数据库迁移独立安排,不在当前首轮范围内。
+- 已退役 AI host 自身 SQLite 不绑定本轮 control-plane Turso 切换;历史迁移说明保留在 recycle 归档中。
### 7.4 违规后果
diff --git a/scripts/desktop-hot.js b/scripts/desktop-hot.js
index 996cffb1..c9c383c0 100644
--- a/scripts/desktop-hot.js
+++ b/scripts/desktop-hot.js
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
- * 热启动 mnote-web 单入口,以及按需启用的 FastAPI。
+ * 热启动 mnote-web 单入口,以及按需启用的 FastAPI / opencode。
* 可使用以下环境变量调整行为:
* - ENABLE_BACKEND:设为 "1" or "true" 时启用默认 FastAPI 后端
* - BACKEND_CMD:覆盖 FastAPI 启动命令;设置后即视为显式启用后端
@@ -9,16 +9,7 @@
* - ENABLE_OPENCODE:设为 "1" or "true" 时启用 opencode serve
* - OPENCODE_CMD:覆盖 opencode 启动命令;设置后即视为显式启用 opencode
* - SKIP_OPENCODE:设为 "1" or "true" 可强制跳过 opencode
- * - ENABLE_OPENHUB:设为 "1" or "true" 时启用 OpenHub FastAPI
- * - OPENHUB_CMD / OPENHUB_BACKEND_CMD:覆盖 OpenHub FastAPI 启动命令;设置后即视为显式启用 OpenHub
- * - OPENHUB_HOST / OPENHUB_BIND_HOST:OpenHub FastAPI 监听地址,默认 0.0.0.0 便于局域网访问
- * - OPENHUB_PORT / OPENHUB_BACKEND_PORT:OpenHub FastAPI 端口,默认 18080
- * - SKIP_OPENHUB:设为 "1" or "true" 可强制跳过 OpenHub
- * - OPENHUB_REDIS_URL / OPENHUB_REDIS_DB:记录 OpenHub Redis 位置;OPENHUB_REDIS_HEALTH_URL 可选做 HTTP health 检查
- * - OPENHUB_REDIS_HEALTH_URL:可选 Redis health URL;dev-hot 只检查,不释放或结束 OpenHub 相关端口
- * - OPENHUB_OPENCODE_BASE_URL:OpenHub 侧 opencode serve base URL;默认复用 MNOTE_OPENCODE_BASE_URL
- * - SKIP_OPENHUB_HEALTH:设为 "1" or "true" 可跳过 OpenHub/Redis/opencode health 预检
- * - MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE:默认 "1",禁用 OpenHub Git snapshot/restore/revert 写链
+ * - MNOTE_OPENCODE_XDG_ROOT / MNOTE_OPENCODE_HOME:opencode 专用运行目录
* - MNOTE_CONTROL_PLANE_BACKEND:控制面后端,默认 libsql-local;可设 turso-remote / turso-local-replica / turso-synced
* - MNOTE_TURSO_LOCAL_PATH:libsql-local 本地库路径,默认 /mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db
* - MNOTE_PAGE_AI_PI_WARMUP:设为 "1" or "true" 时,在 mnote-web 可用后预启动 Pi Lab runtime / MCP cache
@@ -64,7 +55,6 @@ function resolveBackendExecutable(envName, fallbackName) {
const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
const backendPortFromEnv = Number(process.env.BACKEND_PORT || 8000);
const opencodePortFromEnv = Number(process.env.OPENCODE_PORT || 4096);
-const openhubPortFromEnv = Number(process.env.OPENHUB_BACKEND_PORT || process.env.OPENHUB_PORT || 18080);
const defaultControlPlaneDir = "/mnt/Data1T/Mnote_data/control-plane";
function hasCommand(command) {
@@ -90,8 +80,8 @@ function buildDefaultBackendCommand(port) {
}
function buildDefaultOpencodeCommand(port) {
- const opencodeXdgRoot = process.env.OPENHUB_OPENCODE_XDG_ROOT || "/mnt/Data1T/Mnote_data/openhub/opencode-runtime";
- const opencodeHome = process.env.OPENHUB_OPENCODE_HOME || "/mnt/Data1T/Mnote_data/openhub/opencode-home";
+ const opencodeXdgRoot = process.env.MNOTE_OPENCODE_XDG_ROOT || "/mnt/Data1T/Mnote_data/opencode/runtime";
+ const opencodeHome = process.env.MNOTE_OPENCODE_HOME || "/mnt/Data1T/Mnote_data/opencode/home";
const modelEnvNames = [
"OPENCODE_API_KEY",
"OPENAI_API_KEY",
@@ -124,19 +114,6 @@ function buildDefaultOpencodeCommand(port) {
].join(" && ");
}
-function buildDefaultOpenHubCommand(port) {
- const openhubBackendDir = process.env.OPENHUB_BACKEND_DIR || "/mnt/Data1T/Mnote_data/openhub/OpenHub/smart-query-backend";
- const openhubHost = process.env.OPENHUB_HOST || process.env.OPENHUB_BIND_HOST || "0.0.0.0";
- const uvicornBin = fs.existsSync(path.join(openhubBackendDir, ".venv", "bin", "uvicorn"))
- ? path.join(openhubBackendDir, ".venv", "bin", "uvicorn")
- : "uvicorn";
- const opencodeBaseUrl = process.env.OPENHUB_OPENCODE_BASE_URL || process.env.MNOTE_OPENCODE_BASE_URL || `http://127.0.0.1:${opencodePortFromEnv}`;
- return [
- `cd ${JSON.stringify(openhubBackendDir)}`,
- `OPENCODE_BASE_URL=${JSON.stringify(opencodeBaseUrl)} MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE=${JSON.stringify(process.env.MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE || "1")} exec ${JSON.stringify(uvicornBin)} app.main:app --host ${JSON.stringify(openhubHost)} --port ${port}`,
- ].join(" && ");
-}
-
function isEnabledEnv(value) {
const normalized = String(value || "").toLowerCase();
return normalized === "1" || normalized === "true";
@@ -154,54 +131,6 @@ function shouldStartOpencode(env = process.env) {
return true;
}
-function shouldStartOpenHub(env = process.env) {
- if (isEnabledEnv(env.SKIP_OPENHUB)) return false;
- if (String(env.OPENHUB_BACKEND_CMD || env.OPENHUB_CMD || "").trim()) return true;
- return isEnabledEnv(env.ENABLE_OPENHUB);
-}
-
-function shouldCheckOpenHubHealth(env = process.env) {
- if (isEnabledEnv(env.SKIP_OPENHUB_HEALTH)) return false;
- return shouldStartOpenHub(env) || isEnabledEnv(env.CHECK_OPENHUB_HEALTH);
-}
-
-function resolveOpenHubHealthPlan(env = process.env) {
- const openhubPort = Number(env.OPENHUB_BACKEND_PORT || env.OPENHUB_PORT || openhubPortFromEnv);
- const opencodePort = Number(env.OPENCODE_PORT || opencodePortFromEnv);
- const baseUrl = String(env.MNOTE_OPENHUB_BASE_URL || env.OPENHUB_BASE_URL || `http://127.0.0.1:${openhubPort}`).replace(/\/+$/, "");
- const redisHealthUrl = String(env.OPENHUB_REDIS_HEALTH_URL || "").trim();
- const redisUrl = String(env.OPENHUB_REDIS_URL || "").trim();
- const redisDb = String(env.OPENHUB_REDIS_DB || "").trim();
- const opencodeBaseUrl = String(env.OPENHUB_OPENCODE_BASE_URL || env.MNOTE_OPENCODE_BASE_URL || `http://127.0.0.1:${opencodePort}`).replace(/\/+$/, "");
- const requireHealth = isEnabledEnv(env.REQUIRE_OPENHUB_HEALTH);
- return {
- enabled: shouldCheckOpenHubHealth(env),
- openhub: {
- label: "OpenHub FastAPI",
- url: env.OPENHUB_HEALTH_URL || `${baseUrl}/api/health`,
- required: requireHealth,
- },
- redis: {
- label: "OpenHub Redis",
- url: redisHealthUrl,
- redisUrl,
- redisDb,
- required: isEnabledEnv(env.REQUIRE_OPENHUB_REDIS_HEALTH),
- skipped: !redisHealthUrl,
- },
- opencode: {
- label: "opencode",
- url: env.OPENCODE_HEALTH_URL || `${opencodeBaseUrl}/global/health`,
- required: requireHealth,
- },
- gitSnapshotRestore: {
- env: "MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE",
- value: String(env.MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE || "1"),
- defaultDisabled: true,
- },
- };
-}
-
function resolveRuntimePlan(env = process.env) {
const frontendPort = Number(env.FRONTEND_PORT || 3000);
const skipGateway = false;
@@ -234,8 +163,6 @@ function resolveRuntimePlan(env = process.env) {
MNOTE_WEB_PUBLIC_BIND: env.MNOTE_WEB_PUBLIC_BIND || `127.0.0.1:${publicPort}`,
MNOTE_KNOWLEDGE_PROVIDER: env.MNOTE_KNOWLEDGE_PROVIDER || "lightrag_legacy",
MNOTE_OPENCODE_BASE_URL: env.MNOTE_OPENCODE_BASE_URL || `http://127.0.0.1:${opencodePortFromEnv}`,
- MNOTE_OPENHUB_BASE_URL: env.MNOTE_OPENHUB_BASE_URL || `http://127.0.0.1:${openhubPortFromEnv}`,
- MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE: env.MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE || "1",
...controlPlaneEnv,
},
};
@@ -279,18 +206,6 @@ const tasks = [
},
]
: []),
- ...(shouldStartOpenHub(process.env)
- ? [
- {
- name: "openhub",
- command:
- process.env.OPENHUB_BACKEND_CMD ||
- process.env.OPENHUB_CMD ||
- buildDefaultOpenHubCommand(openhubPortFromEnv),
- cwd: rootDir,
- },
- ]
- : []),
];
function findTask(name) {
@@ -613,32 +528,6 @@ async function ensurePortFree(port, nameForLog) {
return false;
}
-async function checkHttpHealth(url, label, required) {
- if (!url) {
- logPrefix("openhub-health", `${label} health 未配置,已跳过。`);
- return true;
- }
- try {
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), 1200);
- const response = await fetch(url, {
- method: "GET",
- headers: { accept: "application/json,text/plain,*/*" },
- signal: controller.signal,
- });
- clearTimeout(timer);
- if (response.ok) {
- logPrefix("openhub-health", `${label} 可达:${url}`);
- return true;
- }
- logPrefix("openhub-health", `${label} 返回 HTTP ${response.status}:${url}`);
- return !required;
- } catch (error) {
- logPrefix("openhub-health", `${label} 不可达:${url} (${error.message})`);
- return !required;
- }
-}
-
async function isHttpHealthy(url) {
if (!url) return false;
try {
@@ -656,24 +545,6 @@ async function isHttpHealthy(url) {
}
}
-async function checkOpenHubHealth(plan = resolveOpenHubHealthPlan(process.env)) {
- if (!plan.enabled) {
- return true;
- }
- logPrefix(
- "openhub-health",
- `Git snapshot/restore 默认关闭:${plan.gitSnapshotRestore.env}=${plan.gitSnapshotRestore.value || "1"}`,
- );
- const checks = [
- await checkHttpHealth(plan.openhub.url, plan.openhub.label, plan.openhub.required),
- plan.redis.skipped
- ? (logPrefix("openhub-health", "OpenHub Redis health 未配置,已跳过非破坏性检查。"), true)
- : await checkHttpHealth(plan.redis.url, plan.redis.label, plan.redis.required),
- await checkHttpHealth(plan.opencode.url, plan.opencode.label, plan.opencode.required),
- ];
- return checks.every(Boolean);
-}
-
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, "utf8");
@@ -854,27 +725,6 @@ async function main() {
}
- if (shouldStartOpenHub(process.env) && !process.env.OPENHUB_BACKEND_CMD && !process.env.OPENHUB_CMD) {
- const openhubPortOk = await ensurePortFree(openhubPortFromEnv, "openhub");
- if (!openhubPortOk) {
- console.error(`OpenHub 端口 ${openhubPortFromEnv} 无法释放,已中止启动。`);
- process.exit(1);
- }
- const openhubTask = findTask("openhub");
- if (!openhubTask) {
- throw new Error("缺少 OpenHub 任务配置");
- }
- openhubTask.command = buildDefaultOpenHubCommand(openhubPortFromEnv);
- } else if (isEnabledEnv(process.env.SKIP_OPENHUB)) {
- logPrefix("openhub", "已跳过 OpenHub FastAPI(SKIP_OPENHUB=1)。");
- }
-
- const healthOk = await checkOpenHubHealth(resolveOpenHubHealthPlan(process.env));
- if (!healthOk) {
- console.error("OpenHub health 预检失败,已中止启动。");
- process.exit(1);
- }
-
if (tasks.length === 0) {
console.error("未配置任何可运行的任务,检查环境变量设置。");
process.exit(1);
@@ -895,8 +745,6 @@ if (require.main === module) {
module.exports = {
buildDefaultOpencodeCommand,
- buildDefaultOpenHubCommand,
- checkOpenHubHealth,
collectStaleMnoteWebCargoPids,
ensurePortFree,
getListeningPidsByPort,
@@ -905,12 +753,10 @@ module.exports = {
getProcessNameByPid,
isHttpHealthy,
isPortFree,
- resolveOpenHubHealthPlan,
resolveRuntimePlan,
resolveBackendExecutable,
schedulePiLabWarmup,
shouldStartBackend,
- shouldStartOpenHub,
stopStaleMnoteWebCargoProcesses,
terminatePid,
};
diff --git a/scripts/desktop-hot.test.js b/scripts/desktop-hot.test.js
index 99454569..7e436449 100644
--- a/scripts/desktop-hot.test.js
+++ b/scripts/desktop-hot.test.js
@@ -4,16 +4,13 @@ const net = require("node:net");
const { test } = require("node:test");
const {
buildDefaultOpencodeCommand,
- buildDefaultOpenHubCommand,
collectStaleMnoteWebCargoPids,
resolveBackendExecutable,
ensurePortFree,
isHttpHealthy,
isPortFree,
- resolveOpenHubHealthPlan,
resolveRuntimePlan,
shouldStartBackend,
- shouldStartOpenHub,
stopStaleMnoteWebCargoProcesses,
} = require("./desktop-hot.js");
@@ -145,8 +142,6 @@ test("默认热启动计划只使用 mnote-web 作为 3000 owner", () => {
MNOTE_WEB_PUBLIC_BIND: "127.0.0.1:3000",
MNOTE_KNOWLEDGE_PROVIDER: "lightrag_legacy",
MNOTE_OPENCODE_BASE_URL: "http://127.0.0.1:4096",
- MNOTE_OPENHUB_BASE_URL: "http://127.0.0.1:18080",
- MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE: "1",
MNOTE_CONTROL_PLANE_BACKEND: "libsql-local",
MNOTE_TURSO_LOCAL_PATH: "/mnt/Data1T/Mnote_data/control-plane/control-plane-libsql.db",
});
@@ -193,50 +188,10 @@ test("默认跳过 FastAPI 后端,只有显式开启时才启动", () => {
assert.equal(shouldStartBackend({ ENABLE_BACKEND: "1", SKIP_BACKEND: "1" }), false);
});
-test("OpenHub FastAPI 默认跳过,显式开启或命令覆盖时才启动", () => {
- assert.equal(shouldStartOpenHub({}), false);
- assert.equal(shouldStartOpenHub({ ENABLE_OPENHUB: "1" }), true);
- assert.equal(shouldStartOpenHub({ ENABLE_OPENHUB: "true" }), true);
- assert.equal(shouldStartOpenHub({ OPENHUB_CMD: "custom-openhub" }), true);
- assert.equal(shouldStartOpenHub({ ENABLE_OPENHUB: "1", SKIP_OPENHUB: "1" }), false);
-});
-
-test("OpenHub health plan 包含 FastAPI、Redis、opencode 和默认关闭 Git snapshot/restore", () => {
- const plan = resolveOpenHubHealthPlan({
- ENABLE_OPENHUB: "1",
- REQUIRE_OPENHUB_HEALTH: "1",
- OPENHUB_PORT: "18081",
- OPENCODE_PORT: "4097",
- OPENHUB_REDIS_HEALTH_URL: "http://127.0.0.1:6379/health",
- });
-
- assert.equal(plan.enabled, true);
- assert.equal(plan.openhub.url, "http://127.0.0.1:18081/api/health");
- assert.equal(plan.openhub.required, true);
- assert.equal(plan.redis.url, "http://127.0.0.1:6379/health");
- assert.equal(plan.redis.skipped, false);
- assert.equal(plan.opencode.url, "http://127.0.0.1:4097/global/health");
- assert.deepEqual(plan.gitSnapshotRestore, {
- env: "MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE",
- value: "1",
- defaultDisabled: true,
- });
-});
-
-test("OpenHub 默认命令只启动 FastAPI,不包含 snapshot/restore 写操作", () => {
- const command = buildDefaultOpenHubCommand(18082);
- assert.match(command, /app\.main:app/);
- assert.match(command, /--host "?0\.0\.0\.0"?/);
- assert.match(command, /--port 18082/);
- assert.match(command, /\/mnt\/Data1T\/Mnote_data\/openhub\/OpenHub\/smart-query-backend/);
- assert.match(command, /MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE="1"/);
- assert.doesNotMatch(command, /git\s+(snapshot|restore|revert)|kill-port/i);
-});
-
-test("opencode 默认命令使用 OpenHub 专用运行目录", () => {
+test("opencode 默认命令使用 MNote 专用运行目录", () => {
const command = buildDefaultOpencodeCommand(18085);
- assert.match(command, /\/mnt\/Data1T\/Mnote_data\/openhub\/opencode-runtime/);
- assert.match(command, /\/mnt\/Data1T\/Mnote_data\/openhub\/opencode-home/);
+ assert.match(command, /\/mnt\/Data1T\/Mnote_data\/opencode\/runtime/);
+ assert.match(command, /\/mnt\/Data1T\/Mnote_data\/opencode\/home/);
assert.match(command, /HOME=/);
assert.match(command, /XDG_CONFIG_HOME=/);
assert.doesNotMatch(command, /已检测到现有 opencode/);
diff --git a/scripts/dev-hot.js b/scripts/dev-hot.js
index da7c164f..13491ba7 100644
--- a/scripts/dev-hot.js
+++ b/scripts/dev-hot.js
@@ -61,19 +61,7 @@ function devHotBindAddr(env = process.env) {
function buildDevHotEnv(baseEnv = process.env) {
const opencodePort = String(baseEnv.OPENCODE_PORT || "4096").trim();
- const openhubPort = String(baseEnv.OPENHUB_BACKEND_PORT || baseEnv.OPENHUB_PORT || "18080").trim();
const opencodeBaseUrl = String(baseEnv.MNOTE_OPENCODE_BASE_URL || `http://127.0.0.1:${opencodePort}`).trim();
- const openhubBaseUrl = String(baseEnv.MNOTE_OPENHUB_BASE_URL || `http://127.0.0.1:${openhubPort}`).trim();
- const openhubBackendDir = String(
- baseEnv.OPENHUB_BACKEND_DIR || "/mnt/Data1T/Mnote_data/openhub/OpenHub/smart-query-backend"
- ).trim();
- const openhubOpencodeXdgRoot = String(
- baseEnv.OPENHUB_OPENCODE_XDG_ROOT || "/mnt/Data1T/Mnote_data/openhub/opencode-runtime"
- ).trim();
- const openhubOpencodeHome = String(
- baseEnv.OPENHUB_OPENCODE_HOME || "/mnt/Data1T/Mnote_data/openhub/opencode-home"
- ).trim();
- const skipOpenHub = String(baseEnv.SKIP_OPENHUB || "").trim();
const controlPlaneBackend = String(baseEnv.MNOTE_CONTROL_PLANE_BACKEND || "libsql-local").trim() || "libsql-local";
if (controlPlaneBackend === "sqlite") {
throw new Error("dev:hot 不再支持 SQLite control-plane fallback;请使用 libsql-local/turso-local-replica/turso-remote/turso-synced");
@@ -87,16 +75,7 @@ function buildDevHotEnv(baseEnv = process.env) {
MNOTE_WEB_BIND: devHotBindAddr(baseEnv),
MNOTE_WEB_CMD: String(baseEnv.MNOTE_WEB_CMD || "").trim() || cargoWatchCommand(baseEnv),
MNOTE_KNOWLEDGE_PROVIDER: String(baseEnv.MNOTE_KNOWLEDGE_PROVIDER || "lightrag_legacy").trim(),
- ENABLE_OPENHUB: skipOpenHub ? String(baseEnv.ENABLE_OPENHUB || "") : "1",
- CHECK_OPENHUB_HEALTH: String(baseEnv.CHECK_OPENHUB_HEALTH || "1"),
MNOTE_OPENCODE_BASE_URL: opencodeBaseUrl,
- OPENHUB_OPENCODE_BASE_URL: String(baseEnv.OPENHUB_OPENCODE_BASE_URL || opencodeBaseUrl).trim(),
- OPENHUB_BACKEND_DIR: openhubBackendDir,
- OPENHUB_OPENCODE_XDG_ROOT: openhubOpencodeXdgRoot,
- OPENHUB_OPENCODE_HOME: openhubOpencodeHome,
- MNOTE_OPENHUB_BASE_URL: openhubBaseUrl,
- OPENHUB_HEALTH_URL: String(baseEnv.OPENHUB_HEALTH_URL || `${openhubBaseUrl.replace(/\/+$/, "")}/api/health`).trim(),
- MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE: String(baseEnv.MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE || "1"),
MNOTE_CONTROL_PLANE_BACKEND: controlPlaneBackend,
MNOTE_PAGE_AI_PI_WARMUP: String(baseEnv.MNOTE_PAGE_AI_PI_WARMUP ?? "1").trim() || "1",
MNOTE_PAGE_AI_PI_WARMUP_SEND: String(baseEnv.MNOTE_PAGE_AI_PI_WARMUP_SEND ?? "0").trim() || "0",
diff --git a/scripts/task-ai-management-browser-smoke.js b/scripts/task-ai-management-browser-smoke.js
index 7148443c..579ebe54 100644
--- a/scripts/task-ai-management-browser-smoke.js
+++ b/scripts/task-ai-management-browser-smoke.js
@@ -97,8 +97,8 @@ async function main() {
const openHubLink = page.locator('a[href="/page-ai/openhub/admin"]');
assert(
- await openHubLink.count() >= 1,
- "OpenHub Admin 独立入口必须保留",
+ await openHubLink.count() === 0,
+ "OpenHub Admin 独立入口必须从当前 AI 管理页移除",
);
await page.getByRole("link", { name: "模型配置" }).click();
@@ -122,27 +122,37 @@ async function main() {
await page.getByRole("button", { name: "保存策略" }).click();
await page.locator("[data-ai-admin-tools-save-status]").filter({ hasText: /已保存/ }).waitFor();
- await page.getByRole("link", { name: "技能 / MCP" }).click();
+ await page.getByRole("link", { name: "Skills" }).click();
await page.locator("#ai-admin-skills.is-active").waitFor();
await page.getByRole("button", { name: "+ 添加 Skill" }).click();
const skillRow = page.locator(".mnote-ai-admin-skill-row").last();
await skillRow.locator('[name="skillName"]').fill(skillName);
await skillRow.locator('[name="skillDescription"]').fill("浏览器验收技能");
await skillRow.locator('[name="skillEnabled"]').check();
+ const skillsPanel = page.locator("#ai-admin-skills");
+ await skillsPanel.getByRole("button", { name: "保存 Skills" }).click();
+ await assertEventually(
+ async () => (await skillsPanel.locator("[data-ai-admin-skills-save-status]").textContent()) || "",
+ (text) => text.includes("已保存"),
+ "Skills 配置保存状态未变为已保存",
+ );
+ await page.getByRole("link", { name: "MCP" }).click();
+ await page.locator("#ai-admin-mcp.is-active").waitFor();
await page.getByRole("button", { name: "+ 添加 MCP" }).click();
const mcpRow = page.locator("[data-mcp-idx]").last();
+ await mcpRow.locator("summary").click();
await mcpRow.locator('[name="mcpName"]').fill(mcpName);
await mcpRow.locator('[name="mcpTransport"]').selectOption("stdio");
await mcpRow.locator('[name="mcpCommand"]').fill("scripts/lightrag-native-mcp.sh");
await mcpRow.locator('[name="mcpSecretRefs"]').fill("env://LIGHTRAG_API_KEY");
await mcpRow.locator('[name="mcpEnabled"]').check();
- const skillsPanel = page.locator("#ai-admin-skills");
- await skillsPanel.getByRole("button", { name: "保存配置" }).click();
+ const mcpPanel = page.locator("#ai-admin-mcp");
+ await mcpPanel.getByRole("button", { name: "保存 MCP" }).click();
await assertEventually(
- async () => (await skillsPanel.locator("[data-ai-admin-skills-save-status]").textContent()) || "",
+ async () => (await mcpPanel.locator("[data-ai-admin-mcp-save-status]").textContent()) || "",
(text) => text.includes("已保存"),
- "技能/MCP 配置保存状态未变为已保存",
+ "MCP 配置保存状态未变为已保存",
);
await page.screenshot({
path: path.join(OUTPUT_DIR, "skills-mcp.png"),
@@ -155,19 +165,55 @@ async function main() {
await targetUser.waitFor();
await targetUser.click();
await page.locator('[data-ai-admin-user-settings] h3', { hasText: "ai-user" }).waitFor();
- await page.locator('[data-user-model="omniroute/freefirst-fast"]').evaluate((input) => {
- input.checked = false;
- input.dispatchEvent(new Event("change", { bubbles: true }));
+ const disabledModelId = await page.locator("[data-user-model]").evaluateAll((inputs) => {
+ const checked = inputs
+ .filter((input) => input.checked)
+ .map((input) => input.getAttribute("data-user-model") || "")
+ .filter(Boolean);
+ return checked.length > 1 ? (checked.find((id) => id !== "omniroute/freefirst") || checked[1]) : "";
});
- await page.locator('[data-action="save-user-settings"]').click();
- await page.locator("[data-ai-admin-user-save-status]").filter({ hasText: /已保存/ }).waitFor();
+ if (disabledModelId) {
+ await page.locator(`[data-user-model="${disabledModelId}"]`).evaluate((input) => {
+ input.checked = false;
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ });
+ const saveModelOverride = await Promise.all([
+ page.waitForResponse((response) =>
+ response.url().includes("/api/ai-admin/users/ai-user/settings") &&
+ response.request().method() === "PUT"
+ ),
+ page.locator('[data-action="save-user-settings"]').click(),
+ ]).then(([response]) => response);
+ assert(
+ saveModelOverride.ok(),
+ `模型降权保存失败: ${saveModelOverride.status()} ${await saveModelOverride.text()}`,
+ );
+ }
await page.locator('[data-user-tab="skills"]').click();
- await page.locator(`[data-user-skill="${skillName}"]`).evaluate((input) => {
- input.checked = false;
- input.dispatchEvent(new Event("change", { bubbles: true }));
+ const disabledSkillId = await page.locator("[data-user-skill]").evaluateAll((inputs) => {
+ const checked = inputs
+ .filter((input) => input.checked)
+ .map((input) => input.getAttribute("data-user-skill") || "")
+ .filter(Boolean);
+ return checked[0] || "";
});
- await page.locator('[data-action="save-user-settings"]').click();
- await page.locator("[data-ai-admin-user-save-status]").filter({ hasText: /已保存/ }).waitFor();
+ if (disabledSkillId) {
+ await page.locator(`[data-user-skill="${disabledSkillId}"]`).evaluate((input) => {
+ input.checked = false;
+ input.dispatchEvent(new Event("change", { bubbles: true }));
+ });
+ const saveSkillOverride = await Promise.all([
+ page.waitForResponse((response) =>
+ response.url().includes("/api/ai-admin/users/ai-user/settings") &&
+ response.request().method() === "PUT"
+ ),
+ page.locator('[data-action="save-user-settings"]').click(),
+ ]).then(([response]) => response);
+ assert(
+ saveSkillOverride.ok(),
+ `Skill 降权保存失败: ${saveSkillOverride.status()} ${await saveSkillOverride.text()}`,
+ );
+ }
await page.screenshot({
path: path.join(OUTPUT_DIR, "users.png"),
fullPage: false,
@@ -178,17 +224,23 @@ async function main() {
const drawer = document.querySelector("[data-ai-admin-user-drawer]");
return !drawer || !drawer.classList.contains("is-open");
});
- await page.getByRole("link", { name: "技能 / MCP" }).click();
+ await page.getByRole("link", { name: "Skills" }).click();
await page.reload({ waitUntil: "commit" });
await page.locator("#ai-admin-skills.is-active").waitFor();
- assert.equal(
- await page.locator(`[name="skillName"][value="${skillName}"]`).count(),
- 1,
+ await assertEventually(
+ async () => page.locator('[name="skillName"]').evaluateAll((inputs) =>
+ inputs.map((input) => input.value || ""),
+ ),
+ (values) => values.includes(skillName),
"Skill 保存后刷新必须仍存在",
);
- assert.equal(
- await page.locator(`[name="mcpName"][value="${mcpName}"]`).count(),
- 1,
+ await page.getByRole("link", { name: "MCP" }).click();
+ await page.locator("#ai-admin-mcp.is-active").waitFor();
+ await assertEventually(
+ async () => page.locator('[name="mcpName"]').evaluateAll((inputs) =>
+ inputs.map((input) => input.value || ""),
+ ),
+ (values) => values.includes(mcpName),
"MCP 保存后刷新必须仍存在",
);
@@ -213,16 +265,20 @@ async function main() {
const response = await fetch("/api/ai-admin/users/ai-user/settings", { credentials: "include" });
return response.json();
});
- assert.equal(
- (userSettings.allowedModels || []).some((model) => model.id === "omniroute/freefirst-fast"),
- false,
- "ai-user 刷新后不应包含被禁用模型",
- );
- assert.equal(
- (userSettings.skills || []).find((skill) => skill.id === skillName)?.enabled,
- false,
- "ai-user 刷新后应保留 Skill 禁用覆盖",
- );
+ if (disabledModelId) {
+ assert.equal(
+ (userSettings.allowedModels || []).some((model) => model.id === disabledModelId),
+ false,
+ `ai-user 刷新后不应包含被禁用模型 ${disabledModelId}`,
+ );
+ }
+ if (disabledSkillId) {
+ assert.equal(
+ (userSettings.skills || []).find((skill) => skill.id === disabledSkillId)?.enabled,
+ false,
+ `ai-user 刷新后应保留 Skill 禁用覆盖 ${disabledSkillId}`,
+ );
+ }
const userContext = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const userResponse = await userContext.request.post(`${BASE}/api/auth`, {
@@ -244,16 +300,20 @@ async function main() {
const userEffectiveResponse = await userContext.request.get(`${BASE}/api/ai-settings/effective`);
assert(userEffectiveResponse.ok(), `ai-user effective 读取失败: ${userEffectiveResponse.status()}`);
const userEffective = await userEffectiveResponse.json();
- assert.equal(
- (userEffective.models || []).some((model) => model.id === "omniroute/freefirst-fast"),
- false,
- "ai-user effective 不应包含被管理员取消的模型",
- );
- assert.equal(
- (userEffective.skills || []).some((skill) => skill.name === skillName),
- false,
- "ai-user effective 不应包含被禁用 Skill",
- );
+ if (disabledModelId) {
+ assert.equal(
+ (userEffective.models || []).some((model) => model.id === disabledModelId),
+ false,
+ `ai-user effective 不应包含被管理员取消的模型 ${disabledModelId}`,
+ );
+ }
+ if (disabledSkillId) {
+ assert.equal(
+ (userEffective.skills || []).some((skill) => skill.id === disabledSkillId || skill.name === disabledSkillId),
+ false,
+ `ai-user effective 不应包含被禁用 Skill ${disabledSkillId}`,
+ );
+ }
await userContext.close();
console.log(JSON.stringify({
diff --git a/scripts/task-ai-management-control-plane-static-smoke.js b/scripts/task-ai-management-control-plane-static-smoke.js
index ec6bae37..3eb4fa66 100644
--- a/scripts/task-ai-management-control-plane-static-smoke.js
+++ b/scripts/task-ai-management-control-plane-static-smoke.js
@@ -301,27 +301,16 @@ check(
);
// --------------------------------------------------------------------------
-// Section 6: Pi Rust 主入口与 OpenHub 兼容边界
+// Section 6: Pi Rust 主入口与退役边界
// --------------------------------------------------------------------------
-console.log("\n-- 6. Pi Rust 主入口与 OpenHub 兼容边界 --");
+console.log("\n-- 6. Pi Rust 主入口与退役边界 --");
check(
- "OpenHub agent route /page-ai/openhub/ai exists",
- routesMod.includes('/page-ai/openhub/ai", get(page_ai_openhub::ai_shell)'),
- "page_ai_openhub::ai_shell"
-);
-
-check(
- "OpenHub admin routes still exist",
- routesMod.includes('/page-ai/openhub/admin"') &&
- routesMod.includes("page_ai_openhub::non_ai_route_guard"),
- "non_ai_route_guard for admin"
-);
-
-check(
- "OpenHub status API still exists",
- routesMod.includes('/api/page-ai/openhub/status", get(page_ai_openhub::status)'),
- "GET page_ai_openhub::status"
+ "OpenHub active routes are retired from mnote-web",
+ !routesMod.includes("page_ai_openhub") &&
+ !routesMod.includes("/page-ai/openhub") &&
+ !routesMod.includes("/api/page-ai/openhub"),
+ "OpenHub route module and public routes removed from active router"
);
check(
@@ -380,11 +369,11 @@ check(
);
check(
- "OpenHub is described as compatibility/admin boundary, not default chat",
- aiAdminRs.includes("迁移期兼容 / admin 边界") &&
- aiAdminRs.includes("不再作为默认 Page AI chat") &&
+ "AI management no longer exposes OpenHub admin entry",
+ !aiAdminRs.includes("/page-ai/openhub") &&
+ !aiAdminRs.includes("打开 OpenHub Admin") &&
!aiAdminRs.includes("OpenHub 仍是默认 Page AI"),
- "OpenHub remains only as migration compatibility boundary"
+ "OpenHub admin links removed from active AI management UI"
);
// --------------------------------------------------------------------------
diff --git a/scripts/task-dev-hot-plan-test.js b/scripts/task-dev-hot-plan-test.js
index 18c99c94..822f041f 100644
--- a/scripts/task-dev-hot-plan-test.js
+++ b/scripts/task-dev-hot-plan-test.js
@@ -18,16 +18,12 @@ assert.match(env.MNOTE_WEB_CMD, /crates\/mnote-web\/browser/);
assert.equal(env.FRONTEND_PORT, "3200");
assert.equal(env.MNOTE_WEB_BIND, "0.0.0.0:3200");
assert.equal(env.MNOTE_KNOWLEDGE_PROVIDER, "lightrag_legacy");
-assert.equal(env.ENABLE_OPENHUB, "1");
-assert.equal(env.CHECK_OPENHUB_HEALTH, "1");
assert.equal(env.MNOTE_OPENCODE_BASE_URL, "http://127.0.0.1:4096");
-assert.equal(env.OPENHUB_OPENCODE_BASE_URL, "http://127.0.0.1:4096");
-assert.equal(env.OPENHUB_BACKEND_DIR, "/mnt/Data1T/Mnote_data/openhub/OpenHub/smart-query-backend");
-assert.equal(env.OPENHUB_OPENCODE_XDG_ROOT, "/mnt/Data1T/Mnote_data/openhub/opencode-runtime");
-assert.equal(env.OPENHUB_OPENCODE_HOME, "/mnt/Data1T/Mnote_data/openhub/opencode-home");
-assert.equal(env.MNOTE_OPENHUB_BASE_URL, "http://127.0.0.1:18080");
-assert.equal(env.OPENHUB_HEALTH_URL, "http://127.0.0.1:18080/api/health");
-assert.equal(env.MNOTE_OPENHUB_DISABLE_GIT_SNAPSHOT_RESTORE, "1");
+assert.equal(env.ENABLE_OPENHUB, undefined);
+assert.equal(env.CHECK_OPENHUB_HEALTH, undefined);
+assert.equal(env.OPENHUB_OPENCODE_BASE_URL, undefined);
+assert.equal(env.OPENHUB_BACKEND_DIR, undefined);
+assert.equal(env.MNOTE_OPENHUB_BASE_URL, undefined);
assert.equal(env.MNOTE_CONTROL_PLANE_BACKEND, "libsql-local");
assert.equal(env.MNOTE_TURSO_LOCAL_PATH, "/mnt/Data1T/Mnote_data/control-plane/control-plane-dev-hot-libsql.db");
assert.equal(env.MNOTE_CONTROL_PLANE_DB_PATH, undefined);
@@ -44,11 +40,6 @@ const loopbackEnv = buildDevHotEnv({
assert.equal(loopbackEnv.MNOTE_WEB_BIND, "0.0.0.0:3300");
assert.equal(loopbackEnv.MNOTE_WEB_CMD, "custom");
-const skipEnv = buildDevHotEnv({
- SKIP_OPENHUB: "1",
-});
-assert.equal(skipEnv.ENABLE_OPENHUB, "");
-
const fixturesDisabledEnv = buildDevHotEnv({
MNOTE_WEB_ALLOW_DEV_FIXTURES: "0",
});
diff --git a/scripts/task-pi-lab-api-endpoint-smoke.js b/scripts/task-pi-lab-api-endpoint-smoke.js
index 5dd0302d..4b33afd3 100644
--- a/scripts/task-pi-lab-api-endpoint-smoke.js
+++ b/scripts/task-pi-lab-api-endpoint-smoke.js
@@ -199,8 +199,8 @@ async function main() {
const text = await res.text();
if (!text.includes('createSidebarPageAiPiLabRuntime')) return { passed: false, reason: 'missing expected export' };
if (!text.includes('data-page-ai-pi-lab-drawer')) return { passed: false, reason: 'missing independent drawer marker' };
- if (text.includes('attachPanelToDrawer') || text.includes('setOpenHubVisible')) return { passed: false, reason: 'runtime still references OpenHub drawer integration' };
- if (text.includes('data-page-ai-pi-lab-openhub-tab')) return { passed: false, reason: 'runtime still exposes OpenHub tab inside Pi Lab' };
+ if (text.includes('attachPanelToDrawer') || text.includes('setOpenHubVisible')) return { passed: false, reason: 'runtime still references retired host drawer integration' };
+ if (text.includes('data-page-ai-pi-lab-openhub-tab')) return { passed: false, reason: 'runtime still exposes retired host tab inside Pi Lab' };
if (/setInterval\s*\(/.test(text)) return { passed: false, reason: 'runtime still has setInterval call' };
if (!text.includes('NO setInterval polling')) return { passed: false, reason: 'missing NO setInterval polling comment' };
if (!text.includes('EventSource')) return { passed: false, reason: 'missing EventSource for SSE' };
diff --git a/scripts/task-pi-lab-browser-smoke.js b/scripts/task-pi-lab-browser-smoke.js
index 82728c9f..88ed3668 100644
--- a/scripts/task-pi-lab-browser-smoke.js
+++ b/scripts/task-pi-lab-browser-smoke.js
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Pi Lab browser smoke
-// 验证 Pi Lab 默认独立悬浮入口 + MNote-native drawer,不复用 OpenHub drawer/provider tab/iframe。
+// 验证 Pi Lab 默认独立悬浮入口 + MNote-native drawer,不复用已退役 host drawer/provider tab/iframe。
// 需要 mnote-web 已在运行;MNOTE_PAGE_AI_PI_LAB 默认开启,设为 0 时才强制关闭。
"use strict";
@@ -90,16 +90,16 @@ async function main() {
console.log(" 3. Pi Lab independent drawer visible");
const drawerEvidence = await page.evaluate(() => {
- const openHubDrawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
+ const legacyDrawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
const piDrawer = document.querySelector('[data-page-ai-pi-lab="drawer"]');
const piPanel = document.querySelector('[data-page-ai-pi-lab="panel"]');
const diagnostics = document.querySelector("[data-page-ai-pi-lab-diagnostics]");
return {
piDrawerVisible: Boolean(piDrawer && getComputedStyle(piDrawer).display !== "none"),
panelInPiDrawer: Boolean(piDrawer && piPanel && piDrawer.contains(piPanel)),
- panelInOpenHubDrawer: Boolean(openHubDrawer && piPanel && openHubDrawer.contains(piPanel)),
+ panelInLegacyDrawer: Boolean(legacyDrawer && piPanel && legacyDrawer.contains(piPanel)),
piDrawerHasIframe: Boolean(piDrawer && piDrawer.querySelector("iframe")),
- openHubTabInPi: Boolean(piDrawer && piDrawer.querySelector("[data-page-ai-pi-lab-openhub-tab]")),
+ retiredHostTabInPi: Boolean(piDrawer && piDrawer.querySelector("[data-page-ai-pi-lab-openhub-tab]")),
contextChips: document.querySelectorAll("[data-page-ai-pi-lab-context-strip] [data-page-ai-pi-lab-context]").length,
hasCurrentPageContext: Boolean(document.querySelector("[data-page-ai-pi-lab-current-page]")),
hasChangedFilesContext: Boolean(document.querySelector("[data-page-ai-pi-lab-changed-files]")),
@@ -112,9 +112,9 @@ async function main() {
assert(drawerEvidence.piDrawerVisible, "Pi Lab independent drawer should be visible");
assert(drawerEvidence.panelInPiDrawer, "Pi Lab panel should be mounted inside independent Pi drawer");
- assert.equal(drawerEvidence.panelInOpenHubDrawer, false, "Pi Lab panel must not be inside OpenHub drawer");
+ assert.equal(drawerEvidence.panelInLegacyDrawer, false, "Pi Lab panel must not be inside retired host drawer");
assert.equal(drawerEvidence.piDrawerHasIframe, false, "Pi Lab drawer must not iframe a second app");
- assert.equal(drawerEvidence.openHubTabInPi, false, "Pi Lab drawer must not expose OpenHub provider tab");
+ assert.equal(drawerEvidence.retiredHostTabInPi, false, "Pi Lab drawer must not expose retired host provider tab");
assert(drawerEvidence.contextChips >= 3, `expected context strip chips, got ${drawerEvidence.contextChips}`);
assert(drawerEvidence.hasCurrentPageContext, "context strip should retain current page binding");
assert(drawerEvidence.hasChangedFilesContext, "context strip should retain changed files count");
@@ -298,30 +298,6 @@ async function main() {
assert.notEqual(runtimeEvidence.changedFiles, "0", "changed files chip should be non-zero");
console.log(" 6. Real Pi Rust send, abort, deny, citation, receipt and diff evidence visible");
- const openHubEvidence = await page.evaluate(async () => {
- const api = window.__mnoteSidebarPageAiRuntime;
- if (api && typeof api.openPageAiDrawer === "function") {
- api.openPageAiDrawer();
- }
- await new Promise((resolve) => setTimeout(resolve, 400));
- const openHubDrawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
- const openHubFrame = document.querySelector("[data-page-ai-openhub-iframe]");
- const piDrawer = document.querySelector('[data-page-ai-pi-lab="drawer"]');
- return {
- openHubApiExists: Boolean(api && typeof api.openPageAiDrawer === "function"),
- openHubDrawerExists: Boolean(openHubDrawer),
- openHubHost: openHubDrawer?.getAttribute("data-page-ai-openhub-host") || "",
- openHubFrameExists: Boolean(openHubFrame),
- piStillIndependent: Boolean(piDrawer && openHubDrawer && !openHubDrawer.contains(piDrawer)),
- };
- });
- assert(openHubEvidence.openHubApiExists, "OpenHub drawer API should still exist");
- assert(openHubEvidence.openHubDrawerExists, "OpenHub drawer should still open independently");
- assert.equal(openHubEvidence.openHubHost, "true", "OpenHub drawer should still be the default host");
- assert(openHubEvidence.openHubFrameExists, "OpenHub iframe should still exist outside Pi Lab");
- assert(openHubEvidence.piStillIndependent, "Pi Lab drawer should remain outside OpenHub drawer");
- console.log(" 7. OpenHub default drawer still works independently");
-
fs.mkdirSync(path.dirname(SCREENSHOT), { recursive: true });
await page.locator('[data-page-ai-pi-lab="drawer"]').waitFor({ state: "visible", timeout: UI_TIMEOUT_MS });
await page.screenshot({ path: SCREENSHOT, fullPage: false });
diff --git a/scripts/task-pi-lab-rpc-browser-smoke.js b/scripts/task-pi-lab-rpc-browser-smoke.js
index 67a62ddd..a116f0b0 100644
--- a/scripts/task-pi-lab-rpc-browser-smoke.js
+++ b/scripts/task-pi-lab-rpc-browser-smoke.js
@@ -350,18 +350,18 @@ async function main() {
console.log(" ✅ abort reflected in UI state");
const domEvidence = await page.evaluate(() => {
- const openHubDrawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
+ const retiredHostDrawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
const piDrawer = document.querySelector('[data-page-ai-pi-lab="drawer"]');
return {
piDrawerVisible: Boolean(piDrawer && getComputedStyle(piDrawer).display !== "none"),
piDrawerHasIframe: Boolean(piDrawer && piDrawer.querySelector("iframe")),
- piInsideOpenHub: Boolean(openHubDrawer && piDrawer && openHubDrawer.contains(piDrawer)),
+ piInsideRetiredHost: Boolean(retiredHostDrawer && piDrawer && retiredHostDrawer.contains(piDrawer)),
model: document.querySelector("[data-page-ai-pi-lab-model-chip]")?.textContent || "",
};
});
assert(domEvidence.piDrawerVisible, "Pi drawer should remain visible");
assert.equal(domEvidence.piDrawerHasIframe, false, "Pi drawer must not iframe a second app");
- assert.equal(domEvidence.piInsideOpenHub, false, "Pi drawer must not be inside OpenHub drawer");
+ assert.equal(domEvidence.piInsideRetiredHost, false, "Pi drawer must not be inside retired host drawer");
assert(domEvidence.model.includes("omniroute/gpt-5.4-mini"), `model chip mismatch: ${domEvidence.model}`);
console.log(" ✅ Pi Lab remains native, independent and non-iframe");
diff --git a/scripts/task-pi-lab-static-smoke.js b/scripts/task-pi-lab-static-smoke.js
index 58482715..7af3a9ff 100644
--- a/scripts/task-pi-lab-static-smoke.js
+++ b/scripts/task-pi-lab-static-smoke.js
@@ -115,15 +115,15 @@ const checks = [
['runtime checks enabled flag via status API but does not hide launcher behind it', runtime.includes('enabled') && runtime.includes('checkStatus')],
['runtime renders independent Pi Lab drawer', runtime.includes('data-page-ai-pi-lab-drawer') && runtime.includes('data-page-ai-pi-lab') && runtime.includes('drawer')],
['runtime creates drawer through ensureDrawer', runtime.includes('function ensureDrawer') && runtime.includes('setDrawerVisible')],
- ['runtime does NOT mount inside OpenHub drawer', !runtime.includes('attachPanelToDrawer') && !runtime.includes('wolai-page-ai-drawer')],
- ['runtime does NOT toggle OpenHub iframe visibility', !runtime.includes('setOpenHubVisible') && !runtime.includes('data-page-ai-openhub-frame-wrap')],
- ['runtime has no OpenHub provider tab inside Pi Lab', !runtime.includes('data-page-ai-pi-lab-openhub-tab')],
+ ['runtime does NOT mount inside retired host drawer', !runtime.includes('attachPanelToDrawer') && !runtime.includes('wolai-page-ai-drawer')],
+ ['runtime does NOT toggle retired host iframe visibility', !runtime.includes('setOpenHubVisible') && !runtime.includes('data-page-ai-openhub-frame-wrap')],
+ ['runtime has no retired host provider tab inside Pi Lab', !runtime.includes('data-page-ai-pi-lab-openhub-tab')],
['runtime has context strip chips', runtime.includes('data-page-ai-pi-lab-context-strip') && runtime.includes('data-page-ai-pi-lab-current-page') && runtime.includes('data-page-ai-pi-lab-allowed-roots') && runtime.includes('data-page-ai-pi-lab-lightrag')],
- ['runtime collapses secondary context/settings like OpenHub chrome', runtime.includes('