乐于分享
好东西不私藏

养虾日常:Obsidian插件Wechat Importer 开发全过程记录

养虾日常:Obsidian插件Wechat Importer 开发全过程记录

by WorkBuddy · 2026-04-15本文档记录从需求分析到最终发布的完整过程,供 AI agent 学习参考


一、项目背景

用户已有一个 Python 命令行工具 wechat2obsidian(已打包为 exe),功能是从微信公众号抓取文章并导入到 Obsidian。用户发现了一个 Obsidian 插件 Xiaohongshu Importer(小红书导入插件),希望参考它做一个类似的微信公众号导入插件。

现有工具wechat2obsidian — Python CLI + exe,仓库 https://github.com/Ashley-321/wechat2obsidian

目标:开发 Obsidian 原生插件 Wechat Importer,体验更好、跨平台、可自动更新。


二、第一阶段:可行性分析(10:06)

2.1 用户指令

“分析下这款插件(Xiaohongshu Importer),参考 wechat2obsidian 这个任务,做 Wechat Importer 公众号文章导入的可行性”

2.2 分析过程

技术对比

对比项
Xiaohongshu Importer(XHS)
wechat2obsidian(现有)
Wechat Importer(待做)
实现形式
Obsidian 原生插件(TypeScript)
Python exe(外部工具)
Obsidian 原生插件(TypeScript)
抓取方式
requestUrl()

 直接请求网页
requests

 + BeautifulSoup
requestUrl()

 直接请求网页
是否需要登录
不需要
不需要
不需要
图片处理
可选下载到本地/外链
下载绕防盗链
需要绕防盗链(已有方案)
用户体验
粘贴链接即可
命令行/.bat 脚本
粘贴链接即可
跨平台
Win/Mac/Linux/Mobile
Windows 专属
Win/Mac/Linux/Mobile
更新方式
Obsidian 自动更新
手动替换 exe
Obsidian 自动更新
分发渠道
Obsidian 官方插件市场
GitHub Releases
Obsidian 官方插件市场

结论

完全可行。核心原因:

  1. XHS 插件的脚手架可以直接复用(Obsidian 插件架构通用)
  2. wechat2obsidian 已有完整的微信抓取逻辑(fetcher.py + parser.py + downloader.py),只需翻译成 TypeScript
  3. 图片防盗链有现成解法(加 Referer header)

估算工作量

~3.5 天(有现成逻辑基础,实际只用了半天)

2.3 用户决策

“TypeScript 基础,你来写。插件名 Wechat Importer。先做第一版单一链接导入。”


三、第二阶段:插件开发(13:35)

3.1 用户指令

(确认开发,要求第一版只做单链接导入)

3.2 环境搭建

3.2.1 创建项目目录

d:\WorkBuddy\wechat-importer\

3.2.2 项目文件结构

wechat-importer/├── src/│   └── main.ts          ← 核心逻辑(809行)├── manifest.json         ← 插件元信息├── package.json          ← npm 构建配置├── tsconfig.json         ← TS 编译配置├── esbuild.config.mjs    ← esbuild 构建脚本├── styles.css            ← Modal UI 样式├── main.js               ← 编译输出(21.3 KB)└── .gitignore

3.2.3 安装依赖

npm init -ynpminstall obsidian @types/node typescript esbuild --save-dev

⚠️ 踩坑 1:npm.ps1 被 PowerShell ExecutionPolicy 禁止

# 错误:npx : 无法加载文件 C:\Program Files\nodejs\npx.ps1,因为在此系统上禁止运行脚本# 解决:不用 npx 命令,直接用 node 调用 esbuildnode node_modules/esbuild/bin/esbuild src/main.ts --bundle ...

3.3 核心代码设计

3.3.1 整体架构

代码逻辑从 Python 版三个文件逐行翻译而来:

Python 源文件                          TypeScript 对应─────────────────────────────────────────────────────wechat2obsidian/src/wechat2obsidian/├── fetcher.py    →  fetchArticle()     ← 用 requestUrl 替代 requests├── parser.py     →  htmlToMarkdown()   ← 用 DOMParser + 递归遍历替代正则└── downloader.py →  downloadImage()    ← 用 requestUrl + Node.js https fallback

3.3.2 抓取层 — fetchArticle()

