update at 2025-09-27 08:38:05

This commit is contained in:
douboer
2025-09-27 08:38:05 +08:00
parent 50e8d61864
commit a49e389fe2
10 changed files with 1421 additions and 3 deletions

View File

@@ -0,0 +1,365 @@
# 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 |
| 易扩展 | 提供独立函数/接口减少耦合,如 `selectGalleryImages``extractWeChatMeta` |
| 默认封面配置 | 无任何图片候选时使用 `defaultCoverPic` (可配置) |
| 前置回退解析 | 若 metadataCache 缺失 frontmatter启用手动行级解析回退 |
| Gallery 块扩展 | 支持 `{{<gallery>}}` 块 + 内部 `figure src|link=` 解析 |
| 行级语法扩展 | `[fig .../]``||r`/`||g`/`||b`/`||y`/`||` 统一由 `applyCustomInlineBlocks` 处理 |
| 调试日志节流 | 输出当前文件路径与默认封面选用日志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图片路径修正、`||` 块、`[fig .../]` |
| 图片统一解析 | `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
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/`
### 7.4 Gallery 功能
- 短代码 Regex`{{<gallery dir="..."( figcaption="...")?/ >}}{{<load-photoswipe>}}`
- `_listGalleryImages`:读目录 + 过滤扩展 + 排序 + 截断。
- `selectGalleryImages`:对外通用(支持未来 random / prefix / includeDirInLink
- 输出:多行 `![[file]]`,并追加可选 `figcaption` div。
#### 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` + `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 形式写入下方 <pre> 区域,便于复制。
4. Notice 简要提示DryRun 或 完成)。
错误处理:
- try/catch 包裹,失败写入 resultPre 文本 + Notice。
- run 按钮在执行期间 disabled防止重复触发。
后续增强设想:
| 项目 | 说明 |
|------|------|
| 进度条 | 删除大批量时显示当前进度/总数 |
| 失败重试 | 针对 fails 列表单独重试按钮 |
| 过滤条件 | 增加标题关键词 / 日期起止输入 |
| 多账号选择 | 下拉列出已配置的 appid 列表 |
| 日志导出 | 一键复制 JSON 结果 |