# dsh-notebook-v2 体验修复 v6 —— 计划(待独立评审) > 用户反馈(2026-11-04,第 6 轮真机):md 渲染不对 / 打开要手输工作目录 / Shift-Enter 不自动运行 / > cell 之间没有新增按钮 / 其他同类问题 / 大纲要像 VS Code(md 各级标题 + 函数定义)。 > 附加要求:广泛参考现成 Jupyter 软件,并论证「为什么不直接把 VS Code 的 Jupyter 插件搬过来」。 --- ## 0. 结论先行:六条反馈 → 根因 → 修法 | # | 用户原话 | 实测根因(已核实,不是猜) | 修法 | |---|---|---|---| | 1 | md 渲染有问题,没有常规 md 渲染 | ① markdown 是**手写正则渲染器** `markdownLite`(client.js:528):无嵌套列表、无任务列表、无 `---`、无内联 HTML、无硬换行;标题字号 `20 - level*2` → h1=18px、**h6=8px**(几乎看不见)。② **KaTeX 只有 JS 没有 CSS**:`grep katex.min.css lib/cm-vendor.js lib/client.js` = 0 命中 → 所有公式必然错版(无字体、无间距)。③ 表格对齐标记、围栏语言、图片相对路径全部没处理。 | 引入成熟渲染器 `marked`(MIT,原生 GFM:表格/任务列表/删除线/自动链接/嵌套列表)+ `dompurify` 消毒 + **KaTeX CSS 与字体**打进 vendor;新增 `renderMarkdown(src)` → sanitize 后 HTML;`nb2-md` 作用域样式表(对齐 GitHub/Jupyter 视觉);相对路径图片走新端点 `/nb2/file`。`markdownLite` 保留为 vendor 缺失时兜底。 | | 2 | 打开应直接看工作区里的 notebook,却要我输工作目录 | `workspaceRoot()`(index.js:104)取 `ctx.get("sandboxPolicy").workspaceRoot`,拿不到就回落 `process.cwd()`。**实测运行中进程的默认 cwd = `C:\Users\ASUS\.dsh\profiles\desktop`**(不是工作区)。而「打开」只有一个绝对路径输入框,没有浏览能力。 | ① 默认目录改成**会话自己的工作目录**:`ctx.agents.get(sid)` → `ctx.workingDirectory.get(agent.session)`,退 `workingDirectory.defaultDirectory`,再退 `sandboxPolicy.workspaceRoot`。② 新端点 `GET /nb2/notebooks?sid&dir&depth` 递归列举 `.ipynb`(跳过 `.git/node_modules/.venv/__pycache__` 等,深度≤4,上限 500 条)。③ 「+打开」改为 **picker**:搜索框 + 相对路径/修改时间/大小列表 + ↑↓ Enter + 「手输路径…」兜底。 | | 3 | 左 shift+enter 没有自动运行 | 三处独立缺口:**a)** `CellView.onKeyDown` 在 `if (h2.inControl) return` 处提前返回 —— 焦点落在格内任意按钮/下拉(点过 ▶ 就会)后,所有 Enter 组合键全死;**b)** 焦点在面板空白/标签条/cell 列表(无编辑器、无容器)时**根本没有任何处理器**接收事件;**c)** 最后一格 Shift-Enter 不会像 VS Code 那样自动补一个新格,用户感觉「按了没反应」。 | ① 面板根节点加 **capture 阶段 keydown**,统一分发 `Shift/Ctrl/Alt/CtrlShift-Enter`;命中 `.cm-editor` 或文本输入框才放行给下层。② `CellView` 的 Enter 组合判断提到 `inControl` 之前。③ 运行后 select-below 并**聚焦下一格编辑器**;最后一格则新建一格再聚焦。④ 补 VS Code 的 `Ctrl-Shift-Enter`(上方插入并运行)。 | | 4 | cell 下方 / cell 之间有没有新增按钮 | 只有顶部工具栏的 `+ 代码 / + MD / + SH`,且**只追加到末尾**;没有「在某格之后插入」的可见入口。 | 新增 `CellDivider`:每个 cell 之前、之后、列表末尾都有,hover/选中态显示 `+ 代码` `+ 文本`;点击 = 在该处插入并**自动聚焦新格编辑器**。 | | 5 | 其他同类问题 | 见 §2 排查清单(10 条,已逐条定位) | 逐条修(表格里标 P0/P1/不做) | | 6 | 大纲要像 VS Code:`#`/`##`/`###` + 函数定义 | `buildOutline`(client.js:160)只做 markdown `#` 标题 + 每格首行,**没有任何代码符号**;UI 是扁平列表,不可折叠、无过滤、点击不聚焦。 | ① 代码符号用 **`@lezer/python`**(已随 `@codemirror/lang-python` 打进 vendor,**零额外体积**)解析出 `FunctionDefinition` / `ClassDefinition`(实测可取到 name 与位置),按缩进建树、带行号。② markdown 支持 ATX + setext,跳过围栏。③ UI:可折叠树 + 过滤框 + 当前格高亮 + 点击跳转并聚焦 + 记住展开态。④ 纯函数 + 单测。 | --- ## 1. 「为什么不直接把 VS Code 的 Jupyter 插件搬过来」 这是本轮最值得先讲清楚的一条,因为它决定后面所有取舍。 ### 1.1 为什么搬不动(不是不想) | vscode-jupyter 的层 | 依赖 | 能否搬 | |---|---|---| | 扩展激活 / 命令 / 状态栏 | VS Code **extension host** + `vscode` 模块 | ❌ 没有扩展宿主 | | cell 交互、焦点、工具栏、虚拟滚动 | VS Code **Notebook API**(`vscode.NotebookCellData`、`NotebookCellList`、`CellFocusMode`、`INotebookExecutionStateService`) | ❌ 这是 VS Code workbench 内部实现,不是插件代码 | | 编辑器 | **Monaco**(`CodeCell.setModel`、编辑器池、`CodeCellRenderer.renderTemplate`) | ❌ 我们用的是 CM6(且 Monaco 101MB + worker,见既有决策) | | 输出渲染 | `vscode-notebook-renderer` API + **webview** 隔离 | ❌ 需要 VS Code 的 renderer 通信层 | | 内核协议 | `jupyterlab/services`-style 纯 TS + `nbformat` | ✅ **我们已经等价拥有**(sidecar 走 jupyter_client 的 Jupyter messaging) | | 输出 MIME 处理 / nbformat 归一 | 纯 TS 逻辑(`src/kernels/execution/helpers.ts`) | ✅ 语义已抄(`translateStreamOutput` → 我们的 stream 合并、`raises-exception` 继续) | **一句话**:vscode-jupyter 里「用户看得见的那 80%」是 VS Code 平台代码(workbench + Monaco + webview renderer),插件本体是接线员。搬插件 = 搬一具没有骨架的身体。 ### 1.2 真正该复用的三样东西(本轮就用) 1. **协议层**:已经是标准 Jupyter messaging(不发愁)。 2. **渲染层**:`marked` + `dompurify` + `katex`(JupyterLab 的 rendermime 其实也是这条链路的同族:`markdown-it` + `DOMPurify` + KaTeX,只是它绑 `@lumino/*`)。 3. **符号层**:VS Code 的 Outline 来自 LSP `documentSymbol`;我们没有 LSP,但 `@lezer/python` 是货真价实的 Python 语法树,**已经在包里**,比正则准、比 LSP 轻。 ### 1.3 为什么不引 JupyterLab 的 rendermime `@jupyterlab/rendermime` 与其 markdown 渲染器依赖 `@lumino/widgets` / `@jupyterlab/apputils` 的 DI 容器, 塞进 DSH 客户端插件(只能 `require` 白名单模块)需要连带一套 Lumino widget 生命周期;而它内部用的就是 `markdown-it` + `DOMPurify` + `katex`。**直接用它依赖的那三件,比搬它自己便宜一个数量级。** ### 1.4 边界(明确不做) ipywidgets(`jupyter-js-widgets` 需要 comm + 前端模型,独立工程)、调试器、Monaco、LSP 级类型诊断。 --- ## 2. 「其他同类问题」排查清单(已定位,逐条处置) | # | 问题 | 定位 | 处置 | |---|---|---|---| | 5.1 | 新增 cell 后**不聚焦**,用户得再点一下才能打字 | `addCell` 只 `setSelected`(client.js:2458) | **P0** 新增 `focusReq` 机制:插入后聚焦该格编辑器 | | 5.2 | 工具栏行 2 挤了 11 个按钮,`+代码/+MD/+SH` 语义与分隔线按钮重复 | client.js:3254-3306 | **P0** 分组 + 溢出到 `⋯`;分隔线按钮保留(它是「末尾追加」的语义) | | 5.3 | 大纲点击只 `setSelected` + 滚动,**不聚焦** | client.js:3488-3495 | **P0** 跳转后聚焦该格(markdown 则滚动到位) | | 5.4 | 空 notebook 空态只有一个「新建一格」 | client.js:3536-3543 | **P0** 空态给 `+ 代码` / `+ Markdown` 两个大按钮 | | 5.5 | `.cm-scroller { maxHeight: 60vh }` 可能在长 cell 里产生**双滚动条** | client.js:1056 | **P1** 改为不限制高度(外层列表已滚动),仅在超长时才限 | | 5.6 | 输出区没有「复制」按钮(只能拖选) | `OutputBlockView` | **P1** hover 显示 ⧉ 复制(纯文本输出) | | 5.7 | markdown 渲染区点击进编辑后光标位置不稳定 | `enterEdit` + `setTimeout(0)` | **P1** 进编辑后把光标放到点击行(粗略按 offsetY 估算) | | 5.8 | 「内核 unknown」时无解释 | 行 3 状态 | **P1** 文案改为「内核未启动(点运行即启动)」 | | 5.9 | `window.prompt/confirm/alert` 禁用扫描 | 已有测试 | **保持绿**(新增代码不得引入) | | 5.10 | markdown cell 的 `完成` 按钮 / 双击进编辑两套入口不一致 | client.js:2175 | **P1** 统一:渲染区单击进编辑、双击也进、Esc 退出 | --- ## 3. 文件改动清单(预估) ### 宿主半 `lib/index.js`(+~180 行) - `defaultCwdFor(sid)`:`agents.get(sid)` → `workingDirectory.get(agent.session)` → `workingDirectory.defaultDirectory` → `sandboxPolicy.workspaceRoot` → `process.cwd()`;**替换现有 `workspaceRoot()` 的三处调用点(263/516/657/951)**,并保留 `workspaceRoot()` 作为最后兜底。 - 新路由 `notebooks/list`:`{ sid, dir?, depth? }` → `{ root, cwd, entries:[{path, rel, mtime, size}] }`;`node:fs` 递归,跳过黑名单目录,上限 500。 - 新路由 `file`:`{ base, path }`(base = notebook 所在目录)→ 只允许读 `base` 子树内的**图片/文本**(png/jpg/gif/webp/svg),带 `mime` 与 `Cache-Control`;防目录穿越(`resolve` 后必须 `startsWith(base + sep)`)。 - `katex.css` 路由:与 `vendor.js` 同款,提供 `lib/katex.css`。 ### 客户端 `lib/client.js`(+~700 行 / 改 ~150 行) - 新增:`renderMarkdown()` / `ensureKatexCss()` / `nb2MdCss` / `NotebookPicker` / `CellDivider` / `MarkdownView` / `panelKeydown` 分发器 / `buildOutlineV2`。 - 改:`buildOutline`(→ 树 + 符号)、`CellView`(keydown 顺序、hover 工具栏、聚焦 request)、`addCell`(聚焦)、打开入口、工具栏分组、空态。 ### 构建 `scripts/build-cm-vendor.mjs` + `scripts/cm-entry.js` - `cm-entry.js` 增:`export { marked } from "marked"`、`export { default as DOMPurify } from "dompurify"`、`export { parser as pythonParser } from "@lezer/python"`。 - 新脚本步骤:生成 `lib/katex.css`(`katex.min.css` + 20 个 woff2 内联为 base64;预估 23KB + 348KB ≈ 371KB)。 - 依赖:优先把 `marked`/`dompurify` **显式写进 `/.nb2-build/package.json` 并 `npm i`**(可复现);装不上则退回从 profile `node_modules` 解析(构建期依赖,产物已提交进仓库,运行时零依赖)。 ### 测试 - `test/outline.mjs`(新):Lezer 符号抽取(函数/类/嵌套/async/decorator/语法错误容错)、markdown 标题(ATX/setext/围栏跳过)、树结构。 - `test/markdown.mjs`(新):`marked` 渲染快照(表格/任务列表/嵌套列表/围栏/代码高亮钩子)、`DOMPurify` 必须清掉 `