对应 fetcher.py,从微信公众号 HTML 页面提取文章数据。

核心思路:用 Obsidian 内置的 requestUrl() 直接请求微信文章 URL(绕 CORS),然后用 DOMParser(等同于 Python 的 BeautifulSoup)解析 HTML。

// 关键 HTML 结构(微信文章非常稳定)// 标题    → <h1 class="rich_media_title">// 作者    → <a class="rich_media_meta_link"> 或 id="js_name"// 时间    → <span id="publish_time">// 正文    → <div class="rich_media_content">asyncfunctionfetchArticle(url:string):Promise<ArticleData>{// 1. 请求页面const resp =awaitrequestUrl({ url, headers:HEADERS, method:"GET"});const html = resp.text;// 2. 用 DOMParser 解析(等同于 BeautifulSoup)const parser =newDOMParser();const doc = parser.parseFromString(html,"text/html");// 3. 提取标题(多级 fallback)const h1 = doc.querySelector("h1.rich_media_title");const ogTitle = doc.querySelector('meta[property="og:title"]');// 4. 提取作者(多级 fallback)const authorTag = doc.querySelector("a.rich_media_meta_link");const jsName = doc.getElementById("js_name");// 5. 提取时间(多级 fallback:DOM → JS变量 → 时间戳 → 当前日期)const timeTag = doc.querySelector("span#publish_time");// 如果 DOM 没取到,尝试从 JS 变量取:// var publish_time = "2026-04-15"// var ct = '1744700000'// 6. 提取内容区const contentDiv = doc.querySelector("div.rich_media_content");  contentDiv.querySelectorAll("script, style").forEach(el => el.remove());return{ url, title, author, publishTime, contentDiv, rawHtml: html };}

关键 Headers(必须带 Referer 才能正常抓取):

