🎁 Get the FREE AI Skills Starter GuideSubscribe →
BytesAgainBytesAgain
🦀 ClawHub

WeChat to Obsidian

by @dw1161

将微信公众号文章剪藏到 Obsidian:使用真实无头浏览器加载页面,按原文顺序保留文字、图片和 Markdown 标题层级,图片自动下载到 attachments 目录。

Versionv1.0.3
Downloads535
Stars3
TERMINAL
clawhub install wechat-to-obsidian

📖 About This Skill


name: wechat-to-obsidian version: 1.0.3 description: 将微信公众号文章剪藏到 Obsidian:使用真实无头浏览器加载页面,按原文顺序保留文字、图片和 Markdown 标题层级,图片自动下载到 attachments 目录。 metadata: openclaw: requires: bins: - agent-browser - curl install: - id: agent-browser kind: node package: agent-browser bins: [agent-browser] label: "Install agent-browser (headless browser CLI)"

微信公众号 → 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.17
  • curl
  • 本地可访问的 Obsidian Vault
  • 安装 agent-browser

    npm install -g agent-browser
    agent-browser install
    


    总体流程

    1. 用 agent-browser 打开微信公众号文章。 2. 等待页面加载。 3. 滚动全文,触发懒加载图片。 4. 按 DOM 顺序提取标题、正文和图片。 5. 如果用户未明确指定保存路径,先询问并等待确认。 6. 下载图片到 /attachments/。 7. 写入 Markdown 笔记。 8. 关闭浏览器。 9. 向用户汇报保存路径、图片数量、失败项。


    Step 1 — 打开页面

    agent-browser open ""
    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 引号。
  • 提取结果要保留 typeheading / 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。

    如果用户没有明确路径,必须停下来询问:

    📂 建议保存到://
    📄 文件名:.md
    🖼 图片目录:同级 attachments/
    确认保存到这里吗?还是换个位置?
    

    HARD STOP:用户确认前,不要下载图片,不要写入笔记。


    Step 6 — 下载图片

    只在用户确认保存路径后执行。

    mkdir -p "/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/' \ "" -o "/attachments/"

    关键点:

  • 微信图片下载必须带 Referer:-e 'https://mp.weixin.qq.com/'
  • 文件名建议:-图00.png-图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 "-图%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 写进公开输出。