Files
note2any/detaildesign.md
2025-09-22 14:58:45 +08:00

301 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# note-to-mp 设计文档 (Detail Design)
## 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` |
## 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 → ![[...]] 列表)
↓ marked.parse() (图片扩展 -> LocalImage token
↓ 生成 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()` 融合以补齐空缺字段。
### 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`
- 输出顺序按出现顺序。
## 8. 正则清单
| 场景 | 正则 | 说明 |
|------|------|------|
| frontmatter | `^---[\s\S]*?\n---` | 仅首段 |
| Wikilink 图片 | `!\[\[(.+?)\]\]` | 非贪婪 |
| Markdown 图片 | `!\[[^\]]*\]\(([^\n\r\)]+)\)` | 不跨行 |
| Gallery | `{{<gallery\s+dir=\"([^\"]+)\"(?:\s+figcaption=\"([^\"]*)\")?\s*\/>}}{{<load-photoswipe>}}` | 捕获 dir/caption |
| fig | `\[fig([^>]*?)\/]` | 题注 |
| 行块 | `\|\|r (.*)` 等 | 行级匹配 |
## 9. 错误与边界
| 情况 | 行为 |
|------|------|
| frontmatter 缺尾部 | 不解析,当普通正文 |
| 无 image 且正文无图 | `coverLink` 为空 |
| Gallery 目录缺失 | 原样保留短代码 |
| WebP 转换失败 | 记录日志,使用原文件 |
| 非支持图片扩展 | 忽略该文件 |
## 10. 性能
- 正则线性扫描 O(n)。
- Gallery 目录排序 O(m log m)。
- 可后续对 `_listGalleryImages` 结果加缓存。
## 11. 配置 & 常量
| 常量 | 说明 | 后续计划 |
|------|------|----------|
| `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 |
| 目录缓存 | 减少频繁 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 结果 |