constHEADERS={"User-Agent":"Mozilla/5.0 ...","Referer":"https://mp.weixin.qq.com/",// 关键!没有会 403};

3.3.3 HTML→Markdown — htmlToMarkdown()

对应 parser.py递归遍历 DOM 树(不是正则替换)。

functionhtmlToMarkdown(element: Element, imgMap: Map<string,string>):string{const result:string[]=[];for(const child ofArray.from(element.childNodes)){// 文本节点 → 直接输出if(child.nodeType === Node.TEXT_NODE){...}// 元素节点 → 按 tag 类型递归处理const tag =(child as HTMLElement).tagName.toLowerCase();switch(tag){case"img":// 图片 → ![alt](localPath)case"p":// 段落 → 两端空行case"br":// 换行case"strong":// 粗体 → **text**case"em":// 斜体 → *text*case"a":// 链接 → [text](href)case"h1"-"h6":// 标题 → # textcase"blockquote":// 引用 → > textcase"ul":// 无序列表 → - itemcase"ol":// 有序列表 → 1. itemcase"table":// 表格 → | cell |case"pre":// 代码块 → ```code```case"section":case"div":// 容器 → 递归处理子节点case"span":// 行内容器 → 递归default:// 兜底 → 递归处理}}return result.join("");}

⚠️ 第一版踩的坑:初版用的是正则替换,不是递归遍历,导致很多 HTML 结构解析失败。用户测试后发现问题,全面重写为递归 DOM 遍历。

3.3.4 图片下载 — downloadImage()

对应 downloader.py,处理微信图片防盗链。

asyncfunctiondownloadImage(app: App, url:string, saveDir:string):Promise<string|null>{const filename =getImageFilename(url);// 用 URL hash 生成唯一文件名const filepath =`${saveDir}/${filename}`;// 跳过已下载的文件if(await app.vault.adapter.exists(filepath))return filename;// Method 1: Obsidian requestUrl(优先)const resp =awaitrequestUrl({ url, headers:IMG_HEADERS, method:"GET"});let buffer = resp.arrayBuffer;// Method 2: Node.js https fallback(requestUrl 失败时)if(!buffer || buffer.byteLength ===0){    buffer =awaitdownloadWithNodeHttps(url);}// 写入 Vaultawait app.vault.adapter.writeBinary(filepath, buffer);return filename;}

图片防盗链解法

// 微信图片服务器是 mmbiz.qpic.cn,直接 fetch 会 403// 必须带 Referer header:constIMG_HEADERS={"Referer":"https://mp.weixin.qq.com/",// 关键!"User-Agent":"Mozilla/5.0 ...",};// 只处理微信自己的图片域名constALLOWED_IMG_DOMAINS=["mmbiz.qpic.cn","mmbiz.qlogo.cn"];

图片文件名生成(和 Python 版一致):

functiongetImageFilename(url:string):string{// 从 URL 提取格式:wx_fmt=png → ext = "png"const fmtMatch = url.match(/wx_fmt=(\w+)/);// 用 URL 的简单 hash 作为文件名(避免重复)return`wechat_${hashHex}.${ext}`;}

3.3.5 写入笔记 — writeNote()

对应 cli.py 的保存逻辑。

asyncfunctionwriteNote(app: App, article: ArticleData, settings):Promise<TFile>{// 1. 确保文件夹存在for(const dir of[folder, attachFolder]){if(!await app.vault.adapter.exists(dir))await app.vault.createFolder(dir);}// 2. 下载所有图片,建立 URL → 本地文件名 的映射const imgMap =newMap<string,string>();for(const imgUrl ofextractImageUrls(contentDiv)){const localName =awaitdownloadImage(app, imgUrl, attachFolder);    imgMap.set(imgUrl,`${attachFolder}/${localName}`);awaitsleep(300);// 请求间隔,避免被限}// 3. HTML → Markdown(图片 URL 替换为本地路径)const mdContent =htmlToMarkdown(contentDiv, imgMap);// 4. 组装 Frontmatter + 正文const noteContent =`---title: "文章标题"source: "https://mp.weixin.qq.com/s/..."author: "公众号名称"date: 2026-04-15imported_at: "2026/4/15 13:41:00"tags: [公众号]---# 文章标题> 作者:xxx | 发布:2026-04-15 | [原文链接](...)---正文内容...`;// 5. 写入文件(处理重名:加时间戳后缀)const file =await app.vault.create(notePath, noteContent);return file;}

3.3.6 UI 层 — Modal + Settings

导入弹窗

classWechatImportModalextendsModal{onOpen(){// 创建输入框(URL)// 创建复选框(是否下载图片)// 创建导入按钮// 支持 Enter 键触发导入}}

设置面板

classWechatImporterSettingTabextendsPluginSettingTab{display(){// 文章保存目录(默认:公众号)// 附件保存目录(默认:公众号/attachments/wechat)// 是否下载图片(默认:开启)// 文件名格式(日期-标题 / 仅标题)}}

插件入口

classWechatImporterPluginextendsPlugin{onload(){// 左侧 Ribbon 图标(file-down 图标)this.addRibbonIcon("file-down","导入微信公众号文章",...);// 命令面板(Ctrl+P 搜索"导入微信")this.addCommand({ id:"import-wechat-article", name:"导入微信公众号文章"});// 设置面板this.addSettingTab(newWechatImporterSettingTab(this.app,this));}}

3.4 编译与安装

编译命令

node node_modules/esbuild/bin/esbuild src/main.ts \--bundle\--external:obsidian\--outfile=main.js \--format=cjs \--platform=node \--target=es2022 \--sourcemap

安装到 Obsidian

# 复制 3 个文件到 Vault 的插件目录Copy-Item main.js "D:\Obsidian Vault\.obsidian\plugins\wechat-importer\"Copy-Item manifest.json "..."Copy-Item styles.css "..."

安装后需要关闭并重新打开 Obsidian 才能识别新插件。


四、第三阶段:测试与迭代(14:06 – 15:30)

4.1 问题 1:插件列表不显示

现象:Obsidian 设置 → 第三方插件 → 看不到 Wechat Importer

原因:Obsidian 启动时扫描插件目录,新安装的插件需要重启才能被识别

解决:关闭 Obsidian → 重新打开

4.2 问题 2:图片未导入

现象:文章正文能抓到,但图片没下载

原因:第一版的图片正则太弱,{{IMG:url}} 占位符替换机制有问题

解决:全面重写,改为从 contentDiv.querySelectorAll("img") 提取 data-src(和 Python downloader.py 一致)

4.3 问题 3:内容解析失败(核心问题)

现象:第二次导入时只生成了 frontmatter,正文内容丢失

原因:第一版用的是正则表达式解析 HTML,而不是 DOMParser。正则对 HTML 结构变化的适应力极差,稍有不同就匹配失败。

用户原话

“为什么之前的终端好好的,这个不能直接照搬逻辑吗?”

解决:全面重写核心解析逻辑:

之前(正则方式,脆弱):教训:有现成的 Python 代码可以直接翻译,不要自己重新发明轮子。

4.4 问题 4:用户反馈的细节调整

用户指令 1

“图片的位置,放在公众号文件夹下的 attachments 文件夹下的 wechat 文件夹下”

修改:attachDir 默认值从 attachments/wechat → 公众号/attachments/wechat

用户指令 2

“标题不要加日期”

修改:filenameTemplate 默认值从 date-title → title

4.5 最终配置


五、第四阶段:发布到 GitHub(15:39 – 16:30)

5.1 用户指令

“这一版定稿初版,版本号你定一个,推送 github”

5.2 版本号

定为 v1.0.0

5.3 推送过程

⚠️ 踩坑 1:没有 gh CLI

gh repo create Ashley-321/wechat-importer --public# 错误:gh : 无法将"gh"项识别为 cmdlet

解决:用户手动在 GitHub 网页创建空仓库。

⚠️ 踩坑 2:Git 没有配置用户信息

fatal: unable to auto-detect email address

解决:从 wechat2obsidian 仓库获取之前的配置(Ashley-321)设置到新仓库。

⚠️ 踩坑 3:新仓库推送需要认证

fatal: could not read Username for 'https://github.com'

原因:wechat2obsidian 之前能推送是因为已经 clone 过,凭据缓存了。新仓库首次推送需要认证。

解决:用户生成 GitHub Personal Access Token(ghp_开头,repo 权限),用 token 推送。

推送成功后清除 remote URL 中的 token(安全)。

⚠️ 踩坑 4:GitHub 网络超时

原因:没有开 VPN,GitHub 443 端口被阻断

解决:用户开 ,给 git 配代理:

5.4 最终结果

仓库
地址
说明
wechat2obsidian
https://github.com/Ashley-321/wechat2obsidian
Python exe + Release
wechat-importer
https://github.com/Ashley-321/wechat-importer
Obsidian 插件


六、完整踩坑清单

#
问题
原因
解决方案
1
npm/npx 命令执行失败
PowerShell ExecutionPolicy 禁止 .ps1
用 node node_modules/esbuild/bin/esbuild 直接调用
2
插件安装后不显示
Obsidian 需要重启才扫描新插件
关闭重开 Obsidian
3
图片未导入
图片正则匹配失败
改用 DOMParser querySelectorAll(“img”)
4
内容解析失败(核心)
用正则解析 HTML 太脆弱
全面改用 DOMParser + 递归遍历(照搬 Python 逻辑)
5
标题加了日期
默认 filenameTemplate 是 date-title
改为 title
6
附件目录不对
默认是 attachments/wechat
改为 公众号/attachments/wechat
7
gh CLI 未安装
系统没有 GitHub CLI
用户手动在网页创建仓库
8
Git 无用户信息
新仓库没有 git config
从已有仓库复制配置
9
新仓库推送需认证
没有缓存凭据
用 PAT token 推送
10
GitHub 连接超时
没开 VPN,443 端口被阻断
开 ,git 配代理
11
exe 需要通过 Release 发布
仓库里直接放 exe 不友好
用 GitHub API 创建 Release + 上传 asset

七、关键经验总结

7.1 有现成代码就翻译,不要重新发明

Python 版的 fetcher.py / parser.py / downloader.py 已经在生产环境验证过,直接逐行翻译成 TypeScript 是最快的路径。第一版自己用正则重新实现,结果各种 bug,浪费了大量时间。

7.2 HTML 解析用 DOMParser,不要用正则

正则解析 HTML 是所有 bug 的根源。Obsidian 运行在 Electron 环境中,有完整的 DOMParser 支持,等同于 Python 的 BeautifulSoup,应该优先使用。

7.3 Obsidian 插件开发的关键 API

API
用途
注意事项
requestUrl()
HTTP 请求(绕 CORS)
返回的 arrayBuffer 可能是空的,需要 fallback
app.vault.create()
创建 Markdown 文件
路径相对于 Vault 根目录
app.vault.adapter.writeBinary()
写入二进制文件(图片)
需要 ArrayBuffer
app.vault.adapter.exists()
检查文件是否存在
路径相对于 Vault 根目录
app.vault.createFolder()
创建文件夹
不支持递归创建
new Modal()
弹窗 UI
需要手动 manage 生命周期
this.addRibbonIcon()
左侧工具栏图标
图标名称用 Lucide icon 名
this.addCommand()
命令面板命令
Ctrl+P 可搜索
new Notice()
顶部通知
第二个参数是持续时间(ms),0=不自动关闭

7.4 微信公众号抓取的关键知识

  1. 文章 URL 格式
    https://mp.weixin.qq.com/s/xxxxxxxx
  2. 必须带 Referer header
    Referer: https://mp.weixin.qq.com/,否则 403
  3. 图片用 data-src 而非 src
    :微信用懒加载,真实图片地址在 data-src 属性
  4. 图片域名
    mmbiz.qpic.cn(主域名)、mmbiz.qlogo.cn(头像/logo)
  5. 内容区选择器
    div.rich_media_content(非常稳定,多年未变)
  6. 不需要登录
    :微信公众号文章是公开可访问的
  7. 发布时间获取
    :优先从 DOM 取 #publish_time,fallback 到 JS 变量 var publish_time


八、最终代码结构总览

wechat-importer/├── src/main.ts              ← 全部代码(809 行)│   ├── Settings 定义        (第 28-40 行)│   ├── Constants/Headers    (第 46-60 行)│   ├── fetchArticle()       (第 79-168 行)  ← 翻译自 fetcher.py│   ├── htmlToMarkdown()     (第 174-334 行) ← 翻译自 parser.py│   ├── extractImageUrls()   (第 352-363 行) ← 翻译自 downloader.py│   ├── downloadImage()      (第 386-448 行) ← 翻译自 downloader.py│   ├── writeNote()          (第 503-583 行) ← 翻译自 cli.py│   ├── WechatImportModal    (第 589-657 行) ← UI 弹窗│   ├── SettingTab           (第 663-728 行) ← 设置面板│   └── WechatImporterPlugin (第 734-808 行) ← 插件主类├── manifest.json            ← 插件元信息(id/name/version/minAppVersion)├── package.json             ← 依赖:obsidian, typescript, esbuild├── tsconfig.json            ← TS 编译配置├── esbuild.config.mjs       ← 构建脚本├── styles.css               ← Modal 样式├── main.js                  ← 编译输出(21.3 KB)└── .gitignore

九、用户指令完整记录

时间
用户指令
处理
10:06
分析 XHS 插件,参考 wechat2obsidian 做 Wechat Importer 可行性
可行性分析报告
~12:00
TypeScript 基础,你来写,插件名 Wechat Importer,先做单链接
开始开发
13:35
(确认开发)
搭建工程,编写核心代码
~14:00
重启后能加载了,测试导入,但图片没有导入
检查代码,发现正则问题
~14:20
为什么之前的终端好好的,这个不能直接照搬逻辑吗?
全面重写,照搬 Python 逻辑
~14:30
(测试结果反馈)
多轮迭代修复
~15:06
其他都正确,不要动。图片放公众号/attachments/wechat,标题不要加日期
修改默认配置
~15:30
这一版定稿初版,版本号你定一个,推送 github
v1.0.0,推送到 GitHub
~15:40
你署名是 workbuddy 吗?
确认署名 WorkBuddy
~15:50
新仓库建好了
推送成功
~16:00
这个项目中为什么没有 EXE,小白用不了
推送 exe 到 wechat2obsidian
~16:15
为什么有两个 exe?
删除旧版 wx2obsidian_v0.1.0.exe
~16:20
这个 dist 文件夹没了
修改 .gitignore,推送 dist 目录
~16:30
这个没有啊(Release 页面),我要去新建吗?
用 GitHub API 创建 Release + 上传 exe

by WorkBuddy · 2026-04-15仓库:https://github.com/Ashley-321/wechat-importer

注:此篇文章由workbuddy自己总结,用的指令是
我需要你全面具体的写一下wechat-importer的开发过程,最好是用户指令和你的全部思考编写都包含,导出一个markdown放桌面,因为我要让另一个小白agent学习。