Files
mnote/design/05-editor-mainline/done/5-30-sidebar-starred-shortcuts-sqlite-scoped-explorer-v1.md
T
lix-2026 1882db7681 收口 MNote P0 P1 P2 审查尾项
- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
2026-06-01 09:29:12 +08:00

9.9 KiB
Raw Blame History

5-30 Sidebar 星标置顶快捷入口与 Scoped Explorer 设计 v1

状态:done Owner05-editor-mainline / 03-rust-web / control-plane 背景:星标置顶需要对齐 Wolai 的 Sidebar 顶部快速访问体验,但 MNote 还需要支持把高频本地文件夹固定成 Explorer scoped root,避免每次进入大 workspace 时加载完整根目录。

目标

星标置顶不是文件或文件夹自身的属性,而是用户在 Sidebar 固定的一组快捷入口。

  • 当前页面 .md 可以加入星标置顶,点击后直接打开该页面。
  • FileTree 文件夹右键可以加入星标置顶,点击后切到 Explorer,并把该文件夹作为临时 scoped root 显示。
  • 星标置顶按登录用户持久化到 SQLite control-plane;同一用户在不同设备登录时应看到同一组快捷入口。
  • 顶栏“全网公开”不能默认误导显示;没有真实公开分享状态时隐藏。
  • scoped Explorer 必须避免扫描完整大 workspace,例如 /mnt/Data1T/mnote 下只打开 design/ 时只加载 design/ 子树。

非目标

  • 不把星标写入 Markdown frontmatter。
  • 不写入 .mnote/starred-pins.json 作为长期真相。
  • 不把文件夹变成 Resource Tree 的业务属性。
  • 不在前端 localStorage 中持久化最终星标真相;前端最多做加载态/乐观态缓存。
  • 不在本阶段实现跨设备文件内容同步;本设计只定义用户快捷入口在 control-plane 的一致性。

用户模型

星标置顶等价于“我的 Sidebar 快捷入口”:

  • 页面快捷入口:kind=page,目标是某个 workspace 下的页面 identity。
  • 文件夹快捷入口:kind=folder,目标是某个 workspace 下的相对路径 scope。
  • 快捷入口属于用户,不属于文件夹,不影响其他用户。
  • 如果某台设备没有对应 workspace 或没有该 folder 相对路径,星标项保留但显示失效态,允许移除或重新绑定。

SQLite 数据模型

新增 control-plane 表:sidebar_shortcuts

建议字段:

CREATE TABLE sidebar_shortcuts (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  root_uri TEXT,
  kind TEXT NOT NULL, -- page | folder
  source_kind TEXT NOT NULL DEFAULT 'local_folder',
  target_id TEXT NOT NULL,
  relative_path TEXT,
  document_id TEXT,
  title TEXT NOT NULL,
  icon TEXT,
  sort_order INTEGER NOT NULL DEFAULT 0,
  status TEXT NOT NULL DEFAULT 'active',
  metadata_json TEXT NOT NULL DEFAULT '{}',
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  revision INTEGER NOT NULL DEFAULT 1,
  UNIQUE(user_id, workspace_id, kind, target_id)
);

target_id 规范:

  • 页面:优先 document_id,例如 local-md:design%2FREADME.md
  • 文件夹:local-dir:<encoded relative path> 或稳定的 local:folder:<relative path> 归一化值。

relative_path 用于 scoped Explorer 与跨设备路径映射,必须保存 workspace 内相对路径,不保存绝对本机路径作为主键。

root_uri 必须作为显式字段保存和返回,而不是只塞在 metadata_json 中。原因:

  • 星标文件夹点击必须直接消费保存时的 workspace root 上下文,不能从 workspace_id 字符串反推本机路径。
  • local workspace id 是派生标识,不是路径编码协议;用它反推 file:///... 会把 UI fallback 变成事实源。
  • metadata_json.rootUri 只作为兼容冗余字段,不能作为唯一来源。

长期跨设备映射仍以 workspace_id + relative_path 为主;当前设备打开本地 workspace 时必须把实际 root_uri 一起写入 shortcut,缺失时应显示失效/需重绑定状态,而不是自动猜测路径。

API / 命令面

新增最小 API

  • GET /api/sidebar/shortcuts?workspaceId=...
    • 返回当前用户当前 workspace 的星标快捷入口。
  • POST /api/sidebar/shortcuts
    • upsert 快捷入口。
    • body 包含 workspaceId/rootUri/kind/sourceKind/targetId/relativePath/documentId/title/icon
  • DELETE /api/sidebar/shortcuts/:id
    • 移除当前用户自己的快捷入口。

实现边界:

  • API 只操作 control-plane 用户快捷入口表。
  • API 不写本地文件,不改 Markdown,不改 .mnote 元数据。
  • 写操作要求真实登录用户;dev fallback 不应静默创建跨用户快捷入口。
  • 每次写入追加 auditsidebar.shortcut.created / sidebar.shortcut.removed
  • 后续需要实时同步时,通过 outbox_events 或现有 WS delta 广播 sidebar.shortcut.*

Projection 与 Sidebar 渲染

WorkspaceShellProjection.starred_items 需要从 documents[].is_starred 迁移为 control-plane sidebar_shortcuts 投影。

投影项扩展:

  • id
  • title
  • kind: page | folder
  • href
  • workspaceId
  • sourceKind
  • targetId
  • relativePath
  • rootUri
  • documentId
  • active
  • unavailable

