diff --git a/bugs/07-ai/done/7-66-page-ai-pi-ts-openhub-soft-retirement-checklist-v1.md b/bugs/07-ai/done/7-66-page-ai-pi-ts-openhub-soft-retirement-checklist-v1.md new file mode 100644 index 00000000..8312691e --- /dev/null +++ b/bugs/07-ai/done/7-66-page-ai-pi-ts-openhub-soft-retirement-checklist-v1.md @@ -0,0 +1,34 @@ +# 7-66 Page AI Pi TS / OpenHub 软删除收口 Checklist v1 + +状态:done +Owner:07-ai / mnote-web +日期:2026-07-12 + +## 1. 背景 + +Pi TS 与 OpenHub 已不再作为当前 Page AI 主链。当前主线收口为 MNote 托管的 Pi Rust Page AI,OpenHub 与 Pi TS 仅作为历史对照保留到 `recycle/20260712-pi-ts-openhub-retirement/`。 + +## 2. Checklist + +- [x] 移除 `page_ai_openhub` active route,并把原 route 源码软归档到 recycle。 +- [x] 移除 `/page-ai/openhub/*` 与 `/api/page-ai/openhub/*` active router 入口。 +- [x] 移除 Pi TS runtime fallback、`MNOTE_PAGE_AI_PI_TS_BIN` 与 `PI_LAB_RUNTIME_IMPL_TS`。 +- [x] AI 管理页不再展示 OpenHub Admin 入口或兼容卡片。 +- [x] `dev:hot` / `desktop:hot` 不再默认启动或 health check OpenHub。 +- [x] OpenHub 专项 smoke 与融合设计稿软归档到 recycle。 +- [x] Pi Lab 静态 / 浏览器 smoke 改为验证 Pi Rust 独立 drawer 与退役 host 隔离。 +- [x] 跑完静态、Rust、浏览器与 CodeGraph 验证后移入 `done/`。 + +## 3. 验收命令 + +```bash +node scripts/task-pi-lab-static-smoke.js +node scripts/task-ai-management-control-plane-static-smoke.js +node scripts/task-dev-hot-plan-test.js +node --test scripts/desktop-hot.test.js +cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_pi -- --nocapture +cargo test --manifest-path rust/Cargo.toml -p mnote-web ai_admin -- --nocapture +git diff --check +codegraph sync . +codegraph status . +``` diff --git a/design/README.md b/design/README.md index 1df23a62..171a7217 100644 --- a/design/README.md +++ b/design/README.md @@ -2,7 +2,7 @@ > 更新时间:2026-06-28 > -> 当前产品口径:`VSCode 简化版工作区 + tiptap 的 Markdown 前端编辑器 + OpenHub/opencode agent + WeKnora 知识库融合 + simplemindmap/office 插件 + Wolai 主题 Web 壳 + 鉴权控制面`。 +> 当前产品口径:`VSCode 简化版工作区 + tiptap 的 Markdown 前端编辑器 + Pi Rust Page AI + opencode/native agent 边界 + LightRAG 知识库融合 + simplemindmap/office 插件 + Wolai 主题 Web 壳 + 鉴权控制面`。 > > 当前上位主线:local-first MVP 已初步完成;local-first workspace 是默认数据形态,本地 `.md` 是页面正文真相,Rust kernel / projection / command 持有语义,Rust SQLite control-plane 持有默认 auth、membership、share grants、sync state、AI policy、Page AI runtime session 控制面;Convex / 服务端只作为历史迁移源、显式 cloud source、compat 和 sync replica。入口设计见: > `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md` diff --git a/design/07-ai/process/7-67-openhub-page-ai-fusion-v1.md b/recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-67-openhub-page-ai-fusion-v1.md similarity index 100% rename from design/07-ai/process/7-67-openhub-page-ai-fusion-v1.md rename to recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-67-openhub-page-ai-fusion-v1.md diff --git a/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-checklist-v1.md b/recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-checklist-v1.md similarity index 100% rename from design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-checklist-v1.md rename to recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-checklist-v1.md diff --git a/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-v1.md b/recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-v1.md similarity index 100% rename from design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-v1.md rename to recycle/20260712-pi-ts-openhub-retirement/design/07-ai/process/7-68-openhub-weknora-mnote-deep-fusion-v1.md diff --git a/rust/crates/mnote-web/src/routes/page_ai_openhub.rs b/recycle/20260712-pi-ts-openhub-retirement/rust/crates/mnote-web/src/routes/page_ai_openhub.rs similarity index 100% rename from rust/crates/mnote-web/src/routes/page_ai_openhub.rs rename to recycle/20260712-pi-ts-openhub-retirement/rust/crates/mnote-web/src/routes/page_ai_openhub.rs diff --git a/scripts/task-page-ai-filechange-current-document-refresh-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task-page-ai-filechange-current-document-refresh-smoke.js similarity index 100% rename from scripts/task-page-ai-filechange-current-document-refresh-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task-page-ai-filechange-current-document-refresh-smoke.js diff --git a/scripts/task768-page-ai-openhub-host-static-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task768-page-ai-openhub-host-static-smoke.js similarity index 100% rename from scripts/task768-page-ai-openhub-host-static-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task768-page-ai-openhub-host-static-smoke.js diff --git a/scripts/task770-openhub-git-snapshot-restore-guard-static-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task770-openhub-git-snapshot-restore-guard-static-smoke.js similarity index 100% rename from scripts/task770-openhub-git-snapshot-restore-guard-static-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task770-openhub-git-snapshot-restore-guard-static-smoke.js diff --git a/scripts/task771-openhub-mnote-scope-static-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task771-openhub-mnote-scope-static-smoke.js similarity index 100% rename from scripts/task771-openhub-mnote-scope-static-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task771-openhub-mnote-scope-static-smoke.js diff --git a/scripts/task773-page-ai-openhub-browser-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task773-page-ai-openhub-browser-smoke.js similarity index 100% rename from scripts/task773-page-ai-openhub-browser-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task773-page-ai-openhub-browser-smoke.js diff --git a/scripts/task774-openhub-mnote-send-and-file-edit-e2e.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task774-openhub-mnote-send-and-file-edit-e2e.js similarity index 100% rename from scripts/task774-openhub-mnote-send-and-file-edit-e2e.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task774-openhub-mnote-send-and-file-edit-e2e.js diff --git a/scripts/task775-openhub-mnote-scope-isolation-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task775-openhub-mnote-scope-isolation-smoke.js similarity index 100% rename from scripts/task775-openhub-mnote-scope-isolation-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task775-openhub-mnote-scope-isolation-smoke.js diff --git a/scripts/task776-openhub-changed-files-bridge-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task776-openhub-changed-files-bridge-smoke.js similarity index 100% rename from scripts/task776-openhub-changed-files-bridge-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task776-openhub-changed-files-bridge-smoke.js diff --git a/scripts/task778-openhub-artifact-index-static-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task778-openhub-artifact-index-static-smoke.js similarity index 100% rename from scripts/task778-openhub-artifact-index-static-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task778-openhub-artifact-index-static-smoke.js diff --git a/scripts/task779-openhub-file-edit-document-pane-refresh-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task779-openhub-file-edit-document-pane-refresh-smoke.js similarity index 100% rename from scripts/task779-openhub-file-edit-document-pane-refresh-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task779-openhub-file-edit-document-pane-refresh-smoke.js diff --git a/scripts/task780-openhub-artifact-index-runtime-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task780-openhub-artifact-index-runtime-smoke.js similarity index 100% rename from scripts/task780-openhub-artifact-index-runtime-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task780-openhub-artifact-index-runtime-smoke.js diff --git a/scripts/task782-openhub-weknora-tool-citation-bridge-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task782-openhub-weknora-tool-citation-bridge-smoke.js similarity index 100% rename from scripts/task782-openhub-weknora-tool-citation-bridge-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task782-openhub-weknora-tool-citation-bridge-smoke.js diff --git a/scripts/task786-openhub-history-refresh-browser-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task786-openhub-history-refresh-browser-smoke.js similarity index 100% rename from scripts/task786-openhub-history-refresh-browser-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task786-openhub-history-refresh-browser-smoke.js diff --git a/scripts/task791-openhub-native-mnote-context-static-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task791-openhub-native-mnote-context-static-smoke.js similarity index 100% rename from scripts/task791-openhub-native-mnote-context-static-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task791-openhub-native-mnote-context-static-smoke.js diff --git a/scripts/task792-openhub-native-mnote-context-source-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task792-openhub-native-mnote-context-source-smoke.js similarity index 100% rename from scripts/task792-openhub-native-mnote-context-source-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task792-openhub-native-mnote-context-source-smoke.js diff --git a/scripts/task793-openhub-mnote-context-fastapi-chain-smoke.js b/recycle/20260712-pi-ts-openhub-retirement/scripts/task793-openhub-mnote-context-fastapi-chain-smoke.js similarity index 100% rename from scripts/task793-openhub-mnote-context-fastapi-chain-smoke.js rename to recycle/20260712-pi-ts-openhub-retirement/scripts/task793-openhub-mnote-context-fastapi-chain-smoke.js diff --git a/rust/crates/control-plane/src/sqlite.rs b/rust/crates/control-plane/src/sqlite.rs index f9b93b2d..1ed441b6 100644 --- a/rust/crates/control-plane/src/sqlite.rs +++ b/rust/crates/control-plane/src/sqlite.rs @@ -2160,18 +2160,31 @@ impl ControlPlaneStore for SqliteControlPlaneStore { let conn = self.lock_conn()?; let now = now_text(); - let existing = conn - .query_row( + let existing = if let Some(workspace_id) = input.workspace_id.as_deref() { + conn.query_row( "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision FROM ai_policies - WHERE (?1 IS NULL OR user_id = ?1) - AND (?2 IS NULL OR workspace_id = ?2) + WHERE workspace_id = ?1 ORDER BY created_at DESC LIMIT 1", - params![input.user_id, input.workspace_id], + params![workspace_id], row_to_ai_policy, ) - .optional()?; + .optional()? + } else if let Some(user_id) = input.user_id.as_deref() { + conn.query_row( + "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision + FROM ai_policies + WHERE user_id = ?1 AND workspace_id IS NULL + ORDER BY created_at DESC + LIMIT 1", + params![user_id], + row_to_ai_policy, + ) + .optional()? + } else { + None + }; if let Some(existing) = existing { let revision = existing.revision + 1; @@ -4067,6 +4080,52 @@ mod tests { assert_eq!(user_only.id, user_policy.id); } + #[test] + fn ai_policy_user_upsert_does_not_overwrite_workspace_policy() { + let store = store(); + create_user(&store, "ai_user"); + let workspace = store + .ensure_default_workspace("ai_user") + .expect("default workspace"); + + let workspace_policy = store + .upsert_ai_policy(UpsertAiPolicyInput { + id: None, + user_id: Some("ai_user".to_string()), + workspace_id: Some(workspace.id.clone()), + allowed_roots_json: "[\"file:///workspace-root\"]".to_string(), + model_policy_json: "{\"default\":\"workspace\"}".to_string(), + quota_json: "{}".to_string(), + }) + .expect("workspace policy"); + + let user_policy = store + .upsert_ai_policy(UpsertAiPolicyInput { + id: None, + user_id: Some("ai_user".to_string()), + workspace_id: None, + allowed_roots_json: "[\"file:///user-root\"]".to_string(), + model_policy_json: "{\"default\":\"user\"}".to_string(), + quota_json: "{}".to_string(), + }) + .expect("user policy"); + assert_ne!(user_policy.id, workspace_policy.id); + + let user_only = store + .get_ai_policy("ai_user", None) + .expect("get user policy") + .expect("user policy exists"); + assert_eq!(user_only.id, user_policy.id); + assert!(user_only.allowed_roots_json.contains("user-root")); + + let workspace_only = store + .get_ai_policy("ai_user", Some(&workspace.id)) + .expect("get workspace policy") + .expect("workspace policy exists"); + assert_eq!(workspace_only.id, workspace_policy.id); + assert!(workspace_only.allowed_roots_json.contains("workspace-root")); + } + #[test] fn sync_state_upsert_tracks_cursor_status_and_revision() { let store = store(); diff --git a/rust/crates/control-plane/src/turso.rs b/rust/crates/control-plane/src/turso.rs index cb416754..6a717948 100644 --- a/rust/crates/control-plane/src/turso.rs +++ b/rust/crates/control-plane/src/turso.rs @@ -2684,18 +2684,31 @@ impl ControlPlaneStore for TursoControlPlaneStore { let conn = self.lock_conn()?; let now = now_text(); - let existing = conn - .query_row( + let existing = if let Some(workspace_id) = input.workspace_id.as_deref() { + conn.query_row( "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision FROM ai_policies - WHERE (?1 IS NULL OR user_id = ?1) - AND (?2 IS NULL OR workspace_id = ?2) + WHERE workspace_id = ?1 ORDER BY created_at DESC LIMIT 1", - params![input.user_id, input.workspace_id], + params![workspace_id], row_to_ai_policy, ) - .optional()?; + .optional()? + } else if let Some(user_id) = input.user_id.as_deref() { + conn.query_row( + "SELECT id, user_id, workspace_id, allowed_roots_json, model_policy_json, quota_json, created_at, updated_at, revision + FROM ai_policies + WHERE user_id = ?1 AND workspace_id IS NULL + ORDER BY created_at DESC + LIMIT 1", + params![user_id], + row_to_ai_policy, + ) + .optional()? + } else { + None + }; if let Some(existing) = existing { let revision = existing.revision + 1; @@ -4593,6 +4606,52 @@ mod tests { assert_eq!(user_only.id, user_policy.id); } + #[test] + fn ai_policy_user_upsert_does_not_overwrite_workspace_policy() { + let store = store(); + create_user(&store, "ai_user"); + let workspace = store + .ensure_default_workspace("ai_user") + .expect("default workspace"); + + let workspace_policy = store + .upsert_ai_policy(UpsertAiPolicyInput { + id: None, + user_id: Some("ai_user".to_string()), + workspace_id: Some(workspace.id.clone()), + allowed_roots_json: "[\"file:///workspace-root\"]".to_string(), + model_policy_json: "{\"default\":\"workspace\"}".to_string(), + quota_json: "{}".to_string(), + }) + .expect("workspace policy"); + + let user_policy = store + .upsert_ai_policy(UpsertAiPolicyInput { + id: None, + user_id: Some("ai_user".to_string()), + workspace_id: None, + allowed_roots_json: "[\"file:///user-root\"]".to_string(), + model_policy_json: "{\"default\":\"user\"}".to_string(), + quota_json: "{}".to_string(), + }) + .expect("user policy"); + assert_ne!(user_policy.id, workspace_policy.id); + + let user_only = store + .get_ai_policy("ai_user", None) + .expect("get user policy") + .expect("user policy exists"); + assert_eq!(user_only.id, user_policy.id); + assert!(user_only.allowed_roots_json.contains("user-root")); + + let workspace_only = store + .get_ai_policy("ai_user", Some(&workspace.id)) + .expect("get workspace policy") + .expect("workspace policy exists"); + assert_eq!(workspace_only.id, workspace_policy.id); + assert!(workspace_only.allowed_roots_json.contains("workspace-root")); + } + #[test] fn sync_state_upsert_tracks_cursor_status_and_revision() { let store = store(); 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 948bd343..750320d8 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 @@ -1,7 +1,7 @@ // == Pi Lab Page AI Runtime == // MNote-native adapter for Pi-first Page AI Lab. -// Renders the MNote-owned Pi Rust Page AI surface. OpenHub is kept as a -// compatibility/admin boundary during migration, not as the default chat owner. +// Renders the MNote-owned Pi Rust Page AI surface. Retired hosts stay in +// recycle; this runtime must stay independent. // NO setInterval polling. // // UI note: @earendil-works/pi-web-ui@0.75.3 was checked as the mature upstream UI. 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 bc92ee31..50c96b69 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js @@ -199,12 +199,6 @@ export function createSidebarPageAiRuntime(context) { pageAiOpencodeEventSource: null, pageAiOpencodeEventSessionId: '', pageAiOpencodeEventTimer: 0, - pageAiOpenHubStatus: null, - pageAiOpenHubStatusError: '', - pageAiOpenHubBootstrap: null, - pageAiOpenHubIframeUrl: '', - pageAiOpenHubScopeSummary: '', - pageAiOpenHubFallbackReason: '', pageAiSessionSearchQuery: '', pageAiSessionSearchResults: [], pageAiSessionSearchTimer: 0, @@ -892,10 +886,6 @@ export function createSidebarPageAiRuntime(context) { } } - function pageAiOpenHubHostEnabled() { - return true; - } - function pageAiOpencodeStatusLabel(status) { var node = status && typeof status === 'object' ? status : {}; var state = String(node.status || node.state || node.runtimeStatus || '').trim(); @@ -1151,116 +1141,6 @@ export function createSidebarPageAiRuntime(context) { return lines.join('\n'); } - function pageAiOpenHubScopeSummary(scope) { - var node = scope && typeof scope === 'object' ? scope : {}; - var workspace = node.workspaceScope && typeof node.workspaceScope === 'object' ? node.workspaceScope : {}; - var items = []; - if (node.openhubUserKey || node.openhub_user_key) items.push('openhub_user_key=' + String(node.openhubUserKey || node.openhub_user_key)); - if (node.openhubWorkspaceKey || node.workspace_key) items.push('workspace_key=' + String(node.openhubWorkspaceKey || node.workspace_key)); - if (workspace.workspaceId) items.push('workspace=' + String(workspace.workspaceId)); - if (workspace.rootUri) items.push('rootUri=' + String(workspace.rootUri)); - if (node.openhubSessionScope || node.session_scope) items.push('session_scope=' + String(node.openhubSessionScope || node.session_scope)); - if (node.skillScope || node.skill_scope) items.push('skill_scope=' + String(node.skillScope || node.skill_scope)); - if (node.mcpScope) items.push('mcp_scope=' + String(node.mcpScope)); - if (node.toolPermissionScope || node.tool_permission_scope) items.push('tool_permission_scope=' + String(node.toolPermissionScope || node.tool_permission_scope)); - if (node.weknoraToolScope || node.weknora_tool_scope) items.push('weknora_tool_scope=' + String(node.weknoraToolScope || node.weknora_tool_scope)); - return items.join('\n'); - } - - function pageAiOpenHubStatusText(status) { - var node = status && typeof status === 'object' ? status : {}; - if (node.ok === true || String(node.status || '').trim() === 'ready') return '已连接 OpenHub'; - if (String(node.status || '').trim() === 'degraded') return 'OpenHub 连接受限'; - return '正在连接 OpenHub'; - } - - function pageAiRenderOpenHubHostChrome() { - var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]'); - if (!(drawer instanceof HTMLElement) || drawer.getAttribute('data-page-ai-openhub-host') !== 'true') return; - var bootstrap = pageUiState.pageAiOpenHubBootstrap && typeof pageUiState.pageAiOpenHubBootstrap === 'object' ? pageUiState.pageAiOpenHubBootstrap : {}; - var scope = bootstrap.scope && typeof bootstrap.scope === 'object' ? bootstrap.scope : {}; - var status = pageUiState.pageAiOpenHubStatus && typeof pageUiState.pageAiOpenHubStatus === 'object' ? pageUiState.pageAiOpenHubStatus : {}; - var statusNode = drawer.querySelector('[data-page-ai-openhub-runtime-status]'); - if (statusNode instanceof HTMLElement) { - statusNode.textContent = pageUiState.pageAiOpenHubStatusError || pageAiOpenHubStatusText(status); - statusNode.setAttribute('data-state', pageUiState.pageAiOpenHubStatusError ? 'error' : (status.ok ? 'ok' : 'degraded')); - } - var userNode = drawer.querySelector('[data-page-ai-openhub-user-key]'); - if (userNode instanceof HTMLElement) userNode.textContent = String(scope.openhubUserKey || scope.openhub_user_key || '等待 bootstrap'); - var authNode = drawer.querySelector('[data-page-ai-openhub-auth-truth]'); - if (authNode instanceof HTMLElement) authNode.textContent = '登录真相:MNote mnote_session;拒绝 OpenHub JWT/localStorage'; - var workspaceNode = drawer.querySelector('[data-page-ai-openhub-workspace-scope]'); - if (workspaceNode instanceof HTMLElement) workspaceNode.textContent = pageAiOpenHubScopeSummary(scope) || '等待 workspace/rootUri/session/tool scope'; - var guardNode = drawer.querySelector('[data-page-ai-openhub-route-guard]'); - if (guardNode instanceof HTMLElement) guardNode.textContent = '非 AI 路由 guard:login/admin/file/knowledge 由 MNote 接管'; - var fallbackNode = drawer.querySelector('[data-page-ai-openhub-fallback]'); - if (fallbackNode instanceof HTMLElement) { - fallbackNode.textContent = pageUiState.pageAiOpenHubFallbackReason || 'OpenHub 不可用时提示用户刷新或检查服务,不再提供 opencode 页面回退'; - } - var iframe = drawer.querySelector('[data-page-ai-openhub-iframe]'); - if (iframe instanceof HTMLIFrameElement) { - var nextUrl = String(pageUiState.pageAiOpenHubIframeUrl || bootstrap.openhubIframeUrl || '/page-ai/openhub/ai').trim(); - if (nextUrl && iframe.getAttribute('src') !== nextUrl) iframe.setAttribute('src', nextUrl); - iframe.hidden = !nextUrl; - } - } - - function pageAiEnsureOpenHubHostDrawer() { - var drawer = ensurePageAiDrawer(); - pageAiInstallMNoteOpenFileBridge(); - if (drawer.getAttribute('data-page-ai-openhub-host') !== 'true') { - drawer.setAttribute('data-page-ai-openhub-host', 'true'); - drawer.removeAttribute('data-page-ai-opencode-host'); - drawer.setAttribute('data-mnote-acp-runtime', 'openhub-opencode'); - drawer.innerHTML = '' + - '' + - ''; - } - pageAiRenderOpenHubHostChrome(); - void pageAiBootstrapOpenHubHost(); - return drawer; - } - - async function pageAiBootstrapOpenHubHost() { - var context = pageAiBuildOpencodeContextPayload(); - try { - var statusResponse = await fetch('/api/page-ai/openhub/status', { headers: { accept: 'application/json' }, cache: 'no-store' }); - pageUiState.pageAiOpenHubStatus = await statusResponse.json().catch(function() { return null; }) || {}; - var response = await fetch('/api/page-ai/openhub/bootstrap', { - method: 'POST', - headers: { 'content-type': 'application/json', accept: 'application/json' }, - cache: 'no-store', - body: JSON.stringify(context) - }); - var payload = await response.json().catch(function() { return null; }); - if (!response.ok) throw new Error(payload && (payload.error || payload.message) ? String(payload.error || payload.message) : 'HTTP ' + response.status); - pageUiState.pageAiOpenHubBootstrap = payload || {}; - pageUiState.pageAiOpenHubStatusError = ''; - pageUiState.pageAiOpenHubIframeUrl = String(payload && payload.openhubIframeUrl || '/page-ai/openhub/ai').trim(); - var statusFallback = pageUiState.pageAiOpenHubStatus && pageUiState.pageAiOpenHubStatus.fallback ? pageUiState.pageAiOpenHubStatus.fallback : null; - pageUiState.pageAiOpenHubFallbackReason = payload && payload.fallback - ? String(payload.fallback.reason || '') - : (statusFallback ? String(statusFallback.reason || '') : ''); - pageUiState.pageAiOpenHubScopeSummary = pageAiOpenHubScopeSummary(payload && payload.scope); - pageAiRenderOpenHubHostChrome(); - } catch (error) { - pageUiState.pageAiOpenHubStatusError = error instanceof Error ? error.message : String(error); - pageUiState.pageAiOpenHubFallbackReason = 'OpenHub bootstrap 不可用,请刷新或检查 OpenHub 服务'; - pageAiRenderOpenHubHostChrome(); - } - } - function pageAiPostOpencodeBridgeMessage(type, payload) { var iframe = document.querySelector('[data-page-ai-opencode-iframe]'); if (!(iframe instanceof HTMLIFrameElement) || !iframe.contentWindow) return false; @@ -1268,45 +1148,6 @@ export function createSidebarPageAiRuntime(context) { return true; } - function pageAiInstallMNoteOpenFileBridge() { - if (pageUiState.pageAiMNoteOpenFileBridgeInstalled) return; - pageUiState.pageAiMNoteOpenFileBridgeInstalled = true; - window.addEventListener('message', function(event) { - if (event.origin !== window.location.origin) return; - var message = event.data && typeof event.data === 'object' ? event.data : null; - if (message && message.source === 'openhub-ai' && message.type === 'mnote:minimize-openhub') { - closePageAiDrawer(); - return; - } - if (message && message.source === 'openhub-ai' && message.type === 'mnote:get-active-tab-address') { - var addressPayload = pageAiCurrentActiveTabAddressPayload(); - var requestPayload = message.payload && typeof message.payload === 'object' ? message.payload : {}; - var kind = String(requestPayload.kind || message.kind || 'tab').trim(); - var value = kind === 'folder' ? addressPayload.folderUrl : addressPayload.tabUrl; - if (event.source && typeof event.source.postMessage === 'function') { - event.source.postMessage({ - source: 'mnote-page-ai', - type: 'mnote:active-tab-address', - requestId: message.requestId || '', - payload: Object.assign({}, addressPayload, { - kind: kind === 'folder' ? 'folder' : 'tab', - value: value - }) - }, event.origin); - } - return; - } - if (!message || (message.type !== 'mnote:open-file' && message.type !== 'mnote:open-reference')) return; - if (message.source !== 'openhub-diff' && message.source !== 'openhub-changed-files' && message.source !== 'openhub-citation') return; - var payload = message.payload && typeof message.payload === 'object' ? message.payload : {}; - pageAiOpenOpencodeChangedFile(message.path || payload.path || payload.href || '', { - rootUri: String(message.rootUri || payload.rootUri || payload.root_uri || currentRootUri() || '').trim(), - workspaceId: String(message.workspaceId || payload.workspaceId || payload.workspace_id || resolveWorkspaceId(document.body) || '').trim(), - documentId: String(message.documentId || payload.documentId || payload.document_id || '').trim() - }); - }); - } - function pageAiInstallOpencodeBridge() { if (pageUiState.pageAiOpencodeBridgeInstalled) return; pageUiState.pageAiOpencodeBridgeInstalled = true; @@ -1540,10 +1381,8 @@ export function createSidebarPageAiRuntime(context) { function pageAiEnsureOpencodeHostDrawer() { var drawer = ensurePageAiDrawer(); - pageAiInstallMNoteOpenFileBridge(); if (drawer.getAttribute('data-page-ai-opencode-host') !== 'true') { drawer.setAttribute('data-page-ai-opencode-host', 'true'); - drawer.removeAttribute('data-page-ai-openhub-host'); drawer.setAttribute('data-mnote-acp-runtime', 'opencode'); drawer.innerHTML = '' + '' + @@ -1828,17 +1667,17 @@ export function createSidebarPageAiRuntime(context) { if (isMarkdown) { if (window.__mnoteLocalFolderEventBus && typeof window.__mnoteLocalFolderEventBus.emitChangedFiles === 'function') { window.__mnoteLocalFolderEventBus.emitChangedFiles({ - source: 'openhub_changed_file_bridge', - reason: 'openhub_changed_file', + source: 'opencode_changed_file_bridge', + reason: 'opencode_changed_file', rootUri: rootUri, workspaceId: workspaceId, changedFiles: [{ relativePath: targetPath, documentId: documentId, changeType: 'modified' }], - affectedParents: [{ relativePath: pageAiParentRelativePath(targetPath), reason: 'openhub_changed_file' }] + affectedParents: [{ relativePath: pageAiParentRelativePath(targetPath), reason: 'opencode_changed_file' }] }); } if (typeof window.__mnoteDocumentPaneRuntime.openPrimaryDocument === 'function') { if (documentId === String(currentDocumentId() || '').trim()) { - document.documentElement.setAttribute('data-mnote-page-ai-openhub-document-pane-refresh', targetPath); + document.documentElement.setAttribute('data-mnote-page-ai-opencode-document-pane-refresh', targetPath); return true; } void window.__mnoteDocumentPaneRuntime.openPrimaryDocument({ @@ -1847,7 +1686,7 @@ export function createSidebarPageAiRuntime(context) { sourceKind: currentSourceKind() || 'local_folder', rootUri: rootUri }); - document.documentElement.setAttribute('data-mnote-page-ai-openhub-document-pane-refresh', targetPath); + document.documentElement.setAttribute('data-mnote-page-ai-opencode-document-pane-refresh', targetPath); return true; } } @@ -3872,20 +3711,6 @@ export function createSidebarPageAiRuntime(context) { function openPageAiDrawer() { - if (pageAiOpenHubHostEnabled()) { - pageAiEnsureContextRefState(); - void pageAiLoadAllowedRoots().then(function() { - pageAiRenderOpenHubHostChrome(); - }).catch(function() { - pageAiRenderOpenHubHostChrome(); - }); - var openhubDrawer = pageAiEnsureOpenHubHostDrawer(); - pageAiApplyDrawerWidth(openhubDrawer); - openhubDrawer.hidden = false; - pageUiState.pageAiOpen = true; - updatePageAiTriggerState(); - return; - } if (pageAiOpencodeHostEnabled()) { pageAiEnsureContextRefState(); void pageAiLoadAllowedRoots().then(function() { @@ -5095,9 +4920,6 @@ export function createSidebarPageAiRuntime(context) { }); } } - if (action === 'openhub-refresh-bootstrap') { - void pageAiBootstrapOpenHubHost(); - } if (action === 'cancel-queued-run') { void pageAiCancelQueuedRun(pageAiAction.getAttribute('data-page-ai-queue-id')); } diff --git a/rust/crates/mnote-web/src/provider_identity_sync.rs b/rust/crates/mnote-web/src/provider_identity_sync.rs index f07859ff..37c9272a 100644 --- a/rust/crates/mnote-web/src/provider_identity_sync.rs +++ b/rust/crates/mnote-web/src/provider_identity_sync.rs @@ -1,13 +1,10 @@ use crate::error::WebError; -use control_plane::{ControlPlaneStore, DirectoryGrantRecord, UserRecord}; +use control_plane::UserRecord; use reqwest::header::{HeaderMap, HeaderValue}; use serde_json::{json, Value}; -use std::collections::hash_map::DefaultHasher; use std::env; -use std::hash::{Hash, Hasher}; use std::time::Duration; -const DEFAULT_OPENHUB_BASE_URL: &str = "http://127.0.0.1:18080"; const DEFAULT_WEKNORA_ENDPOINT: &str = "http://127.0.0.1:8080/api/v1"; const PROVIDER_IDENTITY_SYNC_TIMEOUT_MS: u64 = 1_500; @@ -31,13 +28,6 @@ fn clean_url(value: String) -> Option { (!trimmed.is_empty()).then_some(trimmed) } -fn openhub_base_url() -> String { - env::var("MNOTE_OPENHUB_BASE_URL") - .ok() - .and_then(clean_url) - .unwrap_or_else(|| DEFAULT_OPENHUB_BASE_URL.to_string()) -} - fn weknora_endpoint() -> String { env::var("MNOTE_WEKNORA_ENDPOINT") .or_else(|_| env::var("WEKNORA_ENDPOINT")) @@ -67,12 +57,6 @@ fn internal_headers() -> HeaderMap { headers } -fn stable_openhub_user_id(user_id: &str) -> i64 { - let mut hasher = DefaultHasher::new(); - format!("mnote-openhub-user:{user_id}").hash(&mut hasher); - (hasher.finish() % 1_900_000_000) as i64 + 100_000_000 -} - fn fallback_email(user: &UserRecord) -> String { user.email .as_deref() @@ -82,13 +66,6 @@ fn fallback_email(user: &UserRecord) -> String { .unwrap_or_else(|| format!("{}@mnote.local", user.username)) } -fn default_workspace_path(user: &UserRecord) -> String { - format!( - "/mnt/Data1T/Mnote_data/users/{}/workspaces/my-space", - user.id - ) -} - fn sync_disabled() -> bool { !env_flag("MNOTE_PROVIDER_IDENTITY_SYNC", true) } @@ -96,7 +73,6 @@ fn sync_disabled() -> bool { pub async fn sync_provider_identities( user: &UserRecord, password: &str, - directory_grants: &[DirectoryGrantRecord], ) -> Vec { if sync_disabled() { return vec![ProviderIdentitySyncResult { @@ -107,78 +83,16 @@ pub async fn sync_provider_identities( }]; } - let sync_openhub = env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_OPENHUB", true); let sync_weknora = env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_WEKNORA", true); - - match (sync_openhub, sync_weknora) { - (true, true) => { - let (openhub, weknora) = tokio::join!( - sync_openhub_identity(user, password, directory_grants), - sync_weknora_identity(user, password) - ); - vec![openhub, weknora] - } - (true, false) => vec![sync_openhub_identity(user, password, directory_grants).await], - (false, true) => vec![sync_weknora_identity(user, password).await], - (false, false) => vec![ProviderIdentitySyncResult { + if sync_weknora { + vec![sync_weknora_identity(user, password).await] + } else { + vec![ProviderIdentitySyncResult { provider: "all", ok: true, message: "provider identity sync disabled".to_string(), provider_user_id: None, - }], - } -} - -pub async fn sync_openhub_directory_permissions_for_user_id( - control_plane: &dyn ControlPlaneStore, - user_id: &str, -) -> ProviderIdentitySyncResult { - if sync_disabled() || !env_flag("MNOTE_PROVIDER_IDENTITY_SYNC_OPENHUB", true) { - return ProviderIdentitySyncResult { - provider: "openhub", - ok: true, - message: "provider identity sync disabled".to_string(), - provider_user_id: None, - }; - } - let user_id = user_id.trim(); - let provider_user_id = stable_openhub_user_id(user_id); - let directory_grants = match control_plane.list_directory_grants_for_actor(user_id) { - Ok(grants) => grants, - Err(error) => { - return ProviderIdentitySyncResult { - provider: "openhub", - ok: false, - message: format!("mnote directory grants read failed: {error}"), - provider_user_id: Some(provider_user_id.to_string()), - }; - } - }; - let payload = json!({ - "mnote_user_id": user_id, - "allowedRoots": openhub_allowed_roots_payload(&directory_grants), - }); - match post_json( - join_url( - &openhub_base_url(), - &format!("/api/internal/mnote/users/{provider_user_id}/directory-permissions/sync"), - ), - payload, - ) - .await - { - Ok(_) => ProviderIdentitySyncResult { - provider: "openhub", - ok: true, - message: "directory permissions synced".to_string(), - provider_user_id: Some(provider_user_id.to_string()), - }, - Err(error) => ProviderIdentitySyncResult { - provider: "openhub", - ok: false, - message: error.message().to_string(), - provider_user_id: Some(provider_user_id.to_string()), - }, + }] } } @@ -210,74 +124,6 @@ async fn post_json(url: String, body: Value) -> Result { Ok(payload) } -async fn sync_openhub_identity( - user: &UserRecord, - password: &str, - directory_grants: &[DirectoryGrantRecord], -) -> ProviderIdentitySyncResult { - let provider_user_id = stable_openhub_user_id(&user.id); - let payload = json!({ - "provider_user_id": provider_user_id, - "mnote_user_id": user.id, - "username": user.username, - "email": fallback_email(user), - "password": password, - "workspace_path": default_workspace_path(user), - "allowedRoots": openhub_allowed_roots_payload(directory_grants), - "disabled": user.status != "active", - "is_admin": false, - }); - match post_json( - join_url(&openhub_base_url(), "/api/internal/mnote/users/provision"), - payload, - ) - .await - { - Ok(_) => ProviderIdentitySyncResult { - provider: "openhub", - ok: true, - message: "synced".to_string(), - provider_user_id: Some(provider_user_id.to_string()), - }, - Err(error) => ProviderIdentitySyncResult { - provider: "openhub", - ok: false, - message: error.message().to_string(), - provider_user_id: Some(provider_user_id.to_string()), - }, - } -} - -fn openhub_allowed_roots_payload(directory_grants: &[DirectoryGrantRecord]) -> Vec { - directory_grants - .iter() - .filter(|grant| !is_default_workspace_auto_grant(grant)) - .filter(|grant| grant.status.trim() == "active") - .filter(|grant| !grant.root_path.trim().is_empty()) - .map(|grant| { - json!({ - "grantId": grant.id, - "rootUri": grant.root_uri, - "rootPath": grant.root_path, - "permission": grant.permission, - "recursive": grant.recursive, - "capabilities": serde_json::from_str::>(&grant.capabilities_json).unwrap_or_default(), - "source": grant.source, - }) - }) - .collect() -} - -fn is_default_workspace_auto_grant(grant: &DirectoryGrantRecord) -> bool { - grant.source.trim() == "auto" - && grant.permission.trim() == "write" - && grant.recursive - && grant.created_by.as_deref().map(str::trim) == Some(grant.user_id.as_str()) - && grant.workspace_id.is_some() - && grant.root_uri.starts_with("local://users/") - && grant.root_uri.ends_with("/workspaces/my-space") -} - async fn sync_weknora_identity(user: &UserRecord, password: &str) -> ProviderIdentitySyncResult { let payload = json!({ "mnote_user_id": user.id, diff --git a/rust/crates/mnote-web/src/routes/gateway.rs b/rust/crates/mnote-web/src/routes/gateway.rs index 9f70dc4e..b82fe606 100644 --- a/rust/crates/mnote-web/src/routes/gateway.rs +++ b/rust/crates/mnote-web/src/routes/gateway.rs @@ -2301,16 +2301,7 @@ async fn handle_control_plane_auth_action( }); let provider_sync_results = if matches!(flow, "signUp" | "signIn") { - let directory_grants = state - .control_plane() - .list_directory_grants_for_actor(&resolved.user.id) - .unwrap_or_default(); - let results = sync_provider_identities( - &resolved.user, - &password_for_provider_sync, - &directory_grants, - ) - .await; + let results = sync_provider_identities(&resolved.user, &password_for_provider_sync).await; let _ = state.control_plane().append_audit(AppendAuditInput { actor_user_id: Some(resolved.user.id.clone()), action: "control.auth.provider_identities_synced".to_string(), diff --git a/rust/crates/mnote-web/src/routes/local_folder_source.rs b/rust/crates/mnote-web/src/routes/local_folder_source.rs index 288222d0..e19e3114 100644 --- a/rust/crates/mnote-web/src/routes/local_folder_source.rs +++ b/rust/crates/mnote-web/src/routes/local_folder_source.rs @@ -5,7 +5,6 @@ use crate::page_aggregate::{ PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions, PagePermissions, PageStats, PageTree, }; -use crate::provider_identity_sync::sync_openhub_directory_permissions_for_user_id; use crate::routes::local_markdown_parser::{ file_stem_title, parse_markdown_attachment_refs, parse_markdown_page, split_frontmatter, }; @@ -2442,14 +2441,6 @@ pub async fn create_local_access_grant( ) -> Result<(StatusCode, Json), WebError> { let payload = add_control_plane_local_access_grant_for_context(&state, &context, request) .map_err(|error| error.with_context(&context))?; - if let Some(user_id) = payload - .pointer("/grant/userId") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - { - sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await; - } Ok((StatusCode::OK, Json(payload))) } @@ -2460,14 +2451,6 @@ pub async fn create_user_access_grant( ) -> Result<(StatusCode, Json), WebError> { let payload = add_control_plane_user_access_grant_for_context(&state, &context, request) .map_err(|error| error.with_context(&context))?; - if let Some(user_id) = payload - .pointer("/grant/userId") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - { - sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await; - } Ok((StatusCode::OK, Json(payload))) } @@ -2478,14 +2461,6 @@ pub async fn delete_local_access_grant( ) -> Result<(StatusCode, Json), WebError> { let payload = delete_control_plane_local_access_grant_for_context(&state, &context, &grant_id) .map_err(|error| error.with_context(&context))?; - if let Some(user_id) = payload - .get("revokedUserId") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - { - sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await; - } Ok((StatusCode::OK, Json(payload))) } @@ -2496,14 +2471,6 @@ pub async fn delete_user_access_grant( ) -> Result<(StatusCode, Json), WebError> { let payload = delete_control_plane_user_access_grant_for_context(&state, &context, &grant_id) .map_err(|error| error.with_context(&context))?; - if let Some(user_id) = payload - .get("revokedUserId") - .and_then(Value::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - { - sync_openhub_directory_permissions_for_user_id(state.control_plane(), user_id).await; - } Ok((StatusCode::OK, Json(payload))) } diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index e9c2a86b..abfa65a9 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -28,7 +28,6 @@ mod onlyoffice; pub(crate) mod onlyoffice_bridge; mod page_ai_board; mod page_ai_opencode; -mod page_ai_openhub; mod page_ai_pi; mod page_ai_workflow; mod query_support; @@ -408,68 +407,6 @@ pub fn build_router(state: AppState) -> Router { "/api/page-ai/opencode/status", get(page_ai_opencode::status), ) - .route("/api/page-ai/openhub/status", get(page_ai_openhub::status)) - .route( - "/api/page-ai/openhub/bootstrap", - post(page_ai_openhub::bootstrap), - ) - .route( - "/api/page-ai/openhub/artifact-index", - get(page_ai_openhub::artifact_index_get).post(page_ai_openhub::artifact_index_upsert), - ) - .route("/page-ai/openhub/ai", get(page_ai_openhub::ai_shell)) - .route( - "/page-ai/openhub/ai/{*path}", - any(page_ai_openhub::ai_proxy), - ) - .route( - "/page-ai/openhub/login", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/login/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/admin", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/admin/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/file", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/file/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/files", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/files/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/knowledge", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/knowledge/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/git", - any(page_ai_openhub::non_ai_route_guard), - ) - .route( - "/page-ai/openhub/git/{*path}", - any(page_ai_openhub::non_ai_route_guard), - ) .route( "/api/page-ai/opencode/session", post(page_ai_opencode::bind_session), diff --git a/rust/crates/mnote-web/src/routes/page_ai_pi.rs b/rust/crates/mnote-web/src/routes/page_ai_pi.rs index 331a9268..d9bfea97 100644 --- a/rust/crates/mnote-web/src/routes/page_ai_pi.rs +++ b/rust/crates/mnote-web/src/routes/page_ai_pi.rs @@ -1,6 +1,6 @@ //! Pi-first Page AI Lab — MNote 托管的后端垂直切片。 //! -//! 该模块默认启用 MNote 托管的 Pi Rust Page AI 后端;OpenHub 仅保留为迁移期兼容 / admin 边界。 +//! 该模块默认启用 MNote 托管的 Pi Rust Page AI 后端;OpenHub 与 Pi TS 已退役到 recycle 边界。 //! Pi 进程通过 RPC subprocess 托管,MNote bridge tools 在 Rust 后端按 allowed roots 执行权限校验。 use crate::app::AppState; @@ -60,7 +60,6 @@ const PI_LAB_MAX_STARTS_PER_WINDOW: usize = 4; const PI_LAB_MAX_SENDS_PER_WINDOW: usize = 12; const PI_LAB_MAX_TOOLS_PER_WINDOW: usize = 40; const PI_LAB_RUNTIME_IMPL_RUST: &str = "pi-rust"; -const PI_LAB_RUNTIME_IMPL_TS: &str = "pi-ts"; const PI_LAB_MANAGED_BUILTIN_TOOLS: [&str; 8] = [ "read", "write", @@ -970,9 +969,6 @@ fn pi_binary() -> String { if let Some(explicit) = env_trimmed("MNOTE_PAGE_AI_PI_BIN") { return explicit; } - if pi_runtime_impl() == PI_LAB_RUNTIME_IMPL_TS { - return env_trimmed("MNOTE_PAGE_AI_PI_TS_BIN").unwrap_or_else(|| "pi".into()); - } env_trimmed("MNOTE_PAGE_AI_PI_RUST_BIN") .or_else(first_existing_pi_rust_binary) .unwrap_or_else(|| "pi-rust".into()) @@ -983,7 +979,6 @@ fn pi_runtime_impl() -> String { .or_else(|| env_trimmed("MNOTE_PAGE_AI_PI_RUNTIME_IMPL")) .unwrap_or_else(|| PI_LAB_RUNTIME_IMPL_RUST.into()); match raw.trim().to_ascii_lowercase().as_str() { - "ts" | "typescript" | "pi-ts" | "legacy-ts" => PI_LAB_RUNTIME_IMPL_TS.into(), "rust" | "rs" | "pi-rust" | "pi_agent_rust" | "pi-agent-rust" => { PI_LAB_RUNTIME_IMPL_RUST.into() } @@ -1026,15 +1021,10 @@ fn pi_runtime_binary_available(binary: &str) -> bool { .any(|candidate| candidate.is_file()) } -fn pi_runtime_install_hint(runtime_impl: &str, binary: &str) -> Option { +fn pi_runtime_install_hint(_runtime_impl: &str, binary: &str) -> Option { if runtime_mode() == "mock" || pi_runtime_binary_available(binary) { return None; } - if runtime_impl == PI_LAB_RUNTIME_IMPL_TS { - return Some(format!( - "未找到 Pi TS runtime: {binary}。请设置 MNOTE_PAGE_AI_PI_TS_BIN 或 MNOTE_PAGE_AI_PI_BIN。" - )); - } Some(format!( "未找到 Pi Rust runtime: {binary}。请安装到 ~/.local/share/mnote/pi-rust/bin/pi,或设置 MNOTE_PAGE_AI_PI_RUST_BIN / MNOTE_PAGE_AI_PI_BIN。" )) diff --git a/rust/crates/mnote-web/src/ssr/pages/ai_admin.rs b/rust/crates/mnote-web/src/ssr/pages/ai_admin.rs index d5c1fa70..b7c39a38 100644 --- a/rust/crates/mnote-web/src/ssr/pages/ai_admin.rs +++ b/rust/crates/mnote-web/src/ssr/pages/ai_admin.rs @@ -1272,7 +1272,6 @@ const AI_ADMIN_SCRIPT: &str = r#" function renderServicePanels(piStatus, effectivePayload) { var providers = effectivePayload && Array.isArray(effectivePayload.providers) ? effectivePayload.providers : []; - var openhubStatus = '兼容保留'; var defaultModel = effectivePayload && (effectivePayload.defaultModel || effectivePayload.default_model || ''); var piDefaultModel = piStatus ? normalizeModelRef(piStatus.defaultModelProvider || piStatus.default_model_provider, piStatus.defaultModelId || piStatus.default_model_id) @@ -1329,20 +1328,6 @@ const AI_ADMIN_SCRIPT: &str = r#" '' + '' + '' + - '
' + - '

