Skip to content

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 全部保持一致,产物外观相同。移植过程中修掉了原脚本的两个真实缺陷:

  1. 标题渲染成了 <hh1>:原 heading_open 规则写 `<h${tokens[idx].tag} ...>`,而 markdown-it 的 token.tag 已经是 h1,于是产出 <hh1>——未知元素会退化成 inline,所有标题的一级/二级样式全部丢失(实测原稿 h1 与正文同字号)。现改为 <${tokens[idx].tag} ...>,标题按 CSS 正常缩放(h1 = 1.9em)。
  2. 带 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