diff --git a/architecture.md b/architecture.md
new file mode 100644
index 0000000..6fea6f7
--- /dev/null
+++ b/architecture.md
@@ -0,0 +1,131 @@
+# Architecture Overview
+
+> 本文档从整体视角拆分自 `detaildesign.md`,聚焦架构分层、核心模块职责与交互关系。补充细粒度时序与图片处理细节请参见:
+> - `image-pipeline.md`
+> - `render-service-blueprint.md`
+> - `diagrams.md`
+
+## 1. 分层结构
+```
+UI / Interaction (NotePreview)
+ ├─ Toolbar (复制/上传/发草稿/图片集/导出/批量)
+ └─ 状态维护 (当前笔记、主题、代码高亮、公众号 AppId)
+
+Rendering & Composition (ArticleRender + MarkedParser + Extensions)
+ ├─ Markdown 预处理 (frontmatter 剥离 / 可选图片语法转换)
+ ├─ 语法扩展 (LocalFile: wikilink 图片、文件嵌入、Excalidraw、SVG)
+ ├─ 延迟元素缓存 (Mermaid / Excalidraw)
+ └─ 样式注入 (主题 + 代码高亮 + 自定义 CSS)
+
+Assets & Settings Layer
+ ├─ AssetsManager (themes, highlights, customCSS)
+ └─ NMPSettings (frontmatter key 映射 / 功能开关 / 微信配置)
+
+Resource & Media Layer
+ ├─ LocalImageManager (图片登记 / 上传 / 替换 / base64 嵌入)
+ ├─ Image Conversion (WebP -> JPG wasm)
+ └─ html-to-image (Mermaid/Excalidraw 转 PNG)
+
+WeChat Integration Layer
+ ├─ Token 代理 (wxGetToken)
+ ├─ 草稿创建 (wxAddDraft / wxAddDraftImages)
+ ├─ 素材批量获取 (wxBatchGetMaterial)
+ └─ 上传图片 (wxUploadImage)
+
+Utilities & Helpers
+ ├─ debounce, applyCSS, waitForLayoutReady
+ └─ CardDataManager (代码卡片序列化/恢复)
+```
+
+## 2. 核心模块职责矩阵
+| 模块 | 关键职责 | 输入 | 输出 | 失败模式 |
+|------|----------|------|------|----------|
+| NotePreview | 交互、调度、批处理 | 用户操作 / 活动文件 | 渲染触发、发布调用 | 文件为空 / 未配置公众号 |
+| ArticleRender | 渲染 + 元数据 + 发布协调 | Markdown + Settings | HTML + DraftArticle | 解析错误 / token 失败 |
+| MarkedParser + Extensions | Markdown Token 化扩展 | 预处理文本 | Token 树 -> HTML 片段 | 不支持语法 / 路径缺失 |
+| LocalFile | Wikilink/嵌入/Excalidraw/SVG | Token.raw | 标准 HTML / 占位 | 文件不存在 / AuthKey 缺失 |
+| LocalImageManager | 图片生命周期 | 图片引用集合 | 上传后的 URL 替换结果 | 上传失败 / 转码失败 |
+| AssetsManager | 主题 & 高亮加载 | 文件系统 | CSS 文本 | 主题缺失 |
+| NMPSettings | 全局配置 | 存储数据 | 配置实例 | AuthKey 无效 |
+| WeChat API | 外部接口封装 | 请求结构 | 响应 JSON | HTTP 非 200 / errcode |
+| CardDataManager | 代码块序列化 | HTML fragment | 可复制安全内容 | 序列化不匹配 |
+
+## 3. 关键交互流程简述
+### 3.1 渲染触发
+1. 用户打开/切换文件 → `file-open` 事件。
+2. `NotePreview.update()` → 清理旧图片缓存 → `ArticleRender.renderMarkdown()`。
+3. 读取 Markdown → (frontmatter 剥离 + 可选图片语法转换) → Marked 解析扩展 → HTML section → 延迟元素二次渲染。
+4. 更新 UI 下拉:主题 / 高亮 / 公众号。
+
+### 3.2 上传图片
+1. 用户点击“上传图片” → `ArticleRender.uploadImages()`。
+2. 获取 token → 缓存元素转图片 → 本地/远程图片逐步上传 → DOM src 替换 → 复制 HTML 到剪贴板。
+
+### 3.3 发布草稿
+1. “发草稿” → token → 缓存元素转图片。
+2. 上传本地 & 远程图片 → 封面决策(thumb_media_id/frontmatter/正文首图/默认素材)→ 构造 `DraftArticle`。
+3. 调用 `wxAddDraft` → 返回 media_id。
+
+### 3.4 发布图片集 (newspic)
+与草稿类似,但构造 `DraftImages`,content 使用纯文本,图片列表为 `media_id` 数组。
+
+## 4. 元数据 & 封面策略抽象
+逻辑函数(伪):
+```ts
+function resolveCover(originalMarkdown: string, fmCover?: string, thumbMediaId?: string): string | null {
+ if (thumbMediaId) return null; // 已有 media_id 不再上传本地封面
+ if (fmCover) return fmCover; // frontmatter 指定
+ const body = stripFrontmatter(originalMarkdown);
+ const candidates = collectImageOrder(body); // wikilink + markdown
+ return candidates.length ? `![[${candidates[0]}]]` : null;
+}
+```
+
+## 5. 图片收集策略统一要点
+- Wikilink:解析时即登记(路径 → vault file)。
+- Markdown 图片:可配置预处理转 Wikilink,以复用同一逻辑(避免重复正则后处理)。
+- DOM 补偿策略(如后续新渲染管线未登记图片):可在上传前扫描 `
` 回填缺失项。
+
+## 6. 错误处理分层
+| 层 | 代表错误 | 处理策略 |
+|----|----------|----------|
+| 输入层 | 没有活动文件 | 静默返回 / UI 提示 |
+| 渲染层 | Marked 解析异常 | try/catch 显示“渲染失败” HTML |
+| 媒体层 | 图片上传失败 | Notice + 继续其他图片 |
+| 转码层 | wasm 未加载 | 降级:保持 webp 原样尝试上传 |
+| 发布层 | token 获取失败 | 抛异常终止流程 |
+| API 层 | errcode != 0 | 聚合错误信息 + 建议手动复制 |
+
+## 7. 性能关注点
+| 项目 | 当前 | 潜在优化 |
+|------|------|----------|
+| 图片上传 | 串行 | 并发队列 + 重试退避 |
+| Mermaid/Excalidraw | 前台转 PNG | 结果缓存 / worker 线程 |
+| 主题加载 | 每次刷新全部组合 | 按需拼接 + 缓存 hash |
+| Markdown 解析 | 单线程 | 增量渲染(监听 diff) |
+
+## 8. 可扩展挂载点 (未来)
+| Hook | 说明 | 预期参数 |
+|------|------|----------|
+| beforeRender | Markdown 转 HTML 前 | { markdown, file } |
+| afterRender | HTML 完成但未上传 | { html, metadata } |
+| beforeUploadImages | 上传前 | { images[] } |
+| afterUploadImages | 上传后 | { mapping } |
+| beforePublish | 调用 wxAddDraft 前 | { draft } |
+| afterPublish | 成功返回 | { media_id } |
+
+## 9. 风险摘录
+- 多管线并存(旧 ArticleRender vs 新 RenderService 占位)易导致图片未登记 → 需统一抽象。
+- 自动封面选取对远程图片/非图片扩展尚无过滤完备策略。
+- 正则预处理与 Marked tokenizer 逻辑耦合度高,重构时需单测保护。
+
+## 10. 索引 / 交叉引用
+| 文档 | 内容概述 |
+|------|----------|
+| detaildesign.md | 全量深度设计描述 |
+| image-pipeline.md | 图片解析、上传、封面、正则清单 |
+| render-service-blueprint.md | 新一代渲染服务设计计划 |
+| diagrams.md | 架构 / 时序 / 类关系图 (Mermaid) |
+
+---
+> 若修改架构(新增层或跨层依赖),需同步更新本文件与 `diagrams.md`。
diff --git a/detaildesign.md b/detaildesign.md
index a5fb823..a4ba6ef 100644
--- a/detaildesign.md
+++ b/detaildesign.md
@@ -1,5 +1,12 @@
# note-to-mp 设计文档 (Detail Design)
+> 拆分文档索引:
+> - 架构总览:`architecture.md`
+> - 图片管线:`image-pipeline.md`
+> - 渲染服务蓝图:`render-service-blueprint.md`
+> - 图示 (Mermaid):`diagrams.md`
+> 本文件保留全量细节,增量演进请同步上述子文档。
+
## 1. 背景
为满足从 Obsidian 笔记到微信公众号文章的高质量发布需求,插件需要:
- 支持多种图片书写形式(Wikilink 与标准 Markdown)。
diff --git a/diagrams.md b/diagrams.md
new file mode 100644
index 0000000..84de5d6
--- /dev/null
+++ b/diagrams.md
@@ -0,0 +1,155 @@
+# Diagrams (Mermaid)
+
+> 动态架构与主要交互可视化。与文字说明对应:`architecture.md` / `image-pipeline.md` / `render-service-blueprint.md`。
+
+## 1. 模块类图 (当前实现概览)
+```mermaid
+classDiagram
+ class NotePreview {
+ +renderMarkdown()
+ +uploadImages()
+ +postArticle()
+ +postImages()
+ +exportHTML()
+ }
+ class ArticleRender {
+ +renderMarkdown(file)
+ +getMetadata()
+ +uploadImages(appid)
+ +postArticle(appid,cover?)
+ +postImages(appid)
+ +exportHTML()
+ }
+ class LocalImageManager {
+ +setImage(path,info)
+ +uploadLocalImage(token,vault)
+ +uploadRemoteImage(root,token)
+ +replaceImages(root)
+ }
+ class LocalFile {
+ +markedExtension()
+ }
+ class AssetsManager {
+ +loadAssets()
+ +getTheme(name)
+ +getHighlight(name)
+ }
+ class NMPSettings {
+ +wxInfo
+ +authKey
+ +enableMarkdownImageToWikilink
+ }
+ class WeChatAPI {
+ +wxGetToken()
+ +wxAddDraft()
+ +wxUploadImage()
+ }
+
+ NotePreview --> ArticleRender
+ ArticleRender --> LocalImageManager
+ ArticleRender --> AssetsManager
+ ArticleRender --> NMPSettings
+ ArticleRender --> WeChatAPI
+ ArticleRender --> LocalFile
+ LocalFile --> LocalImageManager
+ NotePreview --> NMPSettings
+ NotePreview --> AssetsManager
+```
+
+## 2. 发布草稿时序图
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant NP as NotePreview
+ participant AR as ArticleRender
+ participant WX as WeChatAPI
+ participant LIM as LocalImageManager
+
+ U->>NP: 点击 发草稿
+ NP->>AR: postArticle(appid)
+ AR->>WX: wxGetToken(authKey, appid)
+ WX-->>AR: token
+ AR->>AR: cachedElementsToImages()
+ AR->>LIM: uploadLocalImage(token)
+ LIM-->>AR: local media_id(s)
+ AR->>LIM: uploadRemoteImage(token)
+ LIM-->>AR: remote media_id(s)
+ AR->>LIM: replaceImages()
+ AR->>AR: resolve cover (frontmatter / first image / default)
+ AR->>WX: wxAddDraft(draft JSON)
+ WX-->>AR: media_id | err
+ AR-->>NP: 结果
+ NP-->>U: 成功 / 失败提示
+```
+
+## 3. 图片上传流程图
+```mermaid
+graph TD
+ A[Start UploadImages] --> B{AuthKey/AppId?}
+ B -- No --> Z[Throw Error]
+ B -- Yes --> C[Get Token]
+ C --> D[cachedElementsToImages]
+ D --> E[uploadLocalImage]
+ E --> F[uploadRemoteImage]
+ F --> G[replaceImages]
+ G --> H[Copy HTML to Clipboard]
+ H --> I[End]
+```
+
+## 4. 自动封面推断逻辑
+```mermaid
+graph TD
+ A[Need Cover?] -->|No| Z[Skip]
+ A -->|Yes| B[Strip Frontmatter]
+ B --> C[Scan Markdown Images]
+ B --> D[Scan Wikilink Images]
+ C --> E[Merge Candidates]
+ D --> E[Merge Candidates]
+ E --> F{Any Candidate?}
+ F -- No --> Z[Cover stays empty]
+ F -- Yes --> G[Sort by index]
+ G --> H[Select first -> cover wikilink]
+```
+
+## 5. 未来 RenderService Pipeline 图
+```mermaid
+graph LR
+ L[Loader] --> FM[Frontmatter]
+ FM --> PP[Preprocessors]
+ PP --> P[Parser]
+ P --> TR[Transformers]
+ TR --> RI[ResourceIndex]
+ RI --> R[Renderer]
+ R --> PO[Postprocessors]
+ PO --> EX[Exporters]
+```
+
+## 6. 并发上传示意 (未来优化)
+```mermaid
+graph TD
+ A[images[]] --> B[Partition into tasks]
+ B --> C[Promise Pool (N=4)]
+ C --> D[Upload Task]
+ D --> E{Success?}
+ E -- No --> R[Retry with backoff]
+ E -- Yes --> F[Collect media_id]
+ R --> C
+ F --> G[All Done]
+```
+
+## 7. 状态机概览 (发布按钮)
+```mermaid
+stateDiagram-v2
+ [*] --> Idle
+ Idle --> Uploading : 点击 上传/发布
+ Uploading --> Publishing : 草稿模式
+ Uploading --> Completed : 仅上传
+ Publishing --> Completed : 响应成功
+ Publishing --> Failed : 接口错误
+ Uploading --> Failed : 资源错误
+ Failed --> Idle : 用户重试
+ Completed --> Idle : 新文件切换
+```
+
+---
+需要我将这些图嵌入到 README 的一个“开发者”章节吗?可以继续提出。
diff --git a/image-pipeline.md b/image-pipeline.md
new file mode 100644
index 0000000..7a52ce0
--- /dev/null
+++ b/image-pipeline.md
@@ -0,0 +1,158 @@
+# Image Pipeline & Cover Strategy
+
+> 对应代码核心:`src/markdown/local-file.ts`, `src/article-render.ts` 以及相关预处理逻辑。
+> 若需总体结构参见 `architecture.md`,更广设计参见 `detaildesign.md`。
+
+## 1. 目标
+- 统一处理所有图片来源:Wikilink、标准 Markdown、远程 URL、Base64、生成图 (Mermaid/Excalidraw/SVG)。
+- 保证上传后 HTML 使用微信可访问的正式 URL。
+- 自动封面选取:未指定时从正文首图推断。
+- WebP → JPG 转换保障兼容性。
+
+## 2. 图片来源与归一化
+| 来源 | 输入示例 | 归一化策略 | 备注 |
+|------|----------|-----------|------|
+| Wikilink | `![[foo.png]]` / `![[foo.png|120x80]]` | Marked 扩展直接生成 `
` 并登记 | 支持尺寸 / 位置扩展语法 |
+| Markdown | `` | (可选) 预处理转换成 `![[foo.png]]` | 依赖设置:`enableMarkdownImageToWikilink` |
+| 远程 URL | `
` | 解析为远程上传任务 | 跳过已是微信域名 |
+| Base64 | `
` | 解析成 Blob 上传 | 生成虚拟 id 构造文件名 |
+| Mermaid | 源码块 → SVG/DOM | 转 PNG (html-to-image) / 留 SVG | 替换占位容器 |
+| Excalidraw | `![[diagram.excalidraw]]` | 远程接口取 SVG 或转 PNG | 需 AuthKey |
+| SVG 文件 | `![[icon.svg]]` | 内联 SVG 内容 | 直接嵌入,不上传 |
+
+## 3. 关键数据结构
+```ts
+interface ImageInfo {
+ resUrl: string; // 初始占位或本地资源 URL (vault path 映射)
+ filePath: string; // Vault 内实际文件路径
+ url: string | null; // 上传后微信返回 URL
+ media_id: string | null; // 上传后素材 ID(type=image 时)
+}
+```
+管理容器:`Map`,键值为 `resUrl`(初始唯一标识)。
+
+## 4. 收集阶段
+1. Marked 扩展 (`LocalFile.markedExtension`) 在 token 遍历时识别 LocalImage。
+2. 根据 wikilink / 尺寸参数解析出真实 vault 文件路径:`assetsManager.getResourcePath()`。
+3. 调用 `LocalImageManager.setImage(res.resUrl, info)` 建档。
+4. Markdown 图片(标准语法)若转换启用,提前转成 wikilink 进入同一逻辑;否则需未来扩展 tokenizer 直接登记。
+
+## 5. 上传阶段
+顺序:
+```mermaid
+graph TD
+A[准备 token] --> B[cachedElementsToImages]
+B --> C[uploadLocalImage]
+C --> D[uploadRemoteImage]
+D --> E[replaceImages]
+E --> F[输出 HTML / 复制 / 发布]
+```
+
+### 5.1 本地图片 `uploadLocalImage`
+流程:
+- 遍历登记表,读取 vault 文件二进制。
+- 若扩展名 `.webp` → wasm (`PrepareImageLib`) 转 JPG。
+- 调用 `wxUploadImage(blob, filename, token, type)`。
+- 更新 `ImageInfo.url` / `media_id`。
+- 失败:`Notice + console.error`,继续后续项。
+
+### 5.2 远程图片 `uploadRemoteImage`
+匹配 `
`:
+- 跳过已在微信域名(`mmbiz.qpic.cn`)。
+- 下载 → Blob → (webp 转换) → 上传。
+- 更新 Map,键为原始 src。
+Base64:
+- 解析 data URI → 生成 Blob/扩展名 → 上传。
+
+### 5.3 替换阶段 `replaceImages`
+- 遍历 DOM `
` → 查询 Map → 使用上传后 `url` 覆盖 `src`。
+- 未登记:输出警告(潜在渲染管线遗漏)。
+
+## 6. 自动封面策略
+触发点:`getMetadata()` 中,若 frontmatter 未给出封面且无 `thumb_media_id`。步骤:
+1. 使用 `originalMarkdown`(含 frontmatter 原始文本拷贝)。
+2. 去除 frontmatter 块。
+3. 分别用正则匹配:
+ - Markdown:`/!\[[^\]]*\]\(([^)\s]+)\)/g`
+ - Wikilink:`/!\[\[(.+?)\]\]/g`
+4. 过滤远程 URL(仅保留本地引用或无协议路径)。
+5. 生成候选 `{ idx, basename }` 列表,按出现位置排序。
+6. 第一项 → `![[basename]]` 写入 `res.cover`。
+
+Edge Cases:
+| 情况 | 处理 |
+|------|------|
+| 所有图片都是 http(s) | 不作为封面候选 |
+| 文件名带 query/hash | 使用前切分去除 `?` `#` |
+| markdown 图片后缀非图片 | 跳过 |
+| 重复图片 | 以最先出现的为准 |
+
+## 7. 正则索引
+| 目的 | 正则 | 描述 |
+|------|------|------|
+| Frontmatter 删除 | `/^(---)$.+?^(---)$.+?/ims` | 首段 YAML |
+| Markdown 图片 | `/!\[[^\]]*\]\(([^)\s]+)\)/g` | 捕获路径 group1 |
+| Wikilink 图片 | `/!\[\[(.+?)\]\]/g` | 捕获内部资源 |
+| WebP 检测 | `/\.webp$/i` | 扩展判断 |
+| Base64 前缀 | `^data:image/` | 判断内嵌图 |
+
+## 8. 失败与恢复策略
+| 环节 | 失败示例 | 策略 |
+|------|----------|------|
+| wasm 初始化 | 网络慢 / 未加载 | 继续使用原 webp 上传(失败概率增加) |
+| 本地文件找不到 | 路径错误 / 移动 | 在控制台警告,图片跳过 |
+| 上传 403/401 | token 失效 | 抛异常终止流程(需重新获取 token) |
+| 单图 errcode !=0 | 大小/格式不合规 | Notice 提示 + 继续其他 |
+| 替换未命中 | DOM 没登记 | 记录警告,建议加入补偿扫描 |
+
+## 9. 性能优化方向
+| 问题 | 现状 | 改进 |
+|------|------|------|
+| 上传串行 | 顺序 await | 并发池 (N=3~5) + 重试退避 |
+| WebP 转换阻塞 | 单线程 | 预热 wasm + 批量并行 |
+| 远程下载串行 | 每图 await | 统一收集 Promise.all 控制并发 |
+| 首图扫描重复 | 重新正则两次 | 合并一次统一匹配 pipeline |
+
+## 10. 与发布流程的接口点
+| 函数 | 上下游 | 说明 |
+|------|--------|------|
+| `cachedElementsToImages()` | 延迟图形 → 图片 | 在上传前确保所有图形成为
可被收集 |
+| `uploadLocalImage()` | 发布前 | 更新本地图片 URL/media_id |
+| `uploadRemoteImage()` | 发布前 | 远程/内嵌资源统一化 |
+| `replaceImages()` | 发布前 | DOM HTML 成为最终状态 |
+| `getArticleContent()` | 发布 / 复制 / 导出 | 输出含最终图片 URL 的 HTML |
+
+## 11. 未来扩展
+| 需求 | 设想 |
+|------|------|
+| CDN 直传 | 微信前置 → 自建或 OSS 中转 |
+| 图片压缩 | 上传前可选压缩 (canvas/wasm) |
+| 失效重传 | 保存上传映射 + 校验缺失再补传 |
+| 分块上传 | 大图分片(若接口支持) |
+| 图片校验 | MD5 去重避免重复上传 |
+
+## 12. 快速检查清单 (Debug Checklist)
+- 图片是否在 `LocalImageManager.images` Map 中?
+- 是否执行了 `cachedElementsToImages()`(Mermaid/Excalidraw)?
+- 上传后 `media_id` 是否为空?(格式/大小不合规)
+- DOM 替换后 `
` 是否为微信域?
+- 自动封面未生效?检查:frontmatter cover / 有无本地首图 / 正则是否截获远程 URL。
+
+## 13. 示例
+输入 Markdown:
+```
+---
+title: 示例
+---
+
+正文...
+![[b-second.jpg]]
+```
+处理:
+1. frontmatter 去除后扫描:首个匹配为 Markdown 图片 a.png → 自动封面 `![[a.png]]`。
+2. a.png 转 wikilink (开启转换时) → 登记;b-second.jpg 登记。
+3. 上传顺序:a.png → b-second.jpg。
+4. 最终 HTML `
`。
+
+---
+如需补充“并发上传样例代码”或“封面过滤策略扩展”,请提出。
diff --git a/render-service-blueprint.md b/render-service-blueprint.md
new file mode 100644
index 0000000..5b65994
--- /dev/null
+++ b/render-service-blueprint.md
@@ -0,0 +1,208 @@
+# RenderService Blueprint
+
+> 目的:规划下一代渲染服务以替换(或包装)现有 `ArticleRender` + `MarkedParser` 组合,降低耦合,支持插件化与增量演进。
+> 关联文档:`architecture.md`, `image-pipeline.md`, `detaildesign.md`。
+
+## 1. 现状问题 (Current Pain Points)
+| 问题 | 说明 | 影响 |
+|------|------|------|
+| 逻辑集中 | `ArticleRender` 同时负责读取、解析、样式拼接、延迟元素、上传协调 | 难测试 / 难替换子环节 |
+| 隐式状态 | `originalMarkdown`, `cachedElements` 内部字段耦合流程顺序 | 外部无法复用或二次解析 |
+| 图片收集耦合 Marked 扩展 | 依赖前置 wikilink 转换 | 新语法添加需改内核 |
+| 缺少中间 IR | 无法对 Token/AST 做多阶段转换 | 功能扩展(如统计、Lint)困难 |
+| 发布耦合渲染 | 上传/发布 API 与渲染混合 | 交互模式无法独立测试 |
+| 单线程串行 | 图形生成、图片上传均串行 | 性能受限 |
+
+## 2. 设计目标 (Design Goals)
+| 目标 | 描述 | 衡量指标 |
+|------|------|----------|
+| 分层解耦 | 渲染、转换、资源收集、发布分离 | 新增图片语法无需改核心类 |
+| 插件式中间件 | 可在 parse → transform → render 各阶段注入 | 插件注册 API 清晰 |
+| 可测试 | 纯函数/无副作用阶段隔离 | 单元测试覆盖率提升 |
+| IR 标准化 | 生成统一 AST / 节点类型 | 后续可序列化/缓存 |
+| 并发能力 | 图形/上传并发控制 | 大图文耗时下降 |
+| 可观测性 | 事件 & 钩子 | before/after metrics 输出 |
+
+## 3. 拟议分层 (Proposed Layers)
+```
+RenderPipeline
+ ├─ Loader (读取源 Markdown / 资源定位)
+ ├─ Frontmatter (解析 + Meta Store)
+ ├─ Preprocessors[] (文本级转换:图片语法、短代码、宏)
+ ├─ Parser (Markdown -> AST 统一节点树)
+ ├─ Transformers[] (AST 级:节点重写、图片规范化、封面推断)
+ ├─ ResourceIndex (图片/图形/嵌入引用索引构建)
+ ├─ Renderer (AST -> HTML Fragment)
+ ├─ Postprocessors[] (HTML 级:内联样式、链接修复、统计)
+ └─ Exporters (HTML/Text/JSON/WeChatDraft)
+```
+
+## 4. 关键接口 (Core Interfaces - Draft)
+```ts
+interface RenderContext {
+ file: TFile;
+ raw: string; // 原始 Markdown
+ frontmatter?: Record;
+ meta: WeChatArticleMeta; // 标题/作者/封面等
+ ast?: MdAstRoot; // 标准化 AST
+ resources: ResourceIndex; // 图片/图形等
+ html?: string; // 渲染产物
+ diagnostics: Diagnostic[];
+ flags: Record;
+}
+
+interface PipelineStage {
+ name: string;
+ run(ctx: RenderContext): Promise | void;
+ order?: number; // 可选执行排序
+}
+
+interface ResourceIndex {
+ images: ImageCandidate[];
+ diagrams: DiagramCandidate[];
+ embeds: EmbedRef[];
+}
+
+interface ImageCandidate {
+ id: string; // 稳定标识
+ kind: 'local'|'remote'|'base64'|'generated';
+ original: string; // 原始文本或路径
+ basename?: string;
+ vaultPath?: string;
+ position: number; // 在 raw 中的位置,用于封面决策
+ meta?: Record; // 尺寸/对齐等
+}
+```
+
+## 5. 执行模型 (Execution Model)
+```ts
+class RenderService {
+ private stages: PipelineStage[] = [];
+ use(stage: PipelineStage) { this.stages.push(stage); }
+ async render(file: TFile): Promise {
+ const ctx = createInitialContext(file);
+ for (const s of sortByOrder(this.stages)) {
+ try { await s.run(ctx); } catch (e) { ctx.diagnostics.push({stage: s.name, error: e}); if (isFatal(e)) break; }
+ }
+ return ctx;
+ }
+}
+```
+
+## 6. 示例阶段实现草稿
+| Stage | 说明 | 是否必须 |
+|-------|------|----------|
+| loader | 读取文件与 raw | 是 |
+| frontmatter | 解析 YAML -> ctx.frontmatter / ctx.meta 预填 | 是 |
+| markdownImageCompat | Markdown 图片 -> 统一节点形式 | 可选 |
+| shortcodeGallery | 解析 gallery → 多个 image 节点 | 可选 |
+| parser | Marked/Remark → AST | 是 |
+| imageCollect | AST 遍历收集图片,写入 ctx.resources.images | 是 |
+| coverInfer | 若无封面,从 images 按 position 选首图 | 可选 |
+| renderHTML | AST -> HTML 字符串 | 是 |
+| styleInline | 合成主题/高亮,自定义 CSS 注入 | 可选 |
+| finalize | 产物整理 / hash / 缓存存储 | 可选 |
+
+## 7. AST 规范 (Simplified)
+```ts
+type MdNode = Paragraph | Heading | Code | Image | Link | List | Blockquote | Html | ThematicBreak | Container;
+interface Image { type:'image'; id:string; alt:string; url:string; title?:string; meta?:{width?:number;height?:number;align?:string;} }
+```
+可基于 remark/unified 生态替换当前 marked,或构造最小抽象后桥接。
+
+## 8. 事件 & Hook 设计
+```ts
+interface RenderHooks {
+ on(stage: string, fn: (ctx: RenderContext) => void): void;
+ emit(stage: string, ctx: RenderContext): void;
+}
+```
+预定义事件:`before:stageName` / `after:stageName` / `error:stageName`。
+
+## 9. 并发策略
+- 图片上传与图形生成移出核心渲染,进入 `PublisherService`。
+- `PublisherService.publish(ctx, opts)`:接受 RenderContext,内部:
+ 1. 对 `ctx.resources.images` 并发池(PromisePool)上传。
+ 2. 替换 `ctx.html` 中引用。
+ 3. 构建 `DraftArticle` / `DraftImages`。
+
+## 10. 迁移计划 (Phased Migration)
+| 阶段 | 内容 | 验证点 |
+|------|------|--------|
+| P0 | 搭建 RenderService 空管线 + 复用旧 ArticleRender 结果 | 构建无回归 |
+| P1 | 拆出 loader/frontmatter/parser/coverInfer | 旧行为对比 snapshot |
+| P2 | 新 AST + imageCollect,弃用 wikilink 转换 hack | 图片计数稳定 |
+| P3 | 发布逻辑重构到 PublisherService | 草稿发布一致 |
+| P4 | Hook/插件系统开放 | 外部扩展示例 |
+| P5 | 并发上传 + 缓存 | 性能基线下降 |
+
+## 11. 回滚策略
+- 每阶段保留配置开关:`useLegacyRenderer`。
+- 出现渲染差异:比较 ctx.html 与 legacy.html diff 提示。
+- 发布失败回退:走旧 `ArticleRender.postArticle`。
+
+## 12. 测试策略
+| 测试 | 说明 |
+|------|------|
+| Snapshot 渲染 | 同一 Markdown 输入旧 vs 新 HTML 对比 |
+| AST 结构 | 断言图片/标题节点数量与顺序 |
+| 封面选择 | 多组合(frontmatter + 混合图片顺序) |
+| Hook 调用 | 注册 mock 钩子计次 |
+| 并发上传 | 人工注入延迟 → 顺序与最终替换正确 |
+
+## 13. 指标与可观测
+- ctx.diagnostics 数量与类型统计。
+- 阶段耗时:`performance.now()` 差值注入 ctx.flags。
+- 上传耗时 / 失败率。
+
+## 14. 安全考量
+- Stage 插件沙箱:限制访问仅上下文公开字段。
+- 阶段超时(可选):超过阈值标记 warning。
+- HTML 输出再次 sanitize。
+
+## 15. API 草案
+```ts
+const rs = new RenderService();
+rs.use(loader());
+rs.use(frontmatter());
+rs.use(markdownImageCompat({ enable: settings.enableMarkdownImageToWikilink }));
+rs.use(parser({ engine:'remark' }));
+rs.use(imageCollect());
+rs.use(coverInfer());
+rs.use(renderHTML({ themeProvider, highlightProvider }));
+rs.use(styleInline());
+
+const ctx = await rs.render(activeFile);
+const draft = await publisher.publish(ctx, { type:'article', coverMode:'auto' });
+```
+
+## 16. 依赖与选型比较
+| 方向 | 方案 A | 方案 B | 选择建议 |
+|------|-------|-------|----------|
+| Markdown AST | remark/unified | 继续 marked + 自建 AST 映射 | remark 更标准;初期可混合 |
+| Hook 实现 | 事件总线 (mitt) | 简单数组回调 | 先内建数组回调,后期引入库 |
+| 并发控制 | p-limit | 自写 PromisePool | p-limit 简洁可靠 |
+
+## 17. 风险 & 缓解
+| 风险 | 缓解 |
+|------|------|
+| AST 转换差异导致样式变化 | Snapshot + 逐阶段灰度 |
+| 性能倒退 | 阶段耗时基线监控;必要时跳过冗余阶段 |
+| 插件滥用 Hook | 权限白名单 / 文档规范 |
+| 并发上传触发限流 | 设置最大并发 + 429 重试 |
+
+## 18. 成功判定 (Success Criteria)
+- 同一输入 50 篇示例笔记,新旧 HTML 差异行 <= 2%(忽略动态 id)。
+- 图片收集数量一致,封面判定一致率 100%。
+- 20+ 图片大文档总发布耗时下降 ≥25%。
+- 可插拔 demo:增加一个统计字数的 Stage 无需改核心。
+
+## 19. 下一步行动 (Action Items)
+1. 建立 `RenderService` 目录与最小类骨架。
+2. 搬迁读取/frontmatter/封面逻辑并保留旧 API 外壳。
+3. 引入 AST(先轻量:仅 Image/Heading/Paragraph)。
+4. Snapshot 测试脚手架。
+5. 性能基线脚本:统计渲染 + 上传耗时。
+
+---
+后续若需要,我可以直接生成骨架代码与测试样例,请指示。
diff --git a/src/article-render.ts b/src/article-render.ts
index b702fd2..1f75378 100644
--- a/src/article-render.ts
+++ b/src/article-render.ts
@@ -54,6 +54,7 @@ export class ArticleRender implements MDRendererCallback {
markedParser: MarkedParser;
cachedElements: Map = new Map();
debouncedRenderMarkdown: (...args: any[]) => void;
+ originalMarkdown: string | null = null; // 保存去除前处理前的原始 Markdown
constructor(app: App, itemView: ItemView, styleEl: HTMLElement, articleDiv: HTMLDivElement) {
this.app = app;
@@ -162,10 +163,28 @@ export class ArticleRender implements MDRendererCallback {
else {
md = '没有可渲染的笔记或文件不支持渲染';
}
+ this.originalMarkdown = md; // 保存原始内容(含 frontmatter)供封面/摘要自动提取
if (md.startsWith('---')) {
md = md.replace(FRONT_MATTER_REGEX, '');
}
+ // 将标准 markdown 图片语法转为 wikilink 语法,便于现有 LocalImageManager 识别
+ if (this.settings.enableMarkdownImageToWikilink) {
+ // 匹配 ;不跨行;忽略包含空格的 URL 末尾注释
+ // 捕获路径和文件名,文件名取最后一段
+ md = md.replace(/!\[[^\]]*\]\(([^)\s]+)\)/g, (full, p1) => {
+ try {
+ // 去掉可能的 query/hash
+ const clean = p1.split('#')[0].split('?')[0];
+ const filename = clean.split('/').pop();
+ if (!filename) return full; // 无法解析
+ // 仅当是常见图片扩展时才替换
+ if (!filename.match(/\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i)) return full;
+ return `![[${filename}]]`;
+ } catch { return full; }
+ });
+ }
+
this.articleHTML = await this.markedParser.parse(md);
this.setStyle(this.getCSS());
this.setArticle(this.articleHTML);
@@ -233,11 +252,14 @@ export class ArticleRender implements MDRendererCallback {
if (metadata?.frontmatter) {
const keys = this.assetsManager.expertSettings.frontmatter;
const frontmatter = metadata.frontmatter;
- res.title = this.getFrontmatterValue(frontmatter, keys.title);
- res.author = this.getFrontmatterValue(frontmatter, keys.author);
+ // frontmatter 优先:如果存在 title/author 则直接取之
+ const fmTitle = this.getFrontmatterValue(frontmatter, keys.title) || frontmatter['title'];
+ const fmAuthor = this.getFrontmatterValue(frontmatter, keys.author) || frontmatter['author'];
+ if (fmTitle) res.title = fmTitle;
+ if (fmAuthor) res.author = fmAuthor;
res.digest = this.getFrontmatterValue(frontmatter, keys.digest);
res.content_source_url = this.getFrontmatterValue(frontmatter, keys.content_source_url);
- res.cover = this.getFrontmatterValue(frontmatter, keys.cover);
+ res.cover = this.getFrontmatterValue(frontmatter, keys.cover) || frontmatter['cover'] || frontmatter['image'];
res.thumb_media_id = this.getFrontmatterValue(frontmatter, keys.thumb_media_id);
res.need_open_comment = frontmatter[keys.need_open_comment] ? 1 : undefined;
res.only_fans_can_comment = frontmatter[keys.only_fans_can_comment] ? 1 : undefined;
@@ -252,6 +274,41 @@ export class ArticleRender implements MDRendererCallback {
res.pic_crop_1_1 = '0_0.525_0.404_1';
}
}
+
+ // 如果未显式指定封面,尝试从正文首图( markdown 或 wikilink ) 提取,按出现顺序优先
+ if (!res.cover && this.originalMarkdown) {
+ let body = this.originalMarkdown;
+ if (body.startsWith('---')) body = body.replace(FRONT_MATTER_REGEX, '');
+
+ // 同时匹配两种形式并比较 index
+ const mdImgPattern = /!\[[^\]]*\]\(([^)\s]+)\)/g; // group1 为路径
+ const wikilinkPattern = /!\[\[(.+?)\]\]/g; // group1 为文件名或 path
+ interface Candidate { idx:number; basename:string; }
+ const candidates: Candidate[] = [];
+
+ let m: RegExpExecArray | null;
+ while ((m = mdImgPattern.exec(body)) !== null) {
+ const rawPath = m[1];
+ if (/^https?:\/\//i.test(rawPath)) continue; // 跳过远程
+ const clean = rawPath.split('#')[0].split('?')[0];
+ const basename = clean.split('/').pop();
+ if (basename && /\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i.test(basename)) {
+ candidates.push({ idx: m.index, basename });
+ }
+ }
+ while ((m = wikilinkPattern.exec(body)) !== null) {
+ const inner = m[1].trim();
+ const clean = inner.split('#')[0].split('?')[0];
+ const basename = clean.split('/').pop();
+ if (basename && /\.(png|jpe?g|gif|bmp|webp|svg|tiff)$/i.test(basename)) {
+ candidates.push({ idx: m.index, basename });
+ }
+ }
+ if (candidates.length > 0) {
+ candidates.sort((a,b)=> a.idx - b.idx);
+ res.cover = `![[${candidates[0].basename}]]`;
+ }
+ }
return res;
}
diff --git a/src/settings.ts b/src/settings.ts
index b8f2c23..ea2f78c 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -43,6 +43,7 @@ export class NMPSettings {
excalidrawToPNG: boolean;
isLoaded: boolean = false;
enableEmptyLine: boolean = false;
+ enableMarkdownImageToWikilink: boolean = true; // 自动将  转为 ![[file.ext]]
private static instance: NMPSettings;
@@ -72,6 +73,7 @@ export class NMPSettings {
this.excalidrawToPNG = false;
this.expertSettingsNote = '';
this.enableEmptyLine = false;
+ this.enableMarkdownImageToWikilink = true;
}
resetStyelAndHighlight() {
@@ -101,6 +103,7 @@ export class NMPSettings {
excalidrawToPNG,
expertSettingsNote,
ignoreEmptyLine,
+ enableMarkdownImageToWikilink,
} = data;
const settings = NMPSettings.getInstance();
@@ -155,6 +158,9 @@ export class NMPSettings {
if (ignoreEmptyLine !== undefined) {
settings.enableEmptyLine = ignoreEmptyLine;
}
+ if (enableMarkdownImageToWikilink !== undefined) {
+ settings.enableMarkdownImageToWikilink = enableMarkdownImageToWikilink;
+ }
settings.getExpiredDate();
settings.isLoaded = true;
}
@@ -179,6 +185,7 @@ export class NMPSettings {
'excalidrawToPNG': settings.excalidrawToPNG,
'expertSettingsNote': settings.expertSettingsNote,
'ignoreEmptyLine': settings.enableEmptyLine,
+ 'enableMarkdownImageToWikilink': settings.enableMarkdownImageToWikilink,
}
}
diff --git a/todo.list b/todo.list
index 8567523..0ca6c6a 100644
--- a/todo.list
+++ b/todo.list
@@ -1,4 +1,17 @@
-1. 预处理markdown文件:
+
+1. 目前图片只能识别![[imagefile]],无法识别,把markdownown文件中转化为[[imagefile]]。
+如:转为![[2025ZK1-7.jpg]]
+✅
+
+2. 读取markdown的frontmatter属性,文章标题取title和作者取author内容。
+以附件2025ZK1.md为例:
+提取以下信息(忽略两端的“”):
+- 如tiltle不为空,文章title不使用文件名,使用: 6月特种兵式观展
+- 如果author不为空,公众号文章作者: 大童。
+
+- , ![[name.ext]]不分优先级,看哪个在文章的最前面,取最前面这个作为封面图片
+
+3. 预处理markdown文件:
对{{}}{{}}或{{}}{{}}
- 获取dir中的内容,如"/img/guanzhan/1",与PREPATH拼接,全局定义PRE_PATH=/Users/gavin/myweb/static
图片所在路径:PREPATH+"/img/guanzhan/1",即/Users/gavin/myweb/static/img/guanzhan/1。
@@ -7,8 +20,6 @@
如n=2,取出的图片为xx.jpg,yy.png,那么把{{}}{{}}替换为:
![[xx.jpg]]
![[yy.png]]
- 在main.js单独函数中处理,在预处理内容时调用。
-
2.
对如下:
@@ -54,7 +65,7 @@ slug: guanzhan
提取以下信息(忽略两端的“”):
- 公众号文章title: 6月特种兵式观展
-- 文章作者: 大童
+- 公众号文章作者: 大童
- 文章封面图片:GALLERY_PRE_PATH+"/img/shufa/a.jpg",转化为![[a.jpg]]; 如image为空,封面图片取文章中第一张图片
4.
@@ -66,3 +77,4 @@ slug: guanzhan
注意:如果我把2025ZK1.md 内容:img改成![[2025ZK1-7.jpg]],以上解析没有问题。
5. 文章没有图片,封面使用一张默认图片(设计一张)。
+