OpenHub

' + - renderTag(openhubStatus, 'orange') + - '打开 OpenHub Admin' + - '
' + - '
' + - '
' + - renderKv('定位', '迁移期兼容 / admin 边界,不再作为默认 Page AI chat') + - renderKv('MNote 侧入口', '/page-ai/openhub/ai') + - renderKv('管理入口', '/page-ai/openhub/admin') + - renderKv('后续策略', '只保留一个用户 Page AI 入口,OpenHub 不再扩展重复面板能力') + - '
' + - '
' + - '
' + ''; } if (channelsPanel) { @@ -1408,7 +1393,6 @@ const AI_ADMIN_SCRIPT: &str = r#" if (healthGrid) { healthGrid.innerHTML = [ ['Pi Rust Page AI', piRunning ? '运行中' : (piRuntimeAvailable ? '可启动' : '不可用'), 'impl=' + piRuntimeImpl + ' · runtime=' + piRuntime + ' · model=' + piDefaultModel], - ['OpenHub', '兼容保留', '迁移期 admin / fallback 边界,不再作为默认 Page AI chat'], ['LightRAG', '默认', '唯一默认 knowledge provider,Pi 通过 MNote facade 查询'], ['Control-plane', effectivePayload && (effectivePayload.sourceOfTruth || effectivePayload.source_of_truth) || 'directory_grants', '授权、策略、receipt 真相层'] ].map(function(item) { @@ -1605,7 +1589,7 @@ const AI_ADMIN_SCRIPT: &str = r#" ''; var body = ''; if (activePanel === 'tools') { - body = '

工具权限

按 OpenHub risk_level 分组
' + (tools || '
暂无工具
') + '
'; + body = '

工具权限

按 MNote policy 分组
' + (tools || '
暂无工具
') + '
'; } else if (activePanel === 'skills') { body = '

Skills

全局启用后用户可单独降权
' + (skills || '
暂无 Skill
') + '
'; } else if (activePanel === 'mcp') { @@ -1621,7 +1605,7 @@ const AI_ADMIN_SCRIPT: &str = r#" '' + (roots || '
暂无可单独管理的目录权限
') + ''; } else { - body = '

模型权限

按 provider 分组,参考 OpenHub 用户模型权限
' + (modelRows || '
暂无可用模型
') + + body = '

模型权限

按 provider/model policy 分组
' + (modelRows || '
暂无可用模型
') + '