17 KiB
17 KiB
note-to-mp 设计文档 (Detail Design)
拆分文档索引:
- 架构总览:
architecture.md- 图片管线:
image-pipeline.md- 渲染服务蓝图:
render-service-blueprint.md- 图示 (Mermaid):
diagrams.md本文件保留全量细节,增量演进请同步上述子文档。
1. 背景
为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要:
- 支持多种图片书写形式(Wikilink 与标准 Markdown)。
- 统一图片处理与上传(包括 WebP 转换、水印、封面选择)。
- 自动抽取文章元数据(标题、作者、封面图)。
- 支持自定义短代码(
gallery)与行级语法扩展(||样式块、[fig .../]等)。 - 提供灵活的封面回退逻辑(frontmatter 指定优先,缺省取正文第一图)。
2. 目标
| 目标 | 说明 |
|---|---|
| 图片语法统一 | ![[file.png]] 与  最终统一进入 LocalImage 管线 |
| 元数据抽取 | 自动获取标题、作者、封面图(可回退)供后续上传逻辑使用 |
| 封面回退 | 未显式指定封面时,自动决策第一张图片 |
| Gallery 支持 | 将 {{<gallery .../>}}{{<load-photoswipe>}} 转成图片 wikilinks 列表 |
| 预处理 | 在 Markdown 渲染前执行自定义语法转 HTML |
| 易扩展 | 提供独立函数/接口减少耦合,如 selectGalleryImages、extractWeChatMeta |
| 默认封面配置 | 无任何图片候选时使用 defaultCoverPic (可配置) |
| 前置回退解析 | 若 metadataCache 缺失 frontmatter,启用手动行级解析回退 |
| Gallery 块扩展 | 支持 {{<gallery>}} 块 + 内部 `figure src |
| 行级语法扩展 | [fig .../] 与 ` |
| 调试日志节流 | 输出当前文件路径与默认封面选用日志,3 秒内同路径不重复 |
3. 术语与定义
- Wikilink 图片语法:
![[xxx.png]] - 标准 Markdown 图片:
 - Frontmatter:位于首部
