dsh-plugin-ipynb
给 DeepSeek Harness(DSH)加上读 Jupyter Notebook的能力,两侧都管:
| 半边 | 干什么 |
|---|---|
| Client(侧边栏) | 在右侧边栏打开 .ipynb 时自动渲染成 notebook 视图 —— 不再是一坨原始 JSON。Markdown 单元格、代码单元格、流输出、DataFrame 表格、图片、报错回溯都在。 |
| Host(模型工具) | 给模型三个工具:ipynb_read(读单元格+输出)、ipynb_summary(目录与体检)、ipynb_export(导出 Markdown / Python / 纯文本)。 |
侧边栏预览
打开任意 .ipynb(文件树、引用链接、@ 引用都行),默认渲染器就是 Notebook,不需要点任何东西。
工具栏的渲染器下拉里另外还能选 Code(语法高亮的原始 JSON)和 Plain text(原文)。
渲染规则:
- Markdown 单元格:走平台自己的
MarkdownText(@deepseek-ai/dsh-client-ui-primitives,和聊天区同一套), 所以标题、粗斜体、行内代码、围栏代码块、列表、引用、分隔线、链接、表格都全, 并且$…$/$$…$$/\(…\)/\[…\]的 TeX 由 KaTeX 排版(矩阵、分数、求和、希腊字母、\begin{pmatrix}之类都正常)。MathML 无障碍臂、公式字体都跟着平台走,不需要额外资源。 - 代码单元格:等宽正文 +
In [n]执行序号。 text/latex/text/markdown输出:也交给同一个渲染器 —— 已经是$…$的按原样过, 裸 TeX 自动补上$$…$$走 display 模式。- 输出:
stream:先剥 ANSI 颜色转义,再把\r覆盖式输出(tqdm 进度条)压成最终一行;text/html:表格直接转成真 DOM 表格(pandas DataFrame 的主要形态,自动高度、可横向滚动);非表格 HTML 走sandbox=""的沙箱 iframe(脚本、同源全禁);image/png|jpeg|gif|webp|bmp|svg+xml:data URI 直接内联;error:整段回溯按错误色显示;- 其它 MIME(plotly / widget 之类):列出 MIME 名并说明未渲染,而不是静默空白。
- 超长输出:单条超过 30000 字符截断并标注截掉多少。
- 坏文件:不是合法 JSON、顶层不是对象、UTF-8 解不出来,都给一张说明卡,并指路去 Plain text。
- 样式只用主题 token(
--dsw-alias-*),明暗主题都跟随;不 import 任何 Harness 客户端包。
它是怎么挂进去的
ctx.documentPreviews.register({
id: "dsh-plugin-ipynb/notebook",
extensions: ["ipynb"],
priority: "extension", // 默认档;注册表按 rank 降序排,builtin 排它后面
title: () => "Notebook",
loading: "bytes-complete", // 挂载即整文件读取;text-pages 要滚到底才续页,notebook 必须整份
wrap: false
})
ctx.slots.inject("sidebar.right.tab.document", () => ctx.slots.register(
{ name: "sidebar.right.tab.document", key: "dsh-plugin-ipynb/notebook" }, Body))
纯文本兜底实现声明的是 extensions: [],永远排在命中列表之后,所以默认选中的就是我们,不是 Plain text。
文件
dsh-plugin-ipynb/
├─ client.js # Client 半边:浏览器产物(window.__ModuleLoader__.load)
├─ lib/index.js # Host 半边:三个模型工具
├─ lib/host.js # loader 挂载点(export * 转发,见下)
├─ cordis.patch.yml.tpl # bundle 补丁层模板(install 脚本生成 cordis.patch.yml)
├─ package.json # dsh.bundle.patch + dsh.client + exports["./client"]
└─ test/
├─ smoke.mjs # Host 半边 103 项
├─ client-smoke.mjs # Client 半边 65 项(假 React 真渲染一遍组件树,含公式去向往)
└─ fixtures/ # nbformat 3 / 4 / 损坏 / 空 / BOM 样本
安装
首次安装:先在本目录运行
install.ps1(Windows)或node install.mjs(macOS/Linux)。 脚本按当前目录自动生成cordis.patch.yml(已.gitignore),不写死任何盘符。
plugin_manager install_bundle file:<本目录的绝对路径>
装完在 GUI 的「设置 → 插件」里可见、可整体启用/停用。改 lib/index.js 或 client.js 立即生效(挂载用绝对路径,HMR 会重载客户端模块);只有改 cordis.patch.yml 才需要重装。
自测(零依赖):
node test/smoke.mjs # 103/103
node test/client-smoke.mjs # 65/65
两个坑(都踩过,别再踩)
1. 挂载点为什么是 lib/host.js 而不是 lib/index.js
ClientModuleRegistry 用 specifier 作键缓存包元数据(sourceKey = baseUrl + "\0" + loaderName),
而且这份缓存从不失效。本插件最初以 lib/index.js 挂载时,那份 package.json 还没有 dsh.client,
于是 null 被永久缓存 —— 之后补上 dsh.client 也不会被重新解析。
换一个 specifier 就能让扫描重新读一次 package.json。所以补丁挂的是 lib/host.js
(一层 export * from "./index.js")—— 不要改回 lib/index.js,否则侧边栏渲染器会静默消失,直到 DSH 重启。
2. React.memo(...) 返回的是对象,不是函数
判断「这个导出能不能渲染」不能写 typeof x === "function":
primitives.MarkdownText // memo(function MarkdownText(...) {...})
typeof primitives.MarkdownText // "object" ← 不是 "function"
createElement 完全接受它,但那个 typeof 守卫会判 false。v1.2.0 就是这么栽的:
MarkdownText 明明取到了,却被判成不可用,静默退回自带的极简渲染,于是公式
两次都不渲染,而自测还是绿的(因为替身当时写成了普通函数,正好绕开守卫)。
现在 isRenderableType() 同时接受函数与带 $$typeof 的 memo/forwardRef 对象,
自测里的 MarkdownText 替身也改成 React.memo(...) 包出来的,专门守住这条。
另外:降级不再静默 —— 一旦退回自带渲染,notebook 头部会出现一个
「公式未渲染」的告警 chip(鼠标悬停给出具体原因)。
Host 工具用法
// ipynb_read
{ "path": "analysis/eda.ipynb", "include": ["code", "markdown"], "limit": 20 }
{ "path": "eda.ipynb", "includeSource": false } // 只看输出
// ipynb_summary
{ "path": "eda.ipynb", "previewChars": 120 }
// ipynb_export
{ "path": "eda.ipynb", "format": "markdown", "out": "exports/eda.md" }
path 相对路径按会话工作目录解析,也接受绝对路径。out 同样按会话工作目录解析且
必须留在该目录内(.. 逃逸被拒);read-only 沙箱模式下一律拒绝写盘。
兼容性
刻意做了这些处理,都是实测踩过的坑:
- nbformat 4 与 3 并存:
cells与worksheets[].cells都认;v3 的heading归一化成 Markdown 标题(按level补#),input→ source,prompt_number→execution_count,pyout→execute_result,pyerr→error;stream 的stream字段与 v4 的name字段都当流名读。 source/text两种形态:字符串与「按行拆开的字符串数组」都拼回整段。- BOM / CRLF:UTF-8 BOM 会被剥掉。
- 未知
output_type:不崩,降级成占位并在warnings里明说。 - 坏文件明确报错:JSON 非法、顶层不是对象、路径不存在、目标是目录、超过体积上限(默认 32 MB) 各给各的错误信息,绝不返回半成品。
- 零宿主依赖:
lib/index.js只 import Node 内置模块(以绝对路径挂载的插件没有可达的node_modules,import "@deepseek-ai/dsh-tools"会在导入阶段失败,而 loader 只报一句failed to import);client.js只 require 平台种子模块表里的词 ——react与@deepseek-ai/dsh-client-ui-primitives(见下),并把它声明进dsh.client.external。 工具按 registry 强制的原始 JSON Schema 方言注册,Host 自测里那个 schema 校验器保证 「声明的 schema」和「实际返回的值」始终一致。
配置
cordis.patch.yml 里可选的挂载参数(省略即默认):
- insert:
- id: plugin-ipynb-readonly-1
name: '<本目录>\\lib\\host.js'
config:
maxBytes: 33554432 # 单个 notebook 的读取上限
maxSourceChars: 20000 # 单格源码默认截断
maxOutputChars: 4000 # 单条输出默认截断
cwd: '<可选:工作区绝对路径>' # 拿不到会话工作目录时的兜底
只读声明
Host 侧只读 notebook,唯一的写能力是 ipynb_export 在你显式给 out 时写导出文件,且受会话沙箱与
工作目录围栏约束。Client 侧纯浏览器渲染,HTML 输出只走表格转 DOM(只取文本)或 sandbox="" 的
沙箱 iframe。不联网、不起子进程、不注入微信/QQ 之类的外部进程。
可移植性 / 换机器安装
这个仓库不包含本机绝对路径:cordis.patch.yml 由安装脚本按当前目录生成(已 .gitignore)。
换一台机器:
git clone本仓库到任意目录;- 运行
powershell -ExecutionPolicy Bypass -File .\install.ps1(Windows)或node install.mjs(macOS/Linux); - 在 DSH 里执行
plugin_manager install_bundle file:<本目录的绝对路径>。
运行环境
- Windows / macOS / Linux 均可:Host 侧只用 Node 内置模块,Client 侧是预打包浏览器 bundle。
- Node 20+(DSH 自带);无需
npm install。 - 不依赖 Jupyter 内核;只解析
.ipynb文件本身。
No comments yet. Be the first to write one.