Files
note2any/docs/detaildesign.md
2025-10-09 12:39:24 +08:00

17 KiB
Raw Permalink Blame History

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]]![alt](path/file.png) 最终统一进入 LocalImage 管线
元数据抽取 自动获取标题、作者、封面图(可回退)供后续上传逻辑使用
封面回退 未显式指定封面时,自动决策第一张图片
Gallery 支持 {{<gallery .../>}}{{<load-photoswipe>}} 转成图片 wikilinks 列表
预处理 在 Markdown 渲染前执行自定义语法转 HTML
易扩展 提供独立函数/接口减少耦合,如 selectGalleryImagesextractWeChatMeta
默认封面配置 无任何图片候选时使用 defaultCoverPic (可配置)
前置回退解析 若 metadataCache 缺失 frontmatter启用手动行级解析回退
Gallery 块扩展 支持 {{<gallery>}} 块 + 内部 `figure src
行级语法扩展 [fig .../] 与 `
调试日志节流 输出当前文件路径与默认封面选用日志3 秒内同路径不重复

3. 术语与定义

  • Wikilink 图片语法![[xxx.png]]
  • 标准 Markdown 图片![描述](path/to/xxx.png)
  • 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 图片统一转换

  • RegexLocalFileRegex = /^(?:!\[\[(.*?)\]\]|!\[[^\]]*\]\(([^\n\r\)]+)\))/
  • Markdown 标准图片 tokenizer
    1. 匹配 ![alt](path)matches[0]
    2. 取 basename → 构造 ![[basename]] 语义(内部直接建 LocalImage token不再二次正则回匹配
    3. 避免原先多余 -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/
  • 短代码 Regex{{<gallery dir="..."( figcaption="...")?/ >}}{{<load-photoswipe>}}
  • _listGalleryImages:读目录 + 过滤扩展 + 排序 + 截断。
  • selectGalleryImages:对外通用(支持未来 random / prefix / includeDirInLink
  • 输出:多行 ![[file]],并追加可选 figcaption div。

支持:

{{<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 生成超链接包装。
  • 当前: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 + SettingTab UI 输入框。
  • 迁移:移除硬编码常量 GALLERY_PRE_PATH / GALLERY_NUM_PIC
  • 默认值:defaultCoverPic = 'cover.png'(可为相对/绝对/网络 URL 或 wikilink 形式)。
  • 风险:用户提供的默认封面不存在 → 目前不校验,可后续增加存在性检查与 Notice。

7.8 封面候选决策链(更新版)

  1. 若已有 thumb_media_id(外部指定)→ 不再上传本地封面,保持 null。
  2. frontmatter cover/image非空字符串→ 使用其 basename 生成 wikilink。
  3. 正文扫描首个本地图片markdown / wikilink忽略 http/https
  4. 若正文无 → 使用 gallery 自动展开生成的第一张候选。
  5. 若仍无 → 使用 defaultCoverPic(若配置)。
  6. 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"
---
前言
![首图](img/b-first.png)
![[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 安全措施

  1. confirm 必须为 true。
  2. 可设置 retainLatest 防止误删全部。
  3. dryRun 先预览再正式执行。
  4. 删除逐条执行,可在失败时保留失败列表审计。

A.6 未来增强

方向 说明
并发删除 Promise pool 控制并发提升速度
过滤条件 按标题关键词/日期范围选择性删除
进度通知 分批实时进度 Notice / 状态栏
UI 集成 命令面板 + 二次确认弹窗
时间排序校验 根据返回 update_time 明确排序而非假设

A.7 命令面板入口

已添加命令:清空微信草稿箱 (危险) (id: note-to-mp-clear-drafts)

流程:

  1. 首次 confirm提示风险。
  2. 询问是否 dryRun输入 y 仅预览)。
  3. 若非 dryRun再询问保留最近 N 条。
  4. 二次 confirm 再次确认删除范围。
  5. 调用 clearAllDrafts(null, { confirm:true, dryRun, retainLatest })

失败处理:捕获异常并 Notice 显示;控制台输出详细错误。

A.8 可视化操作面板 (Modal)

新增 ClearDraftsModal:提供表单而非多级 confirm/prompt。

表单字段:

  • appid (可留空自动从当前文章 frontmatter 获取)
  • 保留最近 N 条number默认 0
  • DryRun 复选框(默认勾选)

交互流程:

  1. 打开命令 → 弹出 Modal。
  2. 用户填写/确认参数,首次点“执行”→ 若为真实删除且非 dryRun会再弹出 confirm。
  3. 结果以 JSON 形式写入下方
     区域,便于复制。
  4. Notice 简要提示DryRun 或 完成)。

错误处理:

  • try/catch 包裹,失败写入 resultPre 文本 + Notice。
  • run 按钮在执行期间 disabled防止重复触发。

后续增强设想:

项目 说明
进度条 删除大批量时显示当前进度/总数
失败重试 针对 fails 列表单独重试按钮
过滤条件 增加标题关键词 / 日期起止输入
多账号选择 下拉列出已配置的 appid 列表
日志导出 一键复制 JSON 结果