页面快捷入口:

  • href 指向当前页面打开 URL。
  • 点击后走现有 document open 主链。

文件夹快捷入口:

  • href 可为当前页面 URL 加 query,例如 ?treeView=filetree&filetreeScope=design
  • 更推荐 runtime 拦截点击,调用 openScopedFileTree(relativePath),避免不必要的页面跳转。
  • 点击后切换到 Explorer tab,并显示 scoped Explorer。

Scoped Explorer 行为

文件夹星标点击后:

  1. Sidebar 切到 Explorer tab。
  2. Explorer 标题显示 scoped 状态,例如 Explorer / design
  3. FileTree root 只渲染该文件夹下的 children。
  4. 子目录继续按需加载。
  5. 提供“返回工作区根”入口;点击后恢复 root Explorer。

数据读取:

  • scoped root 为 relativePath=design 时,调用现有 load_local_folder_file_tree_children_snapshot(rootUri, "design") 或对应 API。
  • 不调用完整 root snapshot。
  • 不预加载 scoped root 之外的兄弟目录。

URL / 状态:

  • scoped 状态可以写入 URL querytreeView=filetree&filetreeScope=design
  • 刷新页面后恢复 scoped Explorer。
  • 切回“我的页面”不清除 scope;点击“返回工作区根”才清除 scope。

顶栏星标与公开状态

顶栏星标按钮:

  • 未星标:空心星;tooltip 对齐 Wolai:“点击 加入星标置顶,可在左侧边栏顶部快速访问”。
  • 已星标:高亮星;tooltip “取消星标置顶”。
  • 点击当前页面星标只操作 sidebar_shortcuts

顶栏公开状态:

  • wolai-public-pill 只有当当前页面存在 active share link / public permission 时显示。
  • 没有公开状态时隐藏,不显示“全网公开”。
  • 公开分享弹窗仍属于 share link / permission 领域,不与星标快捷入口混用。

FileTree 右键菜单

文件夹行增加:

  • 未星标:加入星标置顶
  • 已星标:取消星标置顶

启用条件:

  • sourceKind == local_folder
  • rowKind == folder | directory | index
  • 当前用户对 workspace 有至少 read access;写入快捷入口只需要用户偏好写权限,不要求文件系统写权限。

不支持项:

  • 普通附件文件暂不加入星标置顶。
  • 多选批量星标暂不做。

失效与重命名处理

MVP 处理:

  • 文件夹被删除:快捷入口保留,显示失效态;点击提示“文件夹不存在”,允许移除。
  • 文件夹重命名:如果重命名由 MNote 发起,可同步更新对应 relative_path/title/target_id;如果外部重命名,先显示失效态。
  • 页面被删除:页面快捷入口显示失效态,允许移除。

长期可选:

  • 引入 local object stable id 后,星标目标可由 stable object id 跟随重命名。

验收标准

  • 无公开分享状态时,顶栏不显示“全网公开”。
  • 当前页面点击星标后,Sidebar 星标置顶出现该页面;再次点击移除。
  • 右键 design/ 文件夹可加入星标置顶。
  • 点击 design/ 星标后,切到 Explorer scoped root,只加载并显示 design/ 下内容,不显示 workspace root 的其他大目录。
  • 刷新页面后,filetreeScope=design 能恢复 scoped Explorer。
  • 同一用户重新登录后,星标快捷入口仍存在。
  • 不同用户登录同一 workspace 时,星标列表互不污染。
  • 文件夹不存在时,星标项显示失效态并可移除。

测试计划

Rust/control-plane

  • migration 创建 sidebar_shortcuts
  • store 可 upsert/list/delete 当前用户快捷入口。
  • unique 约束防止同一用户同一 workspace 重复固定同一目标。
  • 不同用户同一目标互不影响。

mnote-web

  • API 鉴权:只能读写当前用户自己的快捷入口。
  • WorkspaceShellProjection 从 shortcuts 投影 starred rows。
  • local folder scoped snapshot 不扫描完整 root。

浏览器 smoke

  • 顶栏公开 pill 隐藏断言。
  • 顶栏星标页面 add/remove。
  • FileTree 文件夹右键 add/remove。
  • 点击星标文件夹进入 scoped Explorer,并断言 root siblings 不渲染。
  • 刷新恢复 scoped Explorer。

实施顺序

  1. control-plane migration / model / store:新增 sidebar_shortcuts
  2. mnote-web APIlist/upsert/delete shortcuts。
  3. workspace shell projection:星标区改读 shortcuts,并保留历史 is_starred fixture 兼容测试。
  4. 顶栏:星标按钮接 API;公开 pill 改为条件显示。
  5. FileTree 右键菜单:文件夹加入/取消星标置顶。
  6. Scoped Explorer runtime:支持 filetreeScope,只加载 scoped children,并提供返回 root。
  7. smoke 与 Rust 测试补齐。

决策记录

  • 2026-05-26:星标置顶确定为用户级 Sidebar 快捷入口,不是文件夹/页面自身属性。
  • 2026-05-26:星标快捷入口必须写入 SQLite control-plane,满足同一用户跨设备一致显示。
  • 2026-05-26:文件夹星标点击进入 scoped Explorer,避免大 workspace root 扫描。
  • 2026-06-01:只读复核确认 sidebar_shortcuts migration/store/API、显式 rootUri、scoped Explorer 与 task492-sidebar-starred-shortcuts-smoke.js 已覆盖本文主验收;本文归档到 done/