npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-convert-core

v0.1.0-alpha.9

Published

Standalone conversion kernel: file / url (web page, WeChat article) / video / text to knowledge-ready Markdown + local assets, with an HTTP API and a React console built on @tita-agent/ui. The local media pipeline (audio/video/image → Markdown, subtitles

Readme

dsh-convert-core

转换内核:把 Office / OpenDocument / RTF / EPUB / CSV / 文本层 PDF、音视频与图片、 混合层 PDF、另存下来的网页(含微信公众号文章) 转成 知识就绪 Markdown + 本地图片, 自带 HTTP API 和一个 React(@tita-agent/ui)可视化控制台。 它可以独立运行,不依赖 DSH。

单一职责:只做转换,不联网取内容。 输入是磁盘上的一份文件(或一段内联文本),输出是 Markdown。需要浏览器才能读的页面(风控、验证码、JS 渲染、登录态)由宿主用自己的浏览器读下来 另存为 .html,再按文件交给内核 —— 内核没有「粘一个网址让它去抓」这条路。

npm install dsh-convert-core
npx tita-convert --port 8787
# → http://127.0.0.1:8787/

DSH 侧只是它的再封装:dsh-data-domain-knowledge 把本包当 optionalDependency 懒加载, 设置项、Agent 工具与 /convert 路由在那边,解析逻辑全在这里。

引擎:@firecrawl/anydoc

解析全部交给 @firecrawl/anydoc(MIT,Rust 内核, 官方 Node/Python/WASM binding,本机用 @firecrawl/anydoc-darwin-arm64 预编译二进制)。

  • 一个依赖覆盖 14 种格式:doc/docx/docm、ppt/pps/pot/pptx/pptm/ppsx/ppsm、 xls/xlsx/xlsm/xlsb、odt/ods/odp、rtf、epub、csv、pdf
  • 按内容识别格式(PDF 头、RTF 开组、OLE 流名、ZIP mimetype),改错扩展名也能转
  • 没有 Python、没有 torch、没有模型下载、没有外部二进制,中位数 4.4ms/文档
  • 结构保真:标题层级、w:numPr 列表、合并单元格表格、脚注、公式转 LaTeX
  • 错误码精确:unsupported / needsOcr / malformed / encrypted / resourceLimit / missingPart / io, 内核原样透出(前缀 ANYDOC_)并附上可操作建议

唯一的胶水:内嵌图片

Markdown 无法内嵌字节,所以 anydoc 把内嵌图片渲染成它的 alt 文本,字节留在 document.assets 上(带 mediaType 和 originPart)。而知识召回只认本地相对路径的图片, 所以 src/adapters/anydoc.ts 做了三件事:

  1. 遍历 document.blocks 找 { kind: 'image', source: { kind: 'asset', assetId } },按文档顺序取出图片;
  2. 按内容哈希写成 assets/img-<sha1_12>.<ext>;
  3. 把正文里那个 alt 行换成 ![alt](assets/img-….<ext>);找不到唯一占位时追加到文末 ## 图片 小节并记 warning,而不是猜位置。

非 image/* 的内嵌对象不落地,但计数并写 warning,不静默丢弃。

分层约定

| 层 | 负责 | 不负责 | | --- | --- | --- | | 内核(本包) | 解析、任务队列、进度、取消、交付目录、控制台 | 知识库、DSH 工具、用户审批 | | DSH 插件 | 设置、Agent 工具、/convert 挂载、入库接缝 | 任何解析逻辑 |

内核提供两种集成形态,同一份实现:

const running = await startConvertServer({ port: 8787 })            // 独立运行

const service = await createConvertService({ rootDir })            // 嵌入宿主
const handler = createRequestHandler({ service, basePath: '/convert' })

产物契约

<outRoot>/<YYYY-MM-DD>/<中文名>/    # 日期层由 bucketByDay 打开;叶子名见 deliveryDirName()
  index.md          front matter + 正文,图片引用为 assets/img-<sha1_12>.<ext>
  assets/           内容寻址的本地图片
  source/           原始输入(0048):抓下来的 HTML、上传的那份文件
  manifest.json     来源、引擎与格式、warnings、content_hash、ingest 负载

交付目录是用户会打开的东西,所以叶子名是文档的中文名(output/2026-10-02/月度销售汇总/), 不是 conv_<时间戳>_<随机>:名字取标题,没有标题就用文件名(去掉扩展名),最多 25 个字, 同一天同名让路成 -2、-3。让路结果记在任务的 deliveryName 上,所以重试与重启之后算出来的 都是同一个目录;那份记录早于这个字段时回退到 task id。工作目录不跟着改名(仍按 task id): 工地只要唯一,成品的位置要稳定。

source/ 是底稿,不是资产(0048)。抽取是有损的(正文外的元素、内嵌音视频都不在 产物里),而工作目录在交付成功那一刻就被删掉 —— 不留底稿,事后想换个抽取规则重来一遍就 没有素材了。网页那档由适配器交出真正被解析的那份 HTML(就是宿主交进来的那份 .html); 其他档由内核把暂存输入复制过来。manifest.original 记它的 rel / bytes / sha256,manifest.ingest.original 让知识层也能记住它。内联文本没有独立 底稿(index.md 就是它),所以不写这个键,而不是造一个空的。

manifest.document 记文档自身的元数据(作者、发布时间、站点、描述)—— 适配器能拿到 多少写多少,拿不到就不写。它一路走到 SourceRaw 的 front matter(OKF v0.2 的 description 与 sources[])。

front matter 与知识录入契约对齐:title / created_at / updated_at 必填, external_id / source_format / content_hash / parser / source_path 由内核如实填写。

范围

文件类:14 种文档格式(含文本层 PDF)、本地音视频与图片、扫描件轻量 OCR、另存的页面(.html/.htm)。 还没做:内核内的浏览器档(Playwright / CDP)、在线视频字幕/转写、版面/表格/公式级的高质量扫描档 (docling / MinerU)。

抓取不在本包范围(原 kind=url 已删除)。需要真浏览器才能读的页面(公众号重复访问会跳 验证码页这类风控墙)本来就不是静态抓取能解决的,而宿主手里有真浏览器 —— 让它读下来另存为 .html,再按文件提交。本档会从 og:url / canonical 把原始地址读回来,产物与直抓一致。

Agent 侧的同一件事已经写成 skill:browser-page-to-markdown —— 用 Kimi WebBridge 驱动 用户的真实浏览器读一次页面、把渲染后的 HTML 落到本地,再交给本档(kind=file 的 .html)。 一个实测过的细节:把浏览器拿回来的 poc_token 拼回裸 fetch 没有用(仍 302), 凭证是绑会话的 —— 所以路线是「浏览器读、本档转」,不是「让直抓借用浏览器的凭证」。

needsOcr 不再是一条死路:.pdf 先走 anydoc(文本层质量最好、零依赖),它报 needsOcr 时自动改走 scanned 档,任务消息与 manifest.warnings 里写明换了什么(ADAPTER_FALLBACK), 产物里另有 SCANNED_TIER 说明这一档不做版面还原。判据在 resolveFallbackAdapter() (src/adapters/index.ts),换档只发生一次。特别地,不使用 anydoc 的 ocr: 'hosted' —— 它会把整份文档发往 Firecrawl Parse 云端,与本包「本地优先」的取舍冲突。

适配器

| 输入 | 适配器 | 说明 | | --- | --- | --- | | 14 种文档格式(含文本层 PDF) | anydoc | 见上;内嵌图片落地到 assets/ | | 另存下来的网页 / 微信公众号文章(kind=file 的 .html/.htm) | web | 能力来自内核自带的网页管线 src/web/:读入一份已在手上的 HTML → 正文抽取(公众号走 #js_content,其他站点走 Readability)→ 图片按内容 sha1 落地 assets/ → turndown 出 GFM Markdown。不抓页面、不跟进链接(唯一的网络请求是正文图片);正文图片引用一律改成 assets/…,远程地址不会留在产物里。原始地址从页面自带的 og:url / canonical 读回来(相对地址、source_url、幂等身份都靠它) | | 音视频(.mp4 .mov .mkv .webm …、.mp3 .m4a .wav …)与图片(.png .jpg .webp …) | iddoc | 能力来自内核自带的本地媒体管线 src/media/:字幕优先,无字幕时才本地转写(默认不下载模型,IDDOC_ASR=1 才允许);图片默认只收录不做 OCR;视频关键帧(均匀采样 + 场景变化 + 内容去重)落地到 assets/ | | 扫描件 / 纯图片页 PDF | scanned | 同一包的 convertScannedDocument():pypdfium2 逐页渲染 + 本地 RapidOCR 识别,page-0001.png … 落地为 assets。默认作为 anydoc 报 needsOcr 时的自动兜底;SCANNED_OCR=1 是「跳过 anydoc 直接走 OCR」的显式出口。不做版面还原 | | .md .markdown .txt .json .tsv .yml .log .adoc .rst .org .tex | text | 直通 + 契约归一化(重写 front matter)|

未登记扩展名只有在字节确实是文本(无 NUL、UTF-8 可解码)时才走 text, 否则返回 FORMAT_UNSUPPORTED 并列出支持的格式。

iddoc 与 scanned 是需要外部依赖的两个适配器:iddoc 要 ffmpeg/ffprobe (转写/OCR 档还要 uvx),scanned 只要 uvx。web 不需要外部二进制,但需要网络。 缺依赖时按既有约定返回 DEPENDENCY_MISSING + 安装命令,能力上报里如实写 available: false,不静默降级。

API

| 方法 | 路径 | 说明 | | --- | --- | --- | | GET | /api/health | 服务与交付根目录 | | GET | /api/capabilities | 适配器可用性、引擎版本、支持的扩展名 | | POST | /api/tasks | 受理任务:JSON(text / url / local_path)、原始字节、或 multipart/form-data | | GET | /api/tasks | 任务列表 | | GET | /api/tasks/:id | 任务详情(含 ingest 负载) | | POST | /api/tasks/:id/cancel | 取消(删除半成品交付) | | POST | /api/tasks/:id/retry | 重试(复用已落盘的输入) | | GET | /api/tasks/:id/markdown | 交付的 index.md | | GET | /api/tasks/:id/files/<rel> | 交付目录内任意文件(拒绝路径穿越) | | GET | /api/events | SSE 任务事件流 |

# 内联文本
curl -X POST localhost:8787/api/tasks -H 'content-type: application/json' \
  -d '{"kind":"text","title":"标题","text":"# 内容"}'

# 本机文件(内核负责读取、限流、清理)
curl -X POST localhost:8787/api/tasks -H 'content-type: application/json' \
  -d '{"kind":"file","local_path":"/abs/path/spec.docx"}'

# 网页 / 微信公众号文章(只抓这一个地址)
curl -X POST localhost:8787/api/tasks -H 'content-type: application/json' \
  -d '{"kind":"url","url":"https://mp.weixin.qq.com/s?__biz=..."}'

# 上传
curl -X POST "localhost:8787/api/tasks?kind=file" -F "[email protected]"

控制台

web/index.html + web/app.js + web/styles.css,由 build-console.mjs 用 esbuild 从 console/ 打出来。界面是 React + @tita-agent/ui(仓库共享的 shadcn 底座), 样式表直接拷 packages/ui 编译出的那一份 —— 控制台和三个插件穿同一套视觉,不再各造一套。

运行时仍然零构建:内核只负责把 web/ 下这三个静态文件发出去,npx tita-convert 不需要打包器;打包器只在构建期存在,产物自包含。代价是改控制台的类名之后要重编 packages/ui(@source 清单在那里),这也是三个插件一直以来的做法。

界面结构是两个视图(右上角切换):

  • 转换 —— 一个输入框收文件(拖进来,或点「浏览文件」)、 依赖项清单(ffmpeg / anydoc / uvx / readability / whisper,名字 + 版本 + 它负责什么)、 支持的格式(按谁负责它分组,共 72 种)。
  • 任务列表 —— 四列的表:标题(第二行是它出了什么事)| 状态 | 时间 | 操作。 失败的出路是行内一枚按钮,完整建议收进展开的「说明」活页。停下来的一行可以删除 (记录 + 交付目录一起删,删前确认)。

设计取舍

  • 本地优先:全链路在本机完成,不调用任何云端解析;引擎没有网络代码。
  • 图片必须落地:见上「唯一的胶水」。
  • 可取消:任务用 AbortController,取消后删除半成品交付目录。
  • 进程重启可恢复:任务状态落 tasks.json;重启时把 running/queued 标为 INTERRUPTED_BY_RESTART 并保留已落盘的输入,重试不必重来。
  • 不伪造成功:引擎的错误码原样透出,needsOcr / encrypted / resourceLimit 各自带建议,没有「转成功但内容为空」的分支。