微信公众号 → Obsidian 剪藏
这个 Skill 用来把 mp.weixin.qq.com 微信公众号文章保存成 Obsidian Markdown 笔记。
核心目标:
- 真实浏览器加载:用
agent-browser打开页面,避免普通curl/web_fetch被微信反爬拦截。 - 图文顺序保留:按 DOM 顺序提取正文和图片,图片插入位置尽量贴近原文。
- 图片本地化:正文图片下载到笔记同级
attachments/目录。 - Markdown 标题层级保留:识别原生
h1-h6和微信常见“视觉标题”,输出##/###。 - 写入前确认:除非用户已经明确指定路径,否则必须先询问保存目录,确认后再写文件。
触发场景
当用户发来微信公众号链接,并表达以下意图时使用本 Skill:
- “存 Obsidian”
- “剪藏”
- “帮我存”
- “保存到这个路径”
- “把这篇公众号文章放进 Obsidian”
链接通常长这样:
https://mp.weixin.qq.com/s/xxxxx
依赖
agent-browser≥ 0.17curl- 本地可访问的 Obsidian Vault
安装 agent-browser:
npm install -g agent-browser
agent-browser install
总体流程
- 用
agent-browser打开微信公众号文章。 - 等待页面加载。
- 滚动全文,触发懒加载图片。
- 按 DOM 顺序提取标题、正文和图片。
- 如果用户未明确指定保存路径,先询问并等待确认。
- 下载图片到
<note_dir>/attachments/。 - 写入 Markdown 笔记。
- 关闭浏览器。
- 向用户汇报保存路径、图片数量、失败项。
Step 1 — 打开页面
agent-browser open "<wechat_url>"
agent-browser wait --load networkidle
如果 networkidle 等待失败,但页面正文已经可见,可以继续执行下一步。
Step 2 — 获取文章标题
agent-browser get title
优先从页面里的 #activity-name 读取标题;如果失败,再用浏览器标题兜底。
Step 3 — 滚动全文,触发懒加载图片
微信图片大量使用懒加载。必须滚动全文后再提取图片 URL,否则图片 src 可能为空。
agent-browser eval "
(async () => {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 2000));
const step = 600;
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(r => setTimeout(r, 300));
}
return 'done';
})()"
Step 4 — 按 DOM 顺序提取内容,并保留 Markdown 结构
重要规则:
agent-browser eval内部尽量使用经典function(){}语法。- 不要在
JSON.stringify()包裹的大段 JS 里混用复杂 shell 引号。 - 提取结果要保留
type:heading/text/img。 - 写笔记时必须把
heading渲染成 Markdown 标题,而不是普通文本。
agent-browser eval "
(function() {
function textOf(node) {
return (node.innerText || '').replace(/\u00a0/g, ' ').trim();
}
function maxFontSize(node) {
var max = 0;
var els = [node].concat(Array.prototype.slice.call(node.querySelectorAll('*')));
els.forEach(function(el) {
var n = parseFloat(getComputedStyle(el).fontSize || '0');
if (n > max) max = n;
});
return max;
}
function hasBold(node) {
if (node.querySelector('strong,b')) return true;
var els = [node].concat(Array.prototype.slice.call(node.querySelectorAll('*')));
return els.some(function(el) {
var fw = getComputedStyle(el).fontWeight || '400';
return fw === 'bold' || parseInt(fw, 10) >= 600;
});
}
function headingLevel(node, text) {
var tag = node.tagName;
if (/^H[1-6]$/.test(tag)) return parseInt(tag.slice(1), 10);
var size = maxFontSize(node);
var bold = hasBold(node);
var short = text.length <= 40;
// 微信常把视觉标题做成带样式的 p / section,而不是 h2 / h3。
// 这里保守识别常见中文大纲标题和数字小标题。
if (/^[一二三四五六七八九十]+[\..、]\s*/.test(text) && (bold || size >= 19 || short)) return 2;
if (/^\d+[\..、]\s*/.test(text) && (bold || size >= 19 || short)) return 3;
if (short && bold && size >= 20) return 3;
return 0;
}
function inlineMarkdown(node) {
var text = textOf(node);
if (!text) return '';
if (node.querySelector('code')) return text.replace(/`/g, '\\`');
return text;
}
var nodes = document.querySelectorAll('#js_content h1,#js_content h2,#js_content h3,#js_content h4,#js_content h5,#js_content h6,#js_content p,#js_content section,#js_content img');
var result = [];
var imgIdx = 0;
nodes.forEach(function(node) {
if (node.tagName === 'IMG') {
var src = node.currentSrc || node.src || node.dataset.src || '';
if (src && src.includes('mmbiz') &&
!src.includes('mmbiz.qlogo') &&
!src.includes('profile')) {
var h = node.naturalHeight || node.height || 0;
var alt = (node.alt || '').toLowerCase();
if ((h >= 50 || h === 0) &&
!alt.includes('二维码') &&
!alt.includes('引导') &&
!alt.includes('赞赏')) {
result.push({ type: 'img', idx: imgIdx++, src: src });
}
}
} else {
if (node.tagName === 'SECTION' && node.querySelector('p,img,section')) return;
var text = textOf(node);
if (text && text.length > 3) {
var level = headingLevel(node, text);
if (level) result.push({ type: 'heading', level: level, text: text });
else result.push({ type: 'text', text: inlineMarkdown(node) });
}
}
});
return JSON.stringify(result);
})()"
Markdown 标题规则
- 原生
h1-h6→#到###### 一. / 一、 / 二.这类章节标题 →##1. Mac / 2. Windows这类数字小标题 →###- 短文本 + 加粗 + 20px 以上字号 →
###
写入时示例:
## 一. Claude Code 安装
### 1. Mac
图片仍然必须保持在 DOM 原始位置,不要统一堆到文末。
图片过滤规则
跳过这些图片:
mmbiz.qlogo:公众号头像mp_profile:账号资料图- 高度小于 50px:装饰线、分割线
alt包含二维码/引导/赞赏:二维码、引导关注、赞赏图
Step 5 — 确认保存位置
如果用户已经明确给出保存路径,可以直接进入 Step 6。
如果用户没有明确路径,必须停下来询问:
📂 建议保存到:<vault_root>/<topic_directory>/
📄 文件名:<title-keywords-YYYY-MM-DD>.md
🖼 图片目录:同级 attachments/
确认保存到这里吗?还是换个位置?
HARD STOP:用户确认前,不要下载图片,不要写入笔记。
Step 6 — 下载图片
只在用户确认保存路径后执行。
mkdir -p "<note_dir>/attachments"
curl -s -L --fail \
-A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' \
-e 'https://mp.weixin.qq.com/' \
"<image_url>" -o "<note_dir>/attachments/<filename>"
关键点:
- 微信图片下载必须带 Referer:
-e 'https://mp.weixin.qq.com/' - 文件名建议:
<slug>-图00.png、<slug>-图01.jpg - 如果 URL 里有
wx_fmt=jpeg,扩展名用.jpg;否则默认.png - 批量下载时用 shell function,不要用
declare -A,因为 zsh 不支持关联数组
批量下载函数示例:
download_img() {
local idx=$1 url=$2
local ext="png"
echo "$url" | grep -q "wx_fmt=jpeg" && ext="jpg"
local fname=$(printf "<slug>-图%02d.%s" "$idx" "$ext")
curl -s -L --fail \
-A 'Mozilla/5.0' \
-e 'https://mp.weixin.qq.com/' \
"$url" -o "$fname" \
&& echo "OK $fname" || echo "FAIL $fname"
}
Step 7 — 写入 Markdown 笔记
必须严格使用 Step 4 的 DOM 顺序。
# {文章标题}
**Source:** WeChat — {作者 / 公众号名}
**Original URL:** {URL}
**Clipped:** {YYYY-MM-DD}
**Tags:** #{tag1} #{tag2}
---
{正文段落}
![[slug-图00.jpg]]
## {二级标题}
{更多正文}
![[slug-图01.png]]
---
**References:**
- {原文链接或正文链接}
注意:
{ type: 'heading', level: 2, text: '...' }必须写成## ...{ type: 'heading', level: 3, text: '...' }必须写成### ...- 图片使用 Obsidian 格式:
![[filename]] - 图片放在笔记同级
attachments/目录
Step 8 — 关闭浏览器
agent-browser close
Step 9 — 汇报结果
向用户说明:
- 笔记路径
- 图片目录
- 成功下载图片数量
- 失败图片数量(如有)
- 提醒:如果移动笔记,最好连同同级
attachments/一起移动
常见问题与修复
| 问题 | 修复 |
|---|---|
图片 src 为空 | 先滚动全文,触发懒加载 |
| 微信图片下载 403 | curl 必须带 -e 'https://mp.weixin.qq.com/' |
| 图片顺序错乱 | 按 DOM 单次遍历,不要文字和图片分开收集 |
zsh 报 bad substitution | 不要用 declare -A;改用 shell function |
SyntaxError: missing ) after argument list | agent-browser eval 里少用复杂箭头函数和嵌套引号 |
async eval 卡住 | 用 (async () => { ... })() 包住 |
| 标题变成普通文字 | 使用 heading 类型并在写入时转成 ## / ### |
输出质量要求
- 不要遗漏标题层级。
- 不要把图片集中放到开头或结尾。
- 不要在未确认路径时写文件。
- 不要把二维码、公众号头像、赞赏码当正文图片。
- 不要把本地绝对路径、账号、token 写进公开输出。