dsh-md2pdf
Verified@puwenhui/dsh-md2pdf · v0.1.2 · MIT
DSH 插件:Markdown → 单文件 HTML + A4 PDF(同一条渲染管线,页眉按文档首个一级标题自动生成)
Install
dsh plugin add @puwenhui/dsh-md2pdf Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
@puwenhui/dsh-md2pdf
DSH 插件:把 Markdown 转成 PDF(同时产出同名单文件 HTML)。在对话里直接说「把某个 .md 转成 PDF」即可,也可在浏览器里点链接预览/下载产物。
定位:把原 webphone-frontend-sdk/tools/md2pdf.mjs 的能力插件化——同一套渲染管线(markdown-it + highlight.js + guide-style.css + 本机 Chromium 打印),从「命令行/拖拽」升级为「对话即用」。
能力一览
| 面 | 说明 |
|---|---|
模型工具 md2pdf_convert |
对话中把 1 个或多个 .md(或目录)转成 PDF |
HTTP 端点 /dsh-md2pdf/download |
浏览器直接预览/下载刚生成的 PDF/HTML |
页眉自动生成:取文档第一个一级标题(# xxx),没有则回退文件名主干;页脚固定为「当前页 / 总页数」。页眉无需人工指定。
安装
从 npm 安装(已发布):
dsh plugin --profile web add @puwenhui/dsh-md2pdf
也可以从本地目录安装(开发中/未发布时):
dsh plugin --profile web add file:<本地插件目录的绝对路径>
包内声明了 dsh.bundle.patch,dsh 会自动把本包追加进 profile 的 dsh.profile.bundles,无需手改任何 profile 清单。
安装后重启宿主(bundle 层只在启动时组装):
dsh web
验证是否挂上(不必启动服务):
dsh --profile web --dump-config | grep md2pdf
若
dsh未加入 PATH,用node <DSH 安装目录>/apps/cli/lib/bin.js代替上面的dsh即可。
⚠️ 安装时实测到的坑:dsh plugin add 会剪掉 profile 的安装回退链接
本插件是在真实 web profile 上实测安装的,过程中踩到一个 DSH 层面的坑,记录在此以免重踩:
dsh plugin add会在 profile 目录里跑一次pnpm。pnpm 的默认行为是清理 extraneous(不在 lockfile 里的)条目。$DSH_HOME/profiles/web/node_modules里除了 pnpm 管的依赖,还可能有 DSH 安装器放的安装回退链接(指向 dsh 安装目录里的内置包)。这些不在 profile 的 lockfile 里,于是会被 pnpm 当作 extraneous 删掉。- 后果:profile 下次启动时报
Cannot find package '@deepseek-ai/dsh-api-session-controller' imported from ...\profiles\web\,整个宿主起不来(报错只提内置包,容易被误判为插件问题)。
判断方法:把 composed config 里每个插件行的 name 逐个从 profile 目录解析一遍,
只要有一个解析不到,宿主就一定起不来:
dsh --profile web --dump-config # 取全部 name
修复:把缺失的内置包补回 $DSH_HOME/profiles/node_modules/@deepseek-ai/
(该目录是「安装依赖闭包的镜像」,由 healProfilesModuleFallback 只增不删地维护;
而 pnpm 只作用于 profiles/web,不会碰它 —— 所以补在这里既正确又不会被下次安装再次剪掉)。
本次实测缺失的 7 个及其来源:
| 包 | 指向 |
|---|---|
@deepseek-ai/dsh-api-session-controller |
packages/api/session-controller |
@deepseek-ai/dsh-api-settings-controller |
packages/api/settings-controller |
@deepseek-ai/dsh-api-workspace-controller |
packages/api/workspace-controller |
@deepseek-ai/dsh-client-ui-session |
packages/client/ui-session |
@deepseek-ai/dsh-client-ui-approval |
packages/client/ui-approval |
@deepseek-ai/dsh-client-ui-chat |
packages/client/ui-chat |
@deepseek-ai/dsh-client-ui-schedule |
packages/client/ui-schedule |
(packages/ 指 DSH 安装目录或源码检出目录下的 packages/ 子目录。)
该问题与插件内容无关:任何
dsh plugin add都可能触发。若dsh plugin add被中断 (例如网络卡在某个 optional 包的拉取上而超时),更容易留下这种半成品状态。
用法
对话里直接说,例如:
- 「把
手册.md转成 PDF」(相对路径按会话工作目录解析) - 「把
docs/目录下所有 md 都转成 PDF」 - 「转这个 md,页眉写『某某手册』」
工具参数:
| 参数 | 必填 | 说明 |
|---|---|---|
inputs |
✅ | 一个或多个 .md 文件路径,或包含 .md 的目录(取其中一层全部 .md) |
out_dir |
输出目录;缺省与源 .md 同目录(沿用原脚本行为) | |
title |
强制指定页眉标题(缺省自动取文档一级标题) | |
label |
页眉右侧标签(缺省用插件配置 headerLabel,通常为空) |
产物与源文件同名:手册.md → 手册.pdf + 手册.html。相对路径按会话工作目录解析。
返回结果里带 downloadUrl,形如
http://127.0.0.1:3080/dsh-md2pdf/download?path=...&disposition=inline,
浏览器打开即预览(disposition=attachment 则下载)。
配置(可选,全部有默认值)
改 cordis.patch.yml 的 config 段:
- insert:
- id: md2pdf
name: "@puwenhui/dsh-md2pdf"
config:
chromePath: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
cssPath: "C:\\path\\to\\your.css"
headerLabel: "263 云通信"
pageFormat: "A4"
downloadBaseUrl: "http://127.0.0.1:3080"
也可用环境变量 MD2PDF_CHROME 指定浏览器。
依赖与本机要求
- 运行时依赖:
markdown-it、highlight.js、puppeteer-core(装在插件自己的node_modules,随包解析,不依赖宿主)。 - 需要本机已装 Chrome 或 Edge(
puppeteer-core不下载浏览器)。未探测到时报错会列出全部候选路径与三种解决办法。 - 自动探测顺序:
MD2PDF_CHROME→config.chromePath→ Chrome 常见安装位 → Edge 常见安装位 → Playwright 的 chromium 缓存。
自检
cd dsh-md2pdf-plugin # 源码检出目录
node scripts/smoke.mjs # 29 项断言:模块契约→工具注册→真渲染→下载端点→DSH schema 校验
node scripts/convert.mjs <a.md> [--out <dir>] [--title <t>] [--label <l>] # 命令行直转(等价原 md2pdf.mjs)
smoke 用假 ctx 把整条链路跑一遍,退出码即结果;convert 是不走 DSH 宿主的 CLI 入口,用于脚本批处理或工具尚未加载时。
与原 tools/md2pdf.mjs 的关系
渲染管线、页眉页脚版式、A4 页边距、guide-style.css 全部保持一致,产物外观相同。移植过程中修掉了原脚本的两个真实缺陷:
- 标题渲染成了
<hh1>:原heading_open规则写`<h${tokens[idx].tag} ...>`,而 markdown-it 的token.tag已经是h1,于是产出<hh1>——未知元素会退化成inline,所有标题的一级/二级样式全部丢失(实测原稿 h1 与正文同字号)。现改为<${tokens[idx].tag} ...>,标题按 CSS 正常缩放(h1 = 1.9em)。 - 带 UTF-8 BOM 的 .md 取不到页眉标题:BOM 让首行
#前多一个\uFEFF,^#匹配失败,页眉退化成文件名。现读取时统一去 BOM,并把正则放宽到允许 ATX 标题前最多 3 个空格。实测原项目的《SDK 接入手册》正是带 BOM 的文件,所以该 bug 在原脚本下是必然触发的。
两项均已在 scripts/smoke.mjs 里加了回归断言。
已知边界
- 目录输入只取一层
.md,不递归。 - 下载端点只服务本进程本次运行中由本插件产出的文件(内存白名单),宿主重启后旧链接失效;端点同时限定 loopback 直连、无代理转发头、Origin 与 Host 同源,无法读取任意文件。
- 单个 .md 过大(几十 MB 级)时 Chromium 打印会明显变慢;工具
timeoutMs设为 180 秒。
目录结构
dsh-md2pdf-plugin/
├── package.json # 声明 dsh.bundle.patch(决定 dsh plugin 自动加入 bundles)
├── cordis.patch.yml # 本插件贡献的插件行 + 可选 config
├── lib/
│ ├── plugin.mjs # 宿主半边:md2pdf_convert 工具 + 下载端点
│ └── guide-style.css # 默认导出样式(与原 tools/guide-style.css 同源)
└── scripts/ # 不随包发布(files 未包含)
├── smoke.mjs # 自检
└── convert.mjs # 命令行直转入口
License
MIT