---包裹的元数据区域。 - Cover(封面):用于公众号首图上传的图片。
- Gallery Shortcode:
{{<gallery dir="/img/foo" figcaption="说明"/>}}{{<load-photoswipe>}}
4. 系统现状概览
主要处理链路:
Raw Markdown
↓ extractWeChatMeta (保留 frontmatter 内容供分析)
↓ 去 frontmatter
↓ transformGalleryShortcodes (gallery → ![[...]] 列表)
↓ transformGalleryBlock (gallery 块/figure → ![[...]] 列表)
↓ marked.parse() (图片扩展 -> LocalImage token)
↓ applyCustomInlineBlocks (fig/彩色段落 轻语法 HTML 化)
↓ 生成 HTML + 样式注入
↓ setArticle()
↓ getArticleContent() -> preprocessContent(line regex 替换) -> 最终 HTML
5. 架构模块划分
| 模块 | 关键函数/变量 | 作用 |
|---|---|---|
| 内容预处理 | preprocessContent() |
行级 Regex 转 HTML(图片路径修正、` |
| 图片统一解析 | LocalFileRegex、MarkdownImage tokenizer |
标准化所有图片为 LocalImage token |
| 图片资源管理 | LocalImageManager |
记录本地图片、上传、替换 URL、Base64 嵌入 |
| Gallery | _listGalleryImages / selectGalleryImages / transformGalleryShortcodes |
短代码 → wikilink 列表(可扩展 figcaption) |
| 元数据抽取 | extractWeChatMeta / getWeChatArticleMeta |
标题 / 作者 / 封面图计算 |
| 封面自动补全 | getMetadata() 尾部逻辑 |
无 frontmatter cover 时回填 |
| 图片上传 | uploadLocalImage / uploadCover |
WebP→JPG、加水印、水印依赖 wasm |
| WebP 支持 | PrepareImageLib + wasm |
转换后再上传 |
| 渲染管线 | renderMarkdown |
串联以上逻辑 |
6. 数据流示意
参见第 4 节图。每个阶段保证产物单向流入下一层,避免循环依赖。
7. 关键算法与实现细节
7.1 图片统一转换
- Regex:
LocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/ - Markdown 标准图片 tokenizer:
- 匹配
→matches[0]。 - 取 basename → 构造
![[basename]]语义(内部直接建 LocalImage token,不再二次正则回匹配)。 - 避免原先多余
-2.png)残留问题。
- 匹配
7.2 元数据抽取(extractWeChatMeta)
- 捕获 frontmatter 简易块(首个
---区间)。 - 解析
title / author / image单行 KV。 image→ 取 basename →![[basename]]。- 回退封面:同时匹配 wikilink + markdown 图片,比较 index 取出现最早的一种。
- 返回:
{ title, author, coverLink, rawImage }。 - 与
getMetadata()融合以补齐空缺字段。 - 若 Obsidian
metadataCache返回为空或缺失字段,触发手动 fallback:扫描首段 frontmatter 行(不依赖外部 YAML 包),支持key: value单行形式;空字符串的 cover/image 会被视为未提供。 - 追加默认封面逻辑:封面候选链(frontmatter cover > 正文首本地图/本地 wikilink/markdown > gallery 生成图 > defaultCoverPic)。
7.3 前置处理(preprocessContent)
[fig .../]→<span>(题注样式)。- 行级命令:
||r / ||g / ||b / ||y / ||→ 不同背景色<p>。 <img src="img/...">→ 前缀补全/img/。
7.4 Gallery 功能
- 短代码 Regex:
{{<gallery dir="..."( figcaption="...")?/ >}}{{<load-photoswipe>}} _listGalleryImages:读目录 + 过滤扩展 + 排序 + 截断。selectGalleryImages:对外通用(支持未来 random / prefix / includeDirInLink)。- 输出:多行
![[file]],并追加可选figcaptiondiv。
7.4.1 块级 Gallery 语法(新增)
支持:
{{<gallery>}}
{{<figure src="/img/foo-1.png" caption="说明" >}}
{{<figure src="/img/foo-2.jpeg" caption="说明2" >}}
{{</gallery>}}
转换:
![[foo-1.png]]
![[foo-2.jpeg]]
规则:
- 仅取 src 的 basename,忽略 caption(后续可扩展为题注输出)。
- 若块内未匹配到任何 figure,保留原文本。
- 正则:
/{{<gallery>}}([\s\S]*?){{<\/gallery>}}/g与内部figureRegex = /{{<figure\s+src="([^"]+)"[^>]*>}}/g。 - 输出顺序按出现顺序。
figure标签支持src="..."与可选link="...",当存在 link 时仍按src的 basename 作为图片候选;后续可利用 link 生成超链接包装。
7.4.2 link 属性与未来 caption 计划
- 当前:
link仅被解析但未输出额外结构,保留在后续渲染扩展阶段使用(例如生成<a>包裹<img>)。 - 规划:
caption字段可映射为 wikilink alias 或<figcaption>。
7.5 行级轻语法扩展 (applyCustomInlineBlocks)
- 输入:渲染后 HTML / 或预处理文本段落。
- 规则:
[fig 内容 /]→<span class="n2m-fig">内容</span>(当前实现可能用内联 style,后续计划换 class)。||r 文本/||g/||b/||y/|| 文本→ 彩色背景段落<p style>...</p>。
- 节点安全:通过转义内部 HTML 以防注入(若未实现需列入风险)。
- 后续:提取公共 class + 主题 CSS。
7.6 调试日志与节流
- 目的:调试封面选取与路径解析;避免刷屏。
- 机制:记录最近一次输出路径时间戳,3 秒内同路径日志抑制。
- 日志包括:当前 markdown 文件绝对路径;默认封面 fallback 触发说明;gallery 转换统计(可选)。
7.7 配置项外化 (Settings 更新)
- 新增:
galleryPrePath,galleryNumPic,defaultCoverPic。 - 位置:
NMPSettings+SettingTabUI 输入框。 - 迁移:移除硬编码常量
GALLERY_PRE_PATH/GALLERY_NUM_PIC。 - 默认值:
defaultCoverPic = 'cover.png'(可为相对/绝对/网络 URL 或 wikilink 形式)。 - 风险:用户提供的默认封面不存在 → 目前不校验,可后续增加存在性检查与 Notice。
7.8 封面候选决策链(更新版)
- 若已有
thumb_media_id(外部指定)→ 不再上传本地封面,保持 null。 - frontmatter cover/image(非空字符串)→ 使用其 basename 生成 wikilink。
- 正文扫描首个本地图片(markdown / wikilink;忽略 http/https)。
- 若正文无 → 使用 gallery 自动展开生成的第一张候选。
- 若仍无 → 使用
defaultCoverPic(若配置)。 - 若
defaultCoverPic也无 → cover 为空。
Edge Cases:
- frontmatter cover: "" → 视为未提供。
- defaultCoverPic 若为绝对 URL → 在上传阶段需区分远程/本地策略。
- gallery 展开后若所有图片为远程 URL(未来支持)→ 不作为本地候选,跳到 defaultCoverPic。
8. 正则清单
| 场景 | 正则 | 说明 |
|---|---|---|
| frontmatter | ^---[\s\S]*?\n--- |
仅首段 |
| Wikilink 图片 | !\[\[(.+?)\]\] |
非贪婪 |
| Markdown 图片 | !\[[^\]]*\]\(([^\n\r\)]+)\) |
不跨行 |
| Gallery | {{<gallery\s+dir=\"([^\"]+)\"(?:\s+figcaption=\"([^\"]*)\")?\s*\/>}}{{<load-photoswipe>}} |
捕获 dir/caption |
| Gallery 块 | {{<gallery>}}([\s\S]*?){{<\/gallery>}} |
块包裹内容 |
| Gallery figure | {{<figure\s+src=\"([^\"]+)\"[^>]*>}} |
提取图片 src |
| Figure link 属性 | link=\"([^\"]+)\" |
可选外链(当前仅解析) |
| fig | \[fig([^>]*?)\/] |
题注 |
| 行块 | ||r (.*) 等 |
行级匹配 |
9. 错误与边界
| 情况 | 行为 |
|---|---|
| frontmatter 缺尾部 | 不解析,当普通正文 |
| 无 image 且正文无图 | coverLink 为空 |
| Gallery 目录缺失 | 原样保留短代码 |
| WebP 转换失败 | 记录日志,使用原文件 |
| 非支持图片扩展 | 忽略该文件 |
10. 性能
- 正则线性扫描 O(n)。
- Gallery 目录排序 O(m log m)。
- 可后续对
_listGalleryImages结果加缓存。
11. 配置 & 常量
| 常量 | 说明 | 后续计划 |
|---|---|---|
galleryPrePath |
画廊根目录(配置项) | 未来参数化 per-block 覆盖 |
galleryNumPic |
默认选图数量(配置项) | 支持块/短代码 count 覆盖 |
defaultCoverPic |
默认封面备用 | 校验存在 / 多备选随机 |
| (移除)GALLERY_PRE_PATH | (已外化) | - |
| (移除)GALLERY_NUM_PIC | (已外化) | - |
| 行级样式内联 | 直接 embed style | 可改 class + CSS |
12. 对外接口
| 方法 | 描述 |
|---|---|
getWeChatArticleMeta() |
获取最近一次渲染抽取的 meta |
getMetadata() |
微信上传所需聚合元数据,含封面补回 |
uploadCover() |
上传封面,含 webp 处理 |
uploadLocalImage() |
上传正文图片 |
renderMarkdown() |
触发整个渲染链路 |
13. 测试建议
| 测试项 | 用例 |
|---|---|
| frontmatter | 正常/缺尾部/缺字段/中文标题 |
| 首图回退 | wikilink 与 markdown 顺序交错 |
| Gallery | 有/无目录;含 caption;空目录 |
| 图片文件名 | 中文/空格/连字符/数字/大小写扩展 |
| 行级语法 | 多种颜色并存/与普通段落混排 |
| WebP | 可转换/未准备 wasm |
| 覆盖逻辑 | frontmatter 不同组合(仅 author、仅 title 等) |
14. 可扩展点
| 方向 | 说明 |
|---|---|
| 更完整 YAML | 使用 js-yaml 支持多行、列表、复杂类型 |
| tags/categories | 抽取为数组并暴露接口 |
| Gallery 参数 | 支持 count=、random=、includeDir= 等 |
| 封面策略 | 配置“frontmatter 优先 / 正文优先 / 首图随机” |
| 图廊 HTML 模式 | 直接生成 <figure> 集合而非 wikilink 列表 |
| 样式外置 | 行级块样式改为统一 CSS class |
| 默认封面池 | 支持数组随机选择 default cover |
| 默认封面校验 | 选择时校验存在性 + Notice 提示 |
| caption alias | gallery figure caption -> wikilink alias/figcaption |
| link wrap | figure link 生成 <a> 包裹图片 |
| debug 开关 | 设置中关闭全部调试日志 |
| 目录缓存 | 减少频繁 IO |
15. 风险与规避
| 风险 | 缓解 |
|---|---|
| 简化 frontmatter 误判 | 提示限制 + 计划引入 YAML 解析 |
| 正则误伤 | 增加单元测试覆盖边界字符 |
| Gallery IO 阻塞 | 后续异步 + loading 占位 |
| 移动端缺 fs | try/catch + 环境判断 |
| 样式散落行内 | 后续集中到主题 CSS |
16. 示例复盘
示例:
---
title: 6月特种兵式观展
author: 大童
image: "/img/shufa/a.jpg"
---
前言

![[c-second.png]]
结果:
- 封面:
![[a.jpg]](frontmatter 优先) - 若删去 image 行 → 封面:
![[b-first.png]](首图)
17. 迭代优先级建议
| 优先级 | 项目 |
|---|---|
| 高 | 封面 UI 选择确认 |
| 中 | YAML 解析器集成 |
| 中 | Gallery 参数化(count/random) |
| 中 | tags/categories 抽取 |
| 低 | 图廊 HTML figure 模式 |
18. 关键函数索引
| 函数 | 作用 |
|---|---|
extractWeChatMeta |
抽取标题/作者/封面回退 |
transformGalleryShortcodes |
gallery 短代码 → wikilinks |
selectGalleryImages |
画廊图片选择封装 |
preprocessContent |
行级语法 HTML 化 |
getWeChatArticleMeta |
获取抽取的 meta |
getMetadata |
最终上传元数据(含封面回填) |
MarkdownImage.tokenizer |
标准图片转 LocalImage token |
LocalFileRegex |
统一匹配图片语法 |
19. 总结
通过“标准化 → 抽取 → 预处理 → 渲染 → 上传”分层设计,确保各功能模块低耦合并可独立演进。当前设计已满足基础运营发布需求,后续可按优先级增强 YAML 解析、封面配置、多图策略与 gallery 表现力。
若需我继续实现 tags/categories 抽取或 gallery 参数扩展,请直接提出。
附录 A. 草稿箱清空功能
A.1 背景
运营过程中测试/多次上传会堆积大量“草稿”,需要一键清理能力,并具备安全保护与预览模式。
A.2 接口
| 方法 | 说明 |
|---|---|
clearAllDrafts(appid, { confirm, batchSize=20, retainLatest=0, dryRun=false }) |
批量列出并删除草稿;需 confirm:true 才执行实际删除 |
A.3 选项说明
| 选项 | 类型 | 说明 |
|---|---|---|
| confirm | boolean | 必须显式 true,否则抛错中止 |
| batchSize | number | 分页拉取条数(默认 20,受微信接口限制) |
| retainLatest | number | 保留最新 N 条(按接口返回顺序) |
| dryRun | boolean | 仅统计将删除的数量,不执行删除 |
A.4 返回结构
{
total: number, // 收集到的全部 media_id 数
skip: number, // 被保留的数量(= retainLatest 实际保留)
success: number, // 实际删除成功数(dryRun= true 时恒 0)
fail: number, // 删除失败数
fails: Array<{ media_id, status? , errcode?, errmsg?, text? }>,
dryRun: boolean
}
A.5 安全措施
confirm必须为 true。- 可设置
retainLatest防止误删全部。 dryRun先预览再正式执行。- 删除逐条执行,可在失败时保留失败列表审计。
A.6 未来增强
| 方向 | 说明 |
|---|---|
| 并发删除 | Promise pool 控制并发提升速度 |
| 过滤条件 | 按标题关键词/日期范围选择性删除 |
| 进度通知 | 分批实时进度 Notice / 状态栏 |
| UI 集成 | 命令面板 + 二次确认弹窗 |
| 时间排序校验 | 根据返回 update_time 明确排序而非假设 |
A.7 命令面板入口
已添加命令:清空微信草稿箱 (危险) (id: note-to-mp-clear-drafts)
流程:
- 首次 confirm:提示风险。
- 询问是否 dryRun(输入 y 仅预览)。
- 若非 dryRun,再询问保留最近 N 条。
- 二次 confirm 再次确认删除范围。
- 调用
clearAllDrafts(null, { confirm:true, dryRun, retainLatest })。
失败处理:捕获异常并 Notice 显示;控制台输出详细错误。
A.8 可视化操作面板 (Modal)
新增 ClearDraftsModal:提供表单而非多级 confirm/prompt。
表单字段:
- appid (可留空自动从当前文章 frontmatter 获取)
- 保留最近 N 条(number,默认 0)
- DryRun 复选框(默认勾选)
交互流程:
- 打开命令 → 弹出 Modal。
- 用户填写/确认参数,首次点“执行”→ 若为真实删除且非 dryRun,会再弹出 confirm。
- 结果以 JSON 形式写入下方
区域,便于复制。
- Notice 简要提示(DryRun 或 完成)。
错误处理:
- try/catch 包裹,失败写入 resultPre 文本 + Notice。
- run 按钮在执行期间 disabled,防止重复触发。
后续增强设想:
| 项目 | 说明 |
|---|---|
| 进度条 | 删除大批量时显示当前进度/总数 |
| 失败重试 | 针对 fails 列表单独重试按钮 |
| 过滤条件 | 增加标题关键词 / 日期起止输入 |
| 多账号选择 | 下拉列出已配置的 appid 列表 |
| 日志导出 | 一键复制 JSON 结果 |