Fetch Archive to Lexiang

Data & APIs

通用文章抓取与归档工具。抓取任意 URL(免费/付费/登录墙)的文章全文,转换为结构化 Markdown,并可选转存到乐享知识库。支持 Substack、Medium、知识星球等付费平台的登录态管理。支持 YouTube 视频下载(yt-dlp)、播客音频下载(小宇宙FM等)、音频转录(Whisper)、翻译(中英对照格式),并将音视频和文字稿上传乐享知识库(文字稿使用在线文档格式,支持按块编辑)。支持 PDF 文件/链接:自动提取文本+精确裁剪图形,非中文内容默认翻译为中英对照后转存乐享。支持微博帖子抓取(CDP 模式绕过登录墙)。关键词触发:抓取文章、获取全文、付费文章、转存知识库、乐享、保存原文、fetch article、归档、YouTube、视频转录、字幕提取、视频下载、播客、podcast、小宇宙、xiaoyuzhou、PDF、论文、arxiv、微博、weibo。

Install

openclaw skills install fetch-archive-to-lexiang

抓取链接内容 & 转存知识库

🎬 视频/音频上传到乐享:必须用 scripts/upload_video_via_openapi.py(走 OpenAPI /cgi-bin/v1/kb/files/upload-params)。不要用 MCP 的 file_apply_uploaddocs/cos-param——它们产出 entry_type=file 的条目,不触发 VOD 转码,视频无法播放。详见下方「YouTube 视频处理 → Step 2:上传到乐享知识库」章节。凭证存放于 ~/.lexiang/openapi.json(不进 git)。

概述

将文章 URL(免费/付费/登录墙)抓取为结构化 Markdown,并自动转存到乐享知识库,实现素材归档和可追溯。

最终产出物

  1. <项目子目录>/<原文标题>.md — 完整文章 Markdown(含图片引用)
  2. <项目子目录>/<原文标题>_meta.json — 结构化元信息(原文链接、作者、发布时间、抓取时间等)
  3. <项目子目录>/images/ — 所有文章配图
  4. 乐享知识库中的文档副本(按天维度归档)

乐享文档链接格式(⚠️ 必须遵守)

转存完成后,必须按以下格式输出可点击访问的链接:

https://lexiangla.com/pages/{entry_id}?company_from=e6c565d6d16811efac17768586f8a025
  • entry_idimport_contententry_create_entry 返回的 entry.id
  • company_from:固定值 e6c565d6d16811efac17768586f8a025(凡哥的企业 ID,不可省略,省略后链接无法访问)
  • 禁止使用 mcp.lexiang-app.com/pages/... 格式——这是 MCP 内部调试链接,用户无法直接访问

文件命名规则(重要)

  • 必须使用原文标题命名,不要用 article.md 等通用名称
  • 文件名格式:<原文标题>.md<原文标题>_meta.json
  • 示例:How Notion uses Custom Agents.mdHow Notion uses Custom Agents_meta.json
  • 如果标题中包含文件名不合法字符(/\:等),替换为 -
  • 乐享知识库转存时也使用原文标题作为文档标题

工作流程

Step 1:素材收集

抓取方式决策树

根据 URL 类型选择抓取方式(按优先级排列):

  1. claude.com / anthropic.com 博客claude.com/blog/*anthropic.com/research/*anthropic.com/news/*)→ 直接用 fetch_article.py(已内置 Webflow SPA 支持,自动检测 .u-rich-text-blog / .w-richtext 容器并移除内嵌 <style> 标签)。用法与其他站点一致:
    python3 scripts/fetch_article.py fetch "<URL>" --output-dir <项目子目录>
    
  2. 微信公众号文章mp.weixin.qq.com)→ 优先使用乐享 MCP file_create_hyperlink(一步到位,后端自动抓取图文+OCR),详见下方「微信公众号文章处理」章节。降级方案:fetch_article.py
  3. YouTube 视频 → 使用 yt_download_transcribe.py(yt-dlp 下载 + Whisper 转录 + AI 翻译),详见下方「YouTube 视频处理」章节
  4. 播客音频(小宇宙 xiaoyuzhoufm.com、Apple Podcasts 等)→ yt-dlp 下载音频 + Whisper 转录,详见下方「播客音频处理」章节
  5. PDF 文件或 PDF 直链(如 arXiv PDF、乐享知识库中已存储的 PDF、本地 PDF 路径)→ 详见下方「PDF 处理」章节。不要web_fetchfetch_article.py 处理 PDF
  6. 微博weibo.com)→ 必须用 fetch_article.py --cdp(微博强制登录,WebFetch/Playwright 均被拦截),详见下方「微博帖子抓取」章节
  7. 付费/登录墙文章 → 用 fetch_article.py(Cookie 注入或 CDP 模式)
  8. 免费图文文章(正文含图片/截图/图表)→ 必须fetch_article.pyweb_fetch 只能返回文本,无法提取和下载页面中的图片)
  9. 免费纯文字文章(正文无配图)→ 可用 web_fetch,内容不完整时切换 fetch_article.py
  10. SPA 动态渲染网站fetch_article.py 抓取正文为空或极少)→ Playwright 直接生成 PDF,详见下方「SPA 网站 Playwright 直接出 PDF」章节
  11. 批量抓取帮助中心/文档站(如 readme.io、GitBook、Guru 等)→ Playwright 直接生成 PDF,详见下方「SPA 网站 Playwright 直接出 PDF」章节
  12. 文字观点 → 直接整理
  13. 图片素材 → 分析图片内容

⚠️ 关键原则web_fetch 工具只能返回文本内容,无法提取和下载页面中的图片。任何包含图片、截图、图表的文章,都必须使用 fetch_article.py 抓取,否则图片信息会完全丢失。当不确定文章是否含图时,默认用 fetch_article.py

⚠️ SPA 降级原则:如果 fetch_article.py 抓取后正文内容极少(< 200 字符),说明该网站是 SPA 动态渲染,通用内容提取器无法工作。此时应切换到 Playwright 直接生成 PDF 方案。

⚠️ 图片处理必须贯穿全流程:抓取阶段产出的 images/ 目录中的图片,在上传到乐享时不可遗漏。无论走哪条路径(md_to_page.py / MCP connector 分块导入 / entry_import_content),只要本地有图片文件,就必须上传到乐享文档中。具体图片上传流程见下方「步骤 4」的降级方案 A。

付费/登录墙文章获取

适用于所有需要登录态才能查看全文的网站(Substack 付费订阅、Medium 会员、知识星球、财新网、The Information 等),使用 fetch_article.py 脚本:

# Cookie 注入模式(默认,适用于大部分站点)
python scripts/fetch_article.py fetch <URL> --output-dir <项目子目录>

# CDP 模式(适用于 Cloudflare 保护站点、需要 Google 账号登录的站点)
python scripts/fetch_article.py fetch <URL> --output-dir <项目子目录> --cdp

两种浏览器模式

模式参数原理适用场景
Cookie 注入(默认)从 Chrome Cookie DB 提取 cookies → 注入 Playwright 浏览器Medium 等大部分站点
CDP--cdp通过 Chrome DevTools Protocol 连接用户真实 Chrome(port 9222),复用完整登录态Substack(自动启用)、OpenAI、Cloudflare 保护站点、LinkedIn、Google 系网站等

自动升级到 CDP 模式的场景

  1. Substack 站点(所有 *.substack.com 及已知自定义域名):自动使用 CDP 模式,并在抓取前校验登录态。未登录时会暂停提示用户在 Chrome 中登录,验证通过后才继续抓取。
  2. Cloudflare 保护站点(如 openai.com):自动切换 CDP 模式,等待 JS challenge 通过。
  3. 手动指定 --cdp 参数。

CDP 模式前置条件:确保 Chrome 浏览器已开启 CDP 远程调试端口:

# 方式1(推荐):直接用带 CDP 的方式启动 Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 &

# 方式2:如果 Chrome 已在运行,需要先关闭再以 CDP 模式重启
# 脚本会自动尝试此操作,但可能需要用户手动确认

⚠️ CDP 独立 profile 的已知限制

脚本会使用独立的 CDP profile 目录(~/.fetch_article/chrome_cdp_profile),虽然会自动复制 Cookies 文件,但以下登录态信息不会被同步

  • localStorage(Substack 等 SPA 站点的会话 token)
  • Service Worker 缓存
  • sessionStorage

实际影响:对于 Substack 等依赖 localStorage 的站点,仅靠 Cookies 复制可能无法完全还原登录态。脚本已通过 Substack 登录态缓存机制(~/.substack/storage_state.json)弥补此限制——首次登录后会保存完整的 Playwright storage state(含 Cookies + localStorage),后续抓取直接复用。

最佳实践

  1. 首次使用 Substack 前,先运行 python scripts/fetch_article.py login 完成登录并缓存
  2. 如果 CDP 模式下登录态校验失败,脚本会自动暂停并引导用户在弹出的 Chrome 窗口中登录
  3. 登录成功后会自动刷新缓存,后续抓取无需重复登录

工作原理

  1. 自动从 Chrome 浏览器的 Cookie 数据库提取目标域名的登录 cookies
  2. 将 cookies 注入 Playwright 浏览器上下文
  3. 加载页面,自动检测并等待 Cloudflare challenge 通过(如有)
  4. 滚动加载懒加载内容、下载所有图片
  5. 自动格式转换:检测下载图片的真实格式(WebP/SVG 伪装成 .png/.jpg 很常见),自动转为真正的 PNG 以确保 PDF 生成和文档嵌入兼容
  6. 将正文转换为 Markdown(article.md),图片保存到 images/ 子目录
  7. 内容提取时自动选择最长的内容容器(避免只抓到免费预览区域)

标题提取增强(多策略回退):

  1. CSS 选择器优先级:h1.post-title > article h1 > [class*="title"] h1 > h1
  2. 回退到 <meta property="og:title"><meta name="title">document.title
  3. 自动清理标题中的网站后缀(如 " - Cursor"" | Substack"
  4. 正文中与已提取标题相同的第一个 <h1> 会被自动去重,避免 MD 中标题重复

作者提取增强

  • CSS 选择器 + meta[name="author"] + [rel="author"] + meta[property="article:author"] 多策略回退

PDF 文件/链接处理(2026-05-12 实战验证 ✅)

触发条件:用户提供的是 PDF 直链(如 arxiv.org/pdf/xxx、COS/CDN 直链)、乐享知识库中已存储的 PDF 条目链接,或本地 PDF 文件路径。

核心原则

  • PDF 的文字和图片必须分两条路处理,不能合并为一步
  • 非中文 PDF(如英文论文)默认翻译为中英对照格式后再转存,不可跳过
  • 最终产出为乐享在线文档(page 类型),支持后续编辑

Step A:获取 PDF 文件

情况1:乐享知识库中已有 PDF 条目(如 lexiangla.com/pages/xxx

# 1. 从 URL 提取 entry_id
entry = mcp.entry_describe_entry(entry_id="<entry_id>")
file_id = entry.target_id  # PDF 的文件ID

# 2. 获取下载链接(有效期 3600s)
result = mcp.file_download_file(file_id=file_id, expire_seconds=3600)
download_url = result.url

# 3. 下载 PDF
curl -L -o paper.pdf "<download_url>"

⚠️ 注意:entry_describe_ai_parse_content 对大型 PDF 会超过 80K 字符限制而报错,不要用它获取 PDF 内容,改用下载方式。

情况2:arXiv 等直链 PDF

curl -L -o paper.pdf "https://arxiv.org/pdf/2605.05538"

Step B:提取文字(pymupdf)

import fitz

doc = fitz.open('paper.pdf')
text = ''
for page in doc:
    text += page.get_text()
with open('paper.txt', 'w') as f:
    f.write(text)
print(f"Pages: {doc.page_count}, chars: {len(text)}")

get_text() 只提取纯文字——矢量图(流程图/柱状图)和表格结构会丢失,这是预期行为,图形通过 Step C 单独处理。

Step C:定位并精确裁剪图形

PDF 中的图形分两类,提取方式不同:

类型判断方法提取方法
光栅图(嵌入的 PNG/JPEG)page.get_images() 有结果page.get_image_rects(xref) 获取坐标 → clip=Rect 裁剪
矢量图(流程图/柱状图,PDF 绘图命令)page.get_images() 无结果,但 page.get_drawings() 有数据page.get_drawings() 获取边界 → clip=Rect 截图

定位图形坐标(先找出各 Figure 所在页面):

doc = fitz.open('paper.pdf')

for page_idx in range(doc.page_count):
    page = doc[page_idx]
    
    # 找 Figure caption 位置(确定图形所在页)
    blocks = page.get_text("blocks")
    for b in blocks:
        if 'Figure' in b[4]:
            print(f"Page {page_idx+1}: [{b[0]:.0f},{b[1]:.0f},{b[2]:.0f},{b[3]:.0f}] {b[4][:60]}")
    
    # 光栅图坐标
    for img in page.get_images(full=True):
        rects = page.get_image_rects(img[0])
        print(f"  Raster img: {rects}")
    
    # 矢量图分布(按左栏/右栏区分,双栏论文左栏 x<295,右栏 x>295)
    drawings = page.get_drawings()
    if drawings:
        xs0 = [d['rect'].x0 for d in drawings]
        xs1 = [d['rect'].x1 for d in drawings]
        ys0 = [d['rect'].y0 for d in drawings]
        ys1 = [d['rect'].y1 for d in drawings]
        print(f"  Vector drawings bbox: ({min(xs0):.0f},{min(ys0):.0f},{max(xs1):.0f},{max(ys1):.0f})")

精确裁剪图形区域(3x 高清,仅裁剪图形本身+caption,不含论文正文):

mat = fitz.Matrix(3.0, 3.0)  # 3x 放大,确保清晰

# 根据上面分析的坐标,裁剪每个 Figure
# 双栏论文参考坐标:左栏 x: 58-290,右栏 x: 295-535
# y 坐标从 caption 文字位置往上定图形起点
clip = fitz.Rect(x0, y0, x1, y1)  # 精确到图形边界,含 caption
pix = page.get_pixmap(matrix=mat, clip=clip)
pix.save(f'Figure{n}.png')

⚠️ 关键注意事项

  • 绝对不能用 page.get_pixmap() 不传 clip——会截整页(含论文正文),不是图形本身
  • get_image_rects() 只对光栅图有效;矢量图只能通过 get_drawings() 的 bbox + caption 坐标推断边界
  • 验证截图效果:Read 工具可预览 PNG,检查是否干净(只含图形+caption)
  • 如果矢量图跨越双栏(x0 < 295),说明是通栏图,裁剪区域应包含完整宽度

Step D:语言检测与翻译

读取 paper.txt 前 500 字符,统计中文字符占比:

  • 中文字符比例 ≥ 30% → 中文内容,跳过翻译
  • 中文字符比例 < 30% → 非中文(如英文论文),必须翻译为中英对照格式

中英对照翻译格式(每段先英文原文,紧跟中文翻译,保留所有结构):

## Introduction / 引言

Standard RAG pipelines follow a static retrieve-then-generate paradigm...

标准 RAG 流水线遵循静态的"先检索再生成"范式...

### Tables(表格保留完整数据,加中文表头)

### Figures(图形位置用文字描述,图片在 Step E 中插入)

翻译方式(按优先级):

  1. translate_gemini.pyGEMINI_API_KEY 可用时)
  2. translate_article.pyOPENAI_API_KEY 可用时)
  3. AI 助手在对话中直接逐段翻译

Step E:导入乐享在线文档 + 插入图片

1. 创建在线文档(import_content)

result = mcp.entry_import_content(
    name="论文标题 中英对照翻译",
    content=open('paper_translated.md').read(),  # 中英对照版
    content_type="markdown",
    parent_id="<日期目录ID>",
    space_id="<SPACE_ID>"
)
page_entry_id = result.entry.id
page_root_block_id = result.entry.target_id  # page 根 block,用于 move/insert

2. 上传各 Figure 图片并插入正确位置

每张图的完整流程(三步):

# Step 1: 申请上传凭证
session_id, upload_url = mcp.block_apply_block_attachment_upload(
    entry_id=page_entry_id,
    name="FigureN.png",
    size=str(file_size),
    mime_type="image/png"
)

# Step 2: 上传图片(必须包含 Content-Length 和 Content-Type)
curl -X PUT "<upload_url>" \
  -H "Content-Type: image/png" \
  -H "Content-Length: <size>" \
  --data-binary @FigureN.png

# Step 3: 在文档正确位置插入 image block
# - 先用 block_list_block_children 获取 block 列表,找到对应章节段落的 index
# - caption 格式:英文原文 / 中文翻译(放在一行,用 / 分隔)
mcp.block_create_block_descendant(
    entry_id=page_entry_id,
    parent_block_id=page_root_block_id,
    index="<正确位置>",
    descendant=[{
        "block_type": "image",
        "image": {
            "session_id": session_id,
            "caption": "Figure N: English caption / 图N:中文翻译",
            "align": "center"
        }
    }]
)

⚠️ 重要

  • caption 英中放在一行:用 / 分隔,不要额外插入独立段落作为图注,否则显示时会有多余换行
  • 图片位置要精确:先获取 block 列表找到对应章节 block 的 index,不能全部 append 到末尾
  • update_block 不支持修改 image block 的 caption:如果 caption 写错了,只能 delete + recreate。重建时可用已有的 file_id(通过 block_describe_block 查询),不需要重新上传文件
  • 每次插入图片后 index 会偏移:如需在不同位置插入多张图,要按顺序从前往后插,并累计计算 index

3. 图片位置确定方法

先获取文档当前全部 block:

blocks = mcp.block_list_block_children(entry_id=page_entry_id, with_descendants=False)

找到对应章节的最后一个 p/h block 的 index,图片插入该 index+1 处。

已验证的工作流(arXiv PDF 2605.05538 实战,2026-05-12)

  • Figure 1/2 为光栅图(嵌入 PNG),用 get_image_rects() 定位
  • Figure 3/4 为矢量图(柱状图),用 get_drawings() bbox + caption 坐标确定裁剪区域
  • Figure 5 为光栅图(截图),用 get_image_rects() 定位
  • 所有图片 3x 渲染后 60-90KB,质量良好

微信公众号文章(mp.weixin.qq.com)专项优化

脚本对微信公众号文章有专门的检测和处理策略:

  1. 自动检测:识别 mp.weixin.qq.com 域名,自动启用微信模式
  2. 无需登录:微信公众号文章是公开可读的,跳过登录检测和 Cookie 注入流程
  3. 专用内容选择器:使用 #js_content / .rich_media_content 精准定位正文区域(而非通用选择器可能匹配到页面其他内容)
  4. 标题提取#activity-name > h1.rich_media_title > 通用 h1 > meta 标签回退
  5. 作者提取#js_name(公众号名称)> .rich_media_meta_nickname > 通用选择器回退
  6. 日期提取#publish_time > 通用 time/date 选择器回退
  7. 图片懒加载增强
    • 微信图片使用 data-src + IntersectionObserver 懒加载
    • 滚动速度放慢(300px 步长、200ms 间隔)以确保触发所有 IntersectionObserver
    • 强制将未触发的 data-src 复制到 src(兜底策略)
    • 图片下载时优先使用 data-src 的高清原图 URL
  8. 图片格式识别:微信图片 URL 格式特殊(mmbiz.qpic.cn/...?wx_fmt=png),从 wx_fmt 查询参数推断文件扩展名
  9. Referer 防盗链:通过 Playwright 页面上下文的 page.request.get() 下载图片,自动携带正确的 Referer 头

Substack 站点(如 www.lennysnewsletter.com)专项优化**: 脚本对 Substack 托管的站点(*.substack.comlennysnewsletter.com 等)有专门的登录检测和登录态缓存**机制:

  1. 登录态缓存:登录成功后自动保存 Playwright storage_state~/.substack/storage_state.json,后续抓取直接复用,无需重复登录和邮箱验证
  2. 优先级:缓存 storage_state > Chrome cookies > 引导登录
  3. 自动检测登录状态:加载页面后检查右上角是否有用户头像(已登录)还是 "Sign in" 按钮(未登录)
  4. 已登录 → 直接抓取全文,并刷新缓存延长有效期
  5. 缓存过期 → 自动清理旧缓存,进入引导登录流程
  6. 未登录 → 打开可见浏览器窗口引导登录,用户在终端输入 y 确认后二次验证,通过后自动缓存

独立登录命令(推荐首次使用时先执行):

python scripts/fetch_article.py login

此命令单独完成 Substack 登录并缓存,不需要指定文章 URL。后续所有 Substack 文章抓取都会自动复用此登录态。

非 Substack 站点的登录确认机制

  • 无 Chrome cookies 时自动切换到非无头模式,打开可见浏览器窗口
  • 终端提示用户完成登录操作后按回车键继续
  • 收到确认信号后重新加载页面并检测付费墙状态

付费墙检测:脚本同时检测以下信号:

  • DOM 元素:[data-testid="paywall"].paywall
  • 文本关键词:This post is for paid subscribersSubscribe to readUpgrade to paid
  • 注意:不同网站的付费墙 DOM 结构和关键词不同,如遇新网站抓取不完整,需检查页面实际的付费墙标识并更新检测逻辑

判断内容是否完整的方法

  • 先用 web_fetch 尝试获取,如果明显被截断(内容不完整、出现付费提示),则切换到 fetch_article.py
  • 抓取完成后必须告知用户查看 article.md 确认内容完整性
  • 关注文章末尾是否有作者署名/总结段落作为完整性标志
  • 如果用户反馈内容不完整,检查:(1) 登录账号是否有付费权限 (2) 页面是否有懒加载内容未触发 (3) 内容选择器是否匹配到了免费预览区而非全文区

产出物

  • <项目子目录>/<原文标题>.md — 完整文章 Markdown(含图片引用)
  • <项目子目录>/<原文标题>_meta.json — 结构化元信息(原文链接、作者、发布时间、抓取时间等)
  • <项目子目录>/images/ — 所有文章配图

<原文标题>_meta.json 格式:

{
  "url": "原文链接",
  "title": "文章标题",
  "subtitle": "副标题",
  "author": "作者",
  "date": "发布时间",
  "content_length": 12345,
  "image_count": 5,
  "images": ["images/img_01_xxx.png", ...],
  "fetched_at": "2026-02-25T10:30:00"
}

X.com / Twitter 帖子抓取(必须用 CDP 模式)

X.com 是登录墙网站的典型代表web_fetch 和普通 Cookie 注入模式都无法抓取,必须使用 CDP 模式

# CDP 模式(必须)
python scripts/fetch_article.py fetch "https://x.com/<username>/status/<id>" --output-dir <项目子目录> --cdp

CDP 模式工作原理

  1. 通过 Chrome DevTools Protocol (port 9222) 连接用户真实 Chrome 浏览器
  2. 复用浏览器中已登录的 X 账号会话
  3. 绕过自动化浏览器检测(X 会检测并阻止 Playwright/Selenium)

CDP 模式前置条件

# 启动 Chrome 并开启 CDP 端口
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 &

# 验证
curl -s http://localhost:9222/json/version

X.com 抓取的特殊处理

  1. 帖子内容会转换为 Markdown 格式
  2. 图片(帖子中的媒体)会下载到 images/ 目录
  3. 帖子中的链接会转换为 Markdown 链接格式
  4. 转发数、点赞数等元信息会保留

产出物

  • <项目子目录>/<原文标题>.md — 帖子 Markdown
  • <项目子目录>/<原文标题>_meta.json — 元信息
  • <项目子目录>/images/ — 帖子中的媒体图片

微博帖子抓取(必须用 CDP 模式)

微博是强制登录墙网站,所有端口(PC 端 weibo.com、移动端 m.weibo.cn、API m.weibo.cn/statuses/show)均需要登录态,web_fetch 和普通 Playwright 都会被重定向到 Sina Visitor System 登录页。必须使用 CDP 模式

抓取命令

# CDP 模式(必须)— 连接本地已登录 Chrome
python scripts/fetch_article.py fetch "https://weibo.com/<uid>/<mid>" --output-dir <项目子目录> --cdp

完整流程

  1. 确保 Chrome 已开启 CDP 端口(port 9222)且已登录微博
  2. 运行 fetch_article.py --cdp:脚本会连接真实 Chrome,复用微博登录态
  3. CDP 连接失败时的自动降级:脚本会回退到 Cookie 注入模式(从 Chrome Cookie DB 提取 cookies),但微博 Cookie 注入通常也能工作
  4. 抓取后整理内容:微博原始 HTML 结构较乱,抓取结果中可能包含导航、按钮等噪音文本,需要手动清理或用 AI 整理为结构化 Markdown
  5. 转存乐享:使用 entry_import_content 创建页面(非 file_create_hyperlink,后者仅支持微信公众号链接)

微博抓取的特殊注意事项

问题说明
web_fetch 失败微博强制登录,WebFetch 会被重定向到 passport.weibo.com/visitor/visitor
Playwright 失败微博检测 HeadlessChrome UA,即使用 --browser=chrome 也会被拦截
CDP 前置条件Chrome 必须已开启 --remote-debugging-port=9222 且已登录微博
内容整理微博页面标题通常是「微博正文 - 微博」,转存时应提取作者名和关键主题作为标题
图片处理微博图片使用 sinaimg.cn CDN,fetch_article.py 可以下载,但部分图片可能需要 Referer

产出物

  • <项目子目录>/article.md — 微博内容 Markdown(注意:微博标题通常是通用的,需手动重命名)
  • <项目子目录>/article_meta.json — 元信息
  • <项目子目录>/images/ — 微博中的图片(如有)

转存乐享时的标题建议: 微博原始标题是「微博正文 - 微博」,转存时应改为有意义的标题,格式建议:<作者>:<主题关键词>,例如:

  • 唐杰THU:最近的一些想法(AI 技术趋势)
  • 李飞飞:关于 Spatial AI 的思考

英文文章翻译为中英对照

对于英文文章(如 X 帖子、英文博客等),可以使用 OpenAI API 翻译为中英对照格式:

翻译脚本 (scripts/translate_article.py):

python scripts/translate_article.py <原文.md> <输出.md> --model gpt-4o-mini

翻译格式

## 英文标题

[英文原文段落]

[中文翻译]

## 第二节英文标题

[英文原文...]

[中文翻译...]

翻译工作流

  1. 先用 fetch_article.py 抓取原文
  2. translate_article.py 翻译为中英对照
  3. 将翻译后的 Markdown 上传到乐享知识库

依赖

  • OPENAI_API_KEY 环境变量

使用 web_fetch 获取的免费文章

对于通过 web_fetch 获取到完整内容的免费文章,同样需要保存原文

  1. 保存原文全文:将 web_fetch 返回的内容直接保存为 Markdown,不做总结、不做摘要、不做改写,保持原文的完整结构和措辞
  2. 文件名使用原文标题:<项目子目录>/<原文标题>.md
  3. 手动构建 <原文标题>_meta.json,包含 URL、标题、作者、日期等元信息
  4. 如果文章包含图片,尽量下载保存到 <项目子目录>/images/

关键区分web_fetch 工具可能会返回总结/摘要版本而非原文全文。如果返回的内容明显是总结(缺少原始段落、引用、细节),需要在 web_fetch 调用时明确要求"返回完整原始全文内容,不要总结或缩写"。保存到本地的必须是原文全文,而不是经过 AI 总结的摘要。

YouTube 视频处理(yt-dlp + Whisper + 翻译 + 乐享)

当用户提供 YouTube 视频链接时,使用 yt_download_transcribe.py 脚本完成完整的下载-转录-翻译-归档工作流。

⚠️ 重要不要使用 web_fetch(无法获取视频内容),不要使用 NotebookLM(已替换为本地 Whisper 方案,速度更快、无外部依赖)。

工作流概述

  1. yt-dlp 下载视频 → 本地 .mp4 文件
  2. ffmpeg 提取音频 → WAV 格式(16kHz 单声道)
  3. Whisper 转录 → 带时间戳的文字稿
  4. AI 翻译(如果是英文)→ 中英对照格式的 Markdown
  5. 上传乐享知识库
    • 文字稿:以在线文档(page)格式上传,支持后续按块维度编辑更新
    • 视频文件:以文件(file)格式上传
  6. 清理:上传成功后删除本地视频文件

Step 1:下载 + 转录 + 翻译

cd <项目子目录>

# 完整流程(下载 + 转录 + 翻译)
python3 scripts/yt_download_transcribe.py "<YouTube URL>" \
  --output-dir . \
  --whisper-model base

# 常用参数:
#   --whisper-model tiny|base|small|medium|large  转录模型(越大越准但越慢)
#   --skip-download    跳过下载(用于重新转录已下载的视频)
#   --skip-translate   跳过翻译步骤
#   --keep-audio       保留提取的音频文件

产出物

  • <视频标题>.mp4 — 下载的视频文件
  • <视频标题>.md — 文字稿 Markdown(英文视频为中英对照格式)
  • <视频标题>_meta.json — 视频元信息

文字稿格式(英文视频,中英对照):

# 视频标题

**频道**: xxx
**发布日期**: 2026-03-10
**时长**: 15:30
**原始链接**: https://www.youtube.com/watch?v=xxx
**转录语言**: en

---

## 文字稿(中英对照)

> 以下内容采用「英文原文 + 中文翻译」对照排列。

**[00:00]**

This is the original English text from the video...

这是视频中的中文翻译文本...

**[01:23]**

Next paragraph of English text...

下一段中文翻译...

Whisper 模型选择建议

模型速度精度适用场景
tiny最快较低快速预览、非关键内容
base中等默认推荐,适合大部分场景
small中等较高口音较重、背景噪音较多
medium重要内容、需要高精度
large最慢最高专业内容、学术演讲

Step 2:上传到乐享知识库

通过 lexiang MCP 工具完成上传,流程与 Step 2(普通文章转存乐享)一致。前提是 lexiang MCP 已连接(参见 Step 2 的「乐享 MCP 工具的调用方式」章节)。

文字稿上传(在线文档 page 类型):

  1. 获取知识库根节点 → 检查/创建日期目录(同上述步骤 1-3)
  2. 调用 entry_import_content(参数:space_id, parent_id=<日期目录ID>, name="<视频标题>", content=<文字稿Markdown内容>, content_type="markdown"
    • ⚠️ 此接口不支持 after 参数,新建文档会追加到目录末尾,无法控制位置
  3. 在线文档支持后续在乐享中按块维度编辑更新(如修正翻译)

视频文件上传(🚨 推荐使用 OpenAPI 路径,MCP 的 file_apply_upload 产生不可播放的 file 条目):

# ✅ 推荐:通过 OpenAPI 上传,产生 entry_type=video,乐享会 VOD 转码,真能播放
python3 scripts/upload_video_via_openapi.py "<视频路径>.mp4" \
    --space-id <space_id> \
    --parent-entry-id <父节点 entry_id> \
    --media-type video

需要在 ~/.lexiang/openapi.json 配置 AppKey/AppSecret/StaffID(不入 git)。

OpenAPI 正确流程(脚本已封装):

  1. POST /cgi-bin/v1/kb/files/upload-params(body: {"name":"xxx.mp4","media_type":"video"})→ 获取 VOD 上传签名 + state
  2. PUT <bucket>.cos.<region>.myqcloud.com/<key> → 上传到 VOD COS
  3. POST /cgi-bin/v1/kb/entries?space_id=xxx&state=xxx(body: entry_type=video, name=xxx.mp4)→ 创建可播放视频节点

🚨 关键踩坑(2026-05-03 实战总结)

  • ❌ 不要用 MCP 的 file_apply_upload——产物是 entry_type=file + extension=video,不触发 VOD 转码,视频无法播放
  • ❌ 不要用 /cgi-bin/v1/docs/cos-param 签名接口——它只支持 attachment/file,签发的 state 不能用于创建 entry_type=video
  • 必须用 /cgi-bin/v1/kb/files/upload-params——支持 media_type=video/audio/file,签发的 state 可用于 kb/entries
  • name 参数必须带文件后缀.mp4 等),否则报"name 需指定文件后缀"
  • kb/entries 接口用 x-staff-id(小写带连字符),不是 StaffID

备用:MCP 三步流程(仅适用于非视频文件,如 PDF)

  1. file_apply_upload(参数:parent_entry_id=<日期目录ID>, name="<文件名>.pdf", size=<文件字节数>, mime_type="application/pdf", upload_type="PRE_SIGNED_URL"
  2. curl -X PUT "<upload_url>" -H "Content-Type: application/pdf" --data-binary "@<文件路径>"
  3. file_commit_upload(参数:session_id=<上一步返回的session_id>

上传成功后:自动删除本地视频文件(rm -f <视频文件路径>),节省磁盘空间。

依赖

  • yt-dlp推荐 brew install yt-dlp,不要用 pip3 install)— YouTube 视频下载。必须用 brew 安装以获取最新版本,pip 版本受限于系统 Python 版本(如 Python 3.9 无法安装 nightly 版),而 brew 版自带独立 Python 环境
  • openai-whisperpip3 install openai-whisper)— 音频转录
  • ffmpegbrew install ffmpeg)— 音频提取
  • openaipip3 install openai)— 翻译(需要 OPENAI_API_KEY 环境变量)。如果没有 API Key,可以跳过翻译步骤,由 AI 助手直接在对话中翻译后更新文档

播客音频处理(yt-dlp + Whisper + 乐享)

当用户提供播客链接时(小宇宙FM xiaoyuzhoufm.com、Apple Podcasts 等),使用 yt-dlp 下载音频 + Whisper 转录的方式处理。

⚠️ 重要:yt-dlp 的 generic extractor 可以从播客页面中自动提取音频 URL(m4a/mp3),不需要 cookies,也不需要专门的播客 extractor。

工作流概述

  1. yt-dlp 下载音频 → 本地 .m4a.mp3 文件(播客没有视频,直接是音频)
  2. ffmpeg 提取/转换音频 → WAV 格式(16kHz 单声道,Whisper 推荐)
  3. Whisper 转录 → 带时间戳的文字稿
  4. 繁简转换(如需要)→ Whisper base 模型对中文会输出繁体,需用 opencc 转为简体
  5. 上传乐享知识库(通过 lexiang MCP 工具):
    • 文字稿:entry_import_content 创建为在线文档(page)格式
    • 音频文件:file_apply_uploadcurl PUTfile_commit_upload 三步上传

Step 1:下载音频

cd <项目子目录>

# yt-dlp 直接下载播客音频(不需要 cookies)
yt-dlp --no-playlist -o "%(title)s.%(ext)s" "<播客链接>"

小宇宙链接格式https://www.xiaoyuzhoufm.com/episode/<episode_id> yt-dlp 会通过 generic extractor 自动从页面中提取 media.xyzcdn.net 的音频直链。

Step 2:提取 WAV + Whisper 转录

# 提取 WAV(16kHz 单声道)
ffmpeg -i "<音频文件>.m4a" -vn -acodec pcm_s16le -ar 16000 -ac 1 -y "<音频文件>.wav"

# Whisper 转录(中文播客指定 language=zh)
python3 -c "
import whisper, json, time
model = whisper.load_model('base')
result = model.transcribe('<音频文件>.wav', language='zh', verbose=False)
with open('whisper_segments.json', 'w', encoding='utf-8') as f:
    json.dump(result['segments'], f, ensure_ascii=False, indent=2)
print(f'Done: {len(result[\"segments\"])} segments')
"

Step 3:合并段落 + 繁简转换 + 生成 Markdown

使用与 YouTube 视频相同的段落合并逻辑(max_gap=1.5s, max_duration=30s,遇句末标点+gap>0.8s 断开)。

关键:Whisper base 模型对中文普通话倾向输出繁体字,必须用 opencc 进行繁简转换:

pip3 install opencc-python-reimplemented
import opencc
converter = opencc.OpenCC("t2s")
simplified_text = converter.convert(traditional_text)

文字稿 Markdown 格式(中文播客):

# 播客标题

> 播客:节目名 | 平台:小宇宙FM
> 嘉宾:xxx | 主播:xxx
> 发布日期:YYYY-MM-DD | 时长:xx分xx秒
> 原始链接:https://www.xiaoyuzhoufm.com/episode/xxx
> 转录工具:Whisper base + OpenCC 繁简转换

---

## Part 1:章节标题

**[00:00]** 第一段转录文本,由多个 Whisper segment 合并而成...

**[01:23]** 第二段转录文本...

## Part 2:章节标题

**[15:30]** 第三段转录文本...

文字稿整理规范(🚨 必须遵守,避免格式混乱)

核心原则:Whisper 输出的 segments 是细碎的短句(通常每条1-5秒),必须先合并为自然段落,再插入章节标题和时间戳。直接按 segment 粒度插入标题会导致同一个标题在每个短句前重复出现。

段落合并策略

🚨 关键 bug 修复(2026-05-08):Whisper base 对中文输出几乎没有句号等标点,因此"句末标点断开"条件基本不会触发。唯一有效的断开条件是 duration 和 gap。必须确保 duration 计算正确:duration = 当前 segment 的 end - 段落起始 cur_start

  1. 相邻 segment 间隔 > 1.0s → 强制断开
  2. 累计时长 > 15s(seg.end - cur_start > 15)→ 强制断开
  3. 遇到句号、问号等句末标点 + gap > 0.5s → 断开为新段落
  4. 合并后的段落开头标注时间戳 **[MM:SS]**

⚠️ 参数选择依据

  • max_duration=15s 而非 30s/60s:因为中文 Whisper 没有标点输出,只能靠 duration 强制切割。15s 约 200 字/段,阅读体验较好
  • max_gap=1.0s:对话中的自然停顿通常 > 1s
  • 目标:48 分钟播客应产出 150-200 段(平均 90-100 字/段)

合并代码参考

paragraphs = []
cur_text = ""
cur_start = 0
cur_end = 0

for seg in segments:
    start, end, text = seg["start"], seg["end"], seg["text"].strip()
    if not text:
        continue
    if cur_text:
        gap = start - cur_end
        duration = end - cur_start  # ⚠️ 必须用 end 而非 cur_end
        if duration > 15 or gap > 1.0:
            paragraphs.append({"start": cur_start, "end": cur_end, "text": cur_text.strip()})
            cur_text, cur_start, cur_end = text, start, end
        else:
            cur_text += text
            cur_end = end
    else:
        cur_text, cur_start, cur_end = text, start, end
if cur_text:
    paragraphs.append({"start": cur_start, "end": cur_end, "text": cur_text.strip()})

章节标题插入策略(🚨 关键,避免重复)

  1. 从播客简介/shownotes 中提取章节时间线
  2. 将章节时间点转换为秒数,建立映射
  3. 每个标题只插入一次:用 inserted_headers = set() 跟踪已插入的标题
  4. 在段落合并完成后,根据段落起始时间匹配最近的章节标题
  5. 匹配条件:段落起始时间 >= 章节时间点该标题尚未插入

常见错误(必须避免)

  • ❌ 在每个 Whisper segment 级别插入章节标题 → 同一标题重复几十次
  • ❌ 用宽松时间容差匹配(如 abs(start - ts) < 5)→ 多个 segment 命中同一标题
  • ❌ 不跟踪已插入状态 → 标题被重复插入
  • ✅ 先合并 segments 为段落,再在段落级别插入标题,每个标题只插入一次

Step 4:上传到乐享知识库

与 YouTube 视频处理相同的流程(通过 lexiang MCP 工具完成,前提是 MCP 已连接):

  1. 获取知识库根节点 → 检查/创建日期目录
  2. 文字稿使用 entry_import_content_to_entry 创建为在线文档(page 类型)不要直接上传 .md 文件(排版会乱,用户无法正常阅读)
  3. 音频文件必须使用 upload_video_via_openapi.py --media-type audio(走 OpenAPI VOD 路径),不要用 MCP 的 file_apply_upload(产生 entry_type=file,无法在线播放)

文字稿在线文档导入方法(🚨 分块导入,避免内容丢失)

核心问题:播客文字稿通常 15-25K chars,无法在单次 MCP 工具调用中传入全部内容。必须分块导入

分块导入流程

  1. 先用 entry_create_entryentry_type="page")创建空白 page,获取 entry_id
  2. 将 markdown 内容按行分块,每块 ≤ 4000 chars(确保没有超长单行)
  3. 第一块entry_import_content_to_entryentry_id=<page_id>, force_write=true, content=<第一块>
  4. 后续块entry_import_content_to_entryentry_id=<page_id>, force_write=false, content=<后续块>)— 追加到末尾
  5. 验证:调用 entry_describe_ai_parse_content 确认内容完整(检查最后一个时间戳是否接近播客总时长)

⚠️ 关键注意事项

  • 每块内容必须是完整的 markdown 结构(不要在标题或段落中间切断)
  • 如果文字稿中有单行超过 4000 chars 的情况(说明合并策略有 bug),需要回到 Step 3 修复合并逻辑
  • 48 分钟播客(~200 段 × ~100 字/段 = ~20K chars)通常需要分 5-6 块导入
# ✅ 正确:通过 OpenAPI 上传音频(产生 entry_type=audio,触发 VOD 转码可播放)
python3 scripts/upload_video_via_openapi.py "<音频文件>.m4a" \
    --space-id <space_id> \
    --parent-entry-id <日期目录 entry_id> \
    --media-type audio

🚨 关键踩坑(2026-05-08 实战验证)

  • file_apply_upload + curl PUT + file_commit_upload → 产出 entry_type=file,音频无法播放
  • upload_video_via_openapi.py --media-type audio → 产出 entry_type=audio,乐享自动 VOD 转码,可在线播放

播客 vs YouTube 的关键区别

维度YouTube 视频播客音频
文件格式.mp4(视频).m4a/.mp3(纯音频)
文件大小较大(HLS 720p ~500MB)较小(~60MB/小时)
下载方式需要 HLS 格式避免 403直接下载,无反爬
cookies通常需要不需要
Whisper 语言通常是英文(需翻译)通常是中文(需繁简转换)
上传 MIMEvideo/mp4audio/mp4audio/mpeg

依赖(额外):

  • opencc-python-reimplementedpip3 install opencc-python-reimplemented)— 繁体转简体(Whisper base 模型中文输出为繁体时需要)

结构化分析

输出结构化分析:

【文章主题】一句话概括
【核心论点】3-5 个关键观点
【关键数据】文章中的重要数据/图表
【利益相关】作者/机构的立场与潜在倾向(如有)
【原文出处】完整标题 + URL

规划图表:第 1 张为总览图,第 2-N 张各聚焦 1 个核心论点。向用户确认图表数量和主题划分。

Step 2:原文保存到乐享知识库

在进入信息图生成流程之前,先将原文完整保存到乐享知识库,确保素材归档和可追溯。

配置文件与初始化

本 skill 的目标知识库等信息通过配置文件管理,不在 SKILL.md 中硬编码

配置文件路径:config.json(位于 skill 根目录,即与本 SKILL.md 同级)

对话式配置初始化(首次使用时自动触发)

config.json_initializedfalsespace_id 为空时,在执行任何乐享操作前,必须先通过对话引导用户完成配置。

核心设计:用户只需要粘贴一个乐享知识库链接,Agent 自动完成所有配置。

链接格式https://<domain>/spaces/<space_id>?company_from=<company_from>

  • 示例:https://lexiangla.com/spaces/b6013f6492894a29abbd89d5f2e636c6?company_from=e6c565d6d16811efac17768586f8a025
  • 从链接中可解析出三个关键信息:域名lexiangla.com)、space_idcompany_from

流程如下

第一步:检测 MCP 连接

  1. 尝试调用任意一个 lexiang MCP 工具(如 whoami)检测 MCP 是否已连接
  2. 如果调用成功 → MCP 已连接,进入第二步
  3. 如果调用失败(MCP 未连接)→ 引导用户完成 MCP 鉴权:
    ⚠️ 乐享 MCP 尚未连接。请先完成鉴权配置:
    
    1. 访问 https://lexiangla.com/mcp 登录后获取 COMPANY_FROM 和 LEXIANG_TOKEN
    2. 按照你使用的 Agent 配置 MCP 连接:
       - CodeBuddy:在 MCP 管理面板中添加 lexiang server
       - OpenClaw:运行 claw install https://github.com/tencent-lexiang/lexiang-mcp-skill
       - 其他 Agent:在 MCP 配置文件中添加 lexiang server
    3. 完成后告诉我,我会继续配置流程。
    
    不要继续后续步骤,等待用户完成 MCP 连接后重试。

第二步:请求用户提供知识库链接

  1. 向用户发送引导消息:
    🔧 首次使用,需要配置目标知识库。
    
    请粘贴你想用来归档文章的乐享知识库链接,格式如:
    https://lexiangla.com/spaces/xxxxx?company_from=yyyyy
    
    💡 获取方式:在乐享中打开目标知识库,复制浏览器地址栏中的链接即可。
    
  2. 等待用户输入,不要自行猜测或列举知识库

第三步:解析链接并验证

  1. 从用户提供的链接中用正则解析出三个字段:
    • domain:链接的域名部分(如 lexiangla.com),用于生成后续访问链接
    • space_id/spaces/ 后面的路径段(如 b6013f6492894a29abbd89d5f2e636c6
    • company_fromcompany_from= 参数值(如 e6c565d6d16811efac17768586f8a025
  2. 如果链接格式不正确(缺少 space_idcompany_from)→ 提示用户重新粘贴正确的链接
  3. 调用 space_describe_space(参数:space_id=<解析出的 space_id>)验证知识库是否存在
  4. 如果验证失败 → 提示用户检查链接是否正确或是否有该知识库的访问权限

第四步:写入配置并确认

  1. 将解析和验证得到的信息写入 config.json
    • lexiang.target_space.space_id = 解析出的 space_id
    • lexiang.target_space.space_name = 从 space_describe_space 返回值获取的知识库名称
    • lexiang.target_space.company_from = 解析出的 company_from
    • lexiang.access_domain.domain = 解析出的域名
    • lexiang.access_domain.page_url_template = https://<domain>/pages/{entry_id}?company_from={company_from}
    • lexiang.access_domain.space_url_template = https://<domain>/spaces/{space_id}?company_from={company_from}
    • _initialized = true
  2. 向用户确认配置结果:
    ✅ 配置完成!
    
    📚 目标知识库:<知识库名称>
    🔗 访问链接:https://<domain>/spaces/<space_id>?company_from=<company_from>
    
    后续抓取的文章将自动归档到此知识库。如需更换,告诉我「重新配置知识库」即可。
    
重新配置

当用户说「重新配置知识库」、「切换知识库」、「更换目标知识库」等类似意图时:

  1. config.json_initialized 设为 false
  2. 重新执行上述对话式初始化流程(从第一步开始)
用户输入容错

用户可能不会粘贴完美的链接,需要处理以下情况:

用户输入处理方式
完整链接 https://lexiangla.com/spaces/xxx?company_from=yyy直接解析 ✅
不带 company_from 的链接 https://lexiangla.com/spaces/xxx提示:「链接中缺少 company_from 参数。请在乐享中重新复制完整链接(地址栏中通常会包含 ?company_from=xxx),或者访问 https://lexiangla.com/mcp 获取你的 COMPANY_FROM 值告诉我。」
纯 space_id b6013f6492894a29abbd89d5f2e636c6提示:「请提供完整的知识库链接(包含 company_from 参数),我需要从链接中同时获取知识库 ID 和企业标识。」
页面链接 https://lexiangla.com/pages/xxx提示:「这是一个页面链接,请提供知识库链接(格式:https://lexiangla.com/spaces/xxx?company_from=yyy)。你可以在乐享中进入目标知识库首页,复制地址栏链接。」
返回的文档链接打不开/无权限链接中缺少 company_from 参数。页面链接必须带 ?company_from=xxx,格式:https://lexiangla.com/pages/<entry_id>?company_from=<company_from>
配置结构
{
  "_initialized": false,
  "lexiang": {
    "target_space": {
      "space_id": "",
      "space_name": "",
      "company_from": ""
    },
    "access_domain": {
      "domain": "lexiangla.com",
      "page_url_template": "https://lexiangla.com/pages/{entry_id}?company_from={company_from}",
      "space_url_template": "https://lexiangla.com/spaces/{space_id}?company_from={company_from}"
    }
  }
}

access_domain 会从用户粘贴的链接中自动提取域名,无需手动配置。适配自定义域名的乐享部署。

后续文档中所有 <SPACE_ID><COMPANY_FROM><ACCESS_DOMAIN> 等占位符,均指从 config.json 中读取的实际值。

乐享 MCP 工具的调用方式(重要 — 多 Agent 适配)

本 skill 需要服务多个 Agent 产品(OpenClaw、CodeBuddy、Claude Desktop 等)。不同 Agent 连接乐享 MCP 的方式不同,但暴露的工具名称和参数完全一致(都是 lexiang MCP server 提供的标准工具)。

核心原则:本 skill 只描述「调用哪个工具 + 传什么参数」,不规定具体的 MCP 调用语法。每个 Agent 按自己的方式调用即可。

各 Agent 产品的 MCP 连接方式

Agent 产品lexiang MCP 连接方式工具调用方式
CodeBuddy~/.codebuddy/mcp.json 中配置 lexiang server,通过 IDE 的 MCP 管理面板启用连接直接调用 space_describe_spacefile_apply_upload 等 lexiang MCP 工具
OpenClawclaw install https://github.com/tencent-lexiang/lexiang-mcp-skill,加载 skill 时自动连接 MCP同上,通过 skill 暴露的 MCP 工具调用
Claude Desktop / 其他 MCP 兼容 Agent在 Agent 的 MCP 配置文件中添加 lexiang server URL同上

MCP 连接检测与降级

在执行乐享操作前,必须先检测 lexiang MCP 是否已连接

  1. 读取 config.json,检查 _initializedlexiang.target_space.space_id
  2. 如果未初始化 → 先触发对话式配置初始化(参见上方「对话式配置初始化」),初始化流程中会自动完成 MCP 连接检测
  3. 如果已初始化,尝试调用 space_describe_space(参数:space_id=<config 中的 space_id>)验证 MCP 连接
  4. 如果调用成功 → MCP 已连接,继续后续流程
  5. 如果调用失败(MCP 未连接)→ 提示用户检查 MCP 连接,给出对应 Agent 的操作指引:
    • CodeBuddy:「请在 MCP 管理面板中确认 lexiang server 已启用并显示为已连接状态」
    • OpenClaw:「请确认已安装 lexiang skill(claw install https://github.com/tencent-lexiang/lexiang-mcp-skill)」
    • 其他 Agent:「请确认 MCP 配置中已添加 lexiang server」

⚠️ 禁止降级为 curl 调用 REST API:即使 MCP 未连接,也不要自行编写 curl 调用乐享 REST API,因为:(1) 认证信息硬编码在 curl 中不安全;(2) 不同 Agent 的执行环境差异大,curl 方式不通用;(3) REST API 的 URL 格式和鉴权方式可能变化。应该引导用户修复 MCP 连接。

认证配置(首次使用时需要):

  1. 访问 https://lexiangla.com/mcp 登录后获取 LEXIANG_TOKEN(访问令牌,格式:lxmcp_xxx

    COMPANY_FROM 无需手动获取 — 会从用户粘贴的知识库链接中自动解析

  2. 配置方式(二选一):

    • 环境变量(推荐):export LEXIANG_TOKEN="lxmcp_xxx"
    • 直接修改 MCP 配置:将 MCP server URL 中的 ${LEXIANG_TOKEN} 占位符替换为实际值
  3. 详细配置步骤参见:lexiang-mcp-skill setup.md

目标知识库

config.jsonlexiang.target_space 中读取:

  • 知识库名称config.lexiang.target_space.space_name
  • 知识库访问链接:按 config.lexiang.access_domain.space_url_template 格式拼接
  • Space IDconfig.lexiang.target_space.space_id

⚠️ 访问链接域名:用户可访问的乐享前端域名从 config.lexiang.access_domain.domain 读取(默认为 lexiangla.com),不是 mcp.lexiang-app.com(后者是 MCP API 服务端域名,浏览器无法直接访问)。所有展示给用户的链接必须按 config.lexiang.access_domain.page_url_template 格式生成。

目录组织方式

天维度组织目录:

知识库根目录/
  2026-02-25/
    文章标题A      (图文文章,在线文档 page 类型,图片内嵌)
    文章标题B      (纯文本文章,在线文档 page 类型)
  2026-02-26/
    文章标题C      (在线文档 page 类型)

⚠️ 默认格式:所有文章(无论是否含图片)统一使用在线文档(page)格式上传。在线文档支持在乐享中直接编辑、划词评论、全文检索,体验远优于 PDF。PDF 仅作为降级方案(md_to_page.py 失败时)或用户明确要求时使用。

操作流程

⚠️ 严格按步骤顺序执行,不得跳步! 必须完成步骤 0→1→2→3→4 的完整流程。尤其是步骤 2(创建日期目录)不可跳过——文档必须上传到当天日期命名的文件夹中,而不是直接上传到知识库根目录。如果跳过步骤 2 直接用 root_entry_id 作为上传目标,文档将错误地出现在根目录下。

通过 lexiang MCP 工具,按以下步骤完成转存:

步骤 0:读取配置(含初始化检测)

  • 读取 skill 目录下的 config.json 文件
  • 检查 _initialized 是否为 truelexiang.target_space.space_id 非空
  • 如果未初始化_initializedfalsespace_id 为空)→ 触发对话式配置初始化流程(参见上方「对话式配置初始化」),完成后再继续
  • 提取 lexiang.target_space.space_idlexiang.access_domain.page_url_template 等配置项

步骤 1:获取知识库根节点

  • 调用 space_describe_space(参数:space_id=<config 中的 SPACE_ID>
  • 从返回结果中提取 root_entry_id

步骤 2:检查/创建当天日期目录(🚨 必须先查再建,禁止直接创建)

🚨 这是本 skill 最常见的错误! 2026-05-11 实战中,Agent 未查询直接创建了同名目录。每次执行到此步骤,必须严格按照下方决策树执行,绝对禁止跳过查询直接调用创建工具。


🚨 执行前必读:两种常见错误

#错误做法 ❌正确做法 ✅
1直接调用 mcp__lexiang__entry_create_entry 创建文件夹先调用 mcp__lexiang__entry_list_children 查询根目录
2只匹配 name=="2026-05-11",不检查 entry_type必须同时匹配 name=="2026-05-11" entry_type=="folder"

决策树(必须逐条执行,不可跳步):

步骤 2a:查询根目录
  工具:mcp__lexiang__entry_list_children
  参数:{"parent_id": "<root_entry_id>"}

  遍历返回的 entries[] 数组:
    查找是否有 entry_type=="folder" 且 name=="当天日期" 的条目
    例如今天 2026-05-11 → 查找 name=="2026-05-11" 且 entry_type=="folder"

  如果找到 → 记录其 id → 【跳到步骤 3,不创建】
  如果没找到 → 【继续到步骤 2b】

步骤 2b:确认不存在后,才能创建
  调用:mcp__lexiang__entry_create_entry
  参数:{"entry_type": "folder", "parent_entry_id": "<root_entry_id>", "name": "当天日期"}

  🚨 创建后必须置顶(否则新目录会出现在末尾):
  1. 先调用 entry_list_children 获取父目录当前第一个条目的 entry_id
  2. 调用 entry_move_entry,使用 **before** 参数传入第一个条目的 entry_id,将新目录移到它之前(即置顶)
  参数:{"entry_id": "<新建的entry_id>", "parent_id": "<root_entry_id>", "before": "<当前第一个条目的entry_id>"}
  
  ⚠️ 注意事项:
  - after="" 实测是移到末尾(非置顶),API 文档描述有误,禁止使用
  - 必须用 before=<第一个条目ID> 才能真正置顶
  - 这一步不可省略!创建目录后如果不执行 move,新目录会默认出现在最底部

📌 关于分页的说明:日期目录按创建时间倒序排列,当天的目录如果存在一定在第一页,无需处理分页。但如果你在处理非日期目录的场景(如查找某个不确定的条目),应注意 next_page_token 的存在。


❌ 错误示例(禁止这样做):

# 错误:直接创建,不查询
→ 调用 mcp__lexiang__entry_create_entry,参数 name="2026-05-11"
→ 结果:知识库中出现多个同名 "2026-05-11" 文件夹

✅ 正确示例(必须这样做):

# 正确:先查询第一页,找到就用,找不到才创建
→ 调用 mcp__lexiang__entry_list_children,参数 {"parent_id": "<root_entry_id>"}
→ 遍历 entries,检查是否有 name=="2026-05-11" 且 entry_type=="folder"
→ 找到 → 记录 id,跳过创建,直接进入步骤 3
→ 没找到 → 调用 mcp__lexiang__entry_create_entry 创建

步骤 3:去重检查

  • 调用 entry_list_children(参数:parent_id=<日期目录ID>)查询该日期目录下已有的条目
  • 按「名称 + 类型」检查是否已存在同名文档,如果已存在则跳过上传并告知用户

步骤 3.5:非中文文章翻译(🚨 强制检查,不可跳过)

⚠️ 重要:无论文章是通过 fetch_article.pyweb_fetch、PDF 下载还是其他方式获取,在上传到乐享之前都必须经过语言检测和翻译步骤。这是一个强制检查点,不存在任何可以跳过的"简化路径"。

PDF 特别说明:PDF 文件(包括英文学术论文)同样适用此规则。非中文 PDF 必须先翻译为中英对照格式后再转存乐享,详见上方「PDF 文件/链接处理」章节的 Step D。

常见遗漏场景

  1. ❌ 用 web_fetch 抓取后直接转 PDF 上传 → 英文原文未翻译
  2. ❌ 觉得文章"看起来不长"就跳过翻译 → 知识库中留下纯英文文档
  3. ❌ 翻译脚本不可用就放弃翻译 → 应该由 Agent 直接在对话中翻译
  4. ❌ PDF 是论文就不翻译 → 英文论文同样必须翻译

正确做法:每篇文章上传前,必须先执行语言检测,非中文则翻译后再上传。

在上传到乐享之前,必须检测原文语言。如果原文不是中文,则需要先翻译为中英对照格式后再归档。

语言检测规则

  • 读取 <原文标题>.md 的前 500 个字符,统计中文字符(Unicode 范围 \u4e00-\u9fff)占比
  • 中文字符占比 ≥ 30% → 判定为中文文章,跳过翻译,直接进入步骤 4
  • 中文字符占比 < 30% → 判定为非中文文章,执行翻译

翻译排版格式(中英对照)

  • 按段落逐段翻译,每段原文紧跟对应中文翻译
  • 段落之间不加分隔线 ---,仅通过空行分隔
  • 中文翻译段落开头不加国旗 emoji(🇨🇳),直接以中文开始
  • 标题也需要翻译,保留原文标题 + 中文翻译标题
  • 列表项、引用块等结构元素同样逐条翻译
  • 保留原文中的图片引用![](images/xxx.png)),图片引用放在对应段落的上方或下方,确保图文对应关系不丢失
# Original English Title
# 中文翻译标题

Original first paragraph text...

第一段的中文翻译...

![](images/img_01_xxx.png)

Original second paragraph text...

第二段的中文翻译...

翻译方式(按优先级)

  1. translate_article.py 脚本(如果 OPENAI_API_KEY 可用):
    python3 scripts/translate_article.py "<原文标题>.md" "<原文标题>_translated.md" --model gpt-4o-mini
    
  2. AI 助手直接翻译(如果无 API Key):由 Agent 在对话中逐段翻译全文,生成 <原文标题>_translated.md

翻译完成后

  • 本地保存两个文件:<原文标题>.md(原文)和 <原文标题>_translated.md(中英对照版)
  • 归档到乐享知识库的必须是翻译后的中英对照版本_translated.md),确保知识库中的内容对中文读者友好
  • 乐享文档标题使用:<原文标题中文翻译>(<原文标题>),如:AI 原型精通阶梯(The AI Prototyping Mastery Ladder)

步骤 3.7:评价信息处理(可选)

如果在转存前用户提供了对文章的评价(例如:"这篇文章好在:1)... 2)..."),需要在上传时自动添加评价信息:

  1. 检测评价信息:在对话中识别用户是否提供了评价内容(关键词:好在、评价、优点、建议等)
  2. 保存评价内容:将评价信息保存到临时文件(如 /tmp/evaluation.txt
  3. 传入脚本参数:调用 md_to_page.py 时,添加 --evaluation-file /tmp/evaluation.txt 参数
  4. 脚本自动处理md_to_page.py 会自动在文档顶部插入评价信息(格式为 blockquote,乐享可能自动转换为 callout 组件)

对于非在线文档格式(如视频)

  • 由于 lexiang MCP 工具中没有创建评论的 API(只有查询评论的 comment_list_commentscomment_describe_comment),暂时无法自动添加评论到视频文件
  • 建议:转存完成后,手动在乐享中添加评论

示例:用户提供评价后的处理流程

# 1. 将用户评价保存到临时文件
cat > /tmp/evaluation.txt << 'EOF'
这篇文章好在:
1)把智能体Agent做了分类,每个分类定义了对应是适用场景;
2)列举了详实的案例说明;
3)通过构建的复杂度、技术架构、实现时长、运行成本、衡量成功等几个维度来系统化地综合判断Agent落地的优先级
4)未来关于Agent选型上,能够提供系统性的参考建议
EOF

# 2. 调用 md_to_page.py,传入评价文件
python3 scripts/md_to_page.py "<原文标题>_translated.md" \
  --parent-id <日期目录ID> --name "<文档标题>" \
  --evaluation-file /tmp/evaluation.txt \
  --token "$LEXIANG_TOKEN" --company-from "$COMPANY_FROM"

评价信息格式说明

  • 脚本会将评价信息格式化为 blockquote(以 > 开头的 Markdown 格式)
  • 在乐享在线文档中,blockquote 可能被自动渲染为 callout 组件(带有左侧竖线或背景色)
  • 如果需要真正的 callout 组件(特殊 block 类型),需要通过 block_create_block_descendant API 创建,但需要先了解 callout 的 block 结构

步骤 3.8:页面内嵌视频检测与链接附加(⚠️ 不可跳过)

在生成 PDF 或上传之前,必须检测页面中是否包含嵌入视频。嵌入视频(如 Wistia、YouTube、Vimeo、Loom 等 iframe 嵌入)在转为 PDF 时会完全丢失,因此需要将视频链接以文本形式附加到文档末尾,确保知识库读者能找到并观看原始视频。

检测范围(按优先级扫描):

  1. <iframe> 嵌入 — 匹配 src 中包含 youtubeyoutu.bevimeoloomwistiavidyardplayer 的 iframe
  2. <video> 标签 — 提取 src 或内部 <source>src
  3. <a> 链接 — 匹配 href 指向 youtube.com/watchyoutu.be/vimeo.com/loom.com/share 等视频平台
  4. 平台特定容器 — 如 readme.io 的 rdmd-embed 组件、[class*="video"] 容器等

视频链接还原规则

  • Wistia embed(fast.wistia.net/embed/iframe/<id>)→ 附加可观看链接 https://fast.wistia.net/embed/iframe/<id>
  • YouTube embed(youtube.com/embed/<id>)→ 还原为 https://www.youtube.com/watch?v=<id>
  • Vimeo embed(player.vimeo.com/video/<id>)→ 还原为 https://vimeo.com/<id>
  • Loom embed(loom.com/embed/<id>)→ 还原为 https://www.loom.com/share/<id>
  • 其他视频 URL → 原样保留

附加格式:在 Markdown 文档末尾(PDF 生成前)追加一个独立章节:


---

## 📹 页面内嵌视频

本页面包含以下嵌入视频,PDF 中无法播放,请通过链接观看:

1. [视频] https://fast.wistia.net/embed/iframe/xxxxx
2. [视频] https://www.youtube.com/watch?v=yyyyy

如果使用 Playwright 直接生成 PDF(非 fetch_article.py 抓取),应在 page.pdf() 之前通过 page.evaluate() 在页面底部注入视频链接信息块。

步骤 4:上传到乐享(统一使用在线文档格式)

🚨 核心原则:所有文章默认使用在线文档(page)格式上传,不再默认转 PDF。 在线文档的优势:支持编辑、划词评论、全文检索、移动端阅读体验好。 PDF 仅在以下情况使用:(1) md_to_page.pyentry_import_content 都失败时的最终降级;(2) 用户明确要求 PDF 格式。

检查 <原文标题>.md 文件同目录下是否存在 images/ 目录且包含图片文件:

🚨 图片判断 Checklist(必须逐条检查)

  1. images/ 目录是否存在且有图片?
  2. Markdown 内容中是否有 ![](images/xxx) 本地引用?(仅检查目录不够!如果 images/ 有图片但 Markdown 中无引用,说明抓取阶段出了问题——正文提取失败导致图片引用丢失,需要重新抓取)
  3. 如果 Markdown 中只有外链图片(![](https://...))而无本地引用,说明图片没有被下载到本地。entry_import_content 导入外链图片后,乐享中如果外链 CDN 有防盗链/过期,图片将不可见。此时应先下载图片到本地,再用 md_to_page.py 导入。

判断结论

  • ✅ images/ 有图片 + Markdown 有 ![](images/xxx)图文文章,走图文路径
  • ⚠️ images/ 有图片 + Markdown 无本地引用 → 抓取异常,需重新抓取或手动修复 Markdown
  • ⚠️ images/ 无图片 + Markdown 有外链图片 → 外链图片,建议下载后转本地引用
  • ✅ images/ 无图片 + Markdown 无图片引用 → 纯文本文章,走纯文本路径
  • 有图片(图文文章) → 使用 scripts/md_to_page.py 将 Markdown 图文导入为在线文档(图片内嵌到正文对应位置):

    python3 scripts/md_to_page.py "<原文标题>.md" \
      --parent-id <日期目录ID> --name "<原文标题>" \
      --token "$LEXIANG_TOKEN" --company-from "$COMPANY_FROM"
    

    脚本会自动:按图片位置拆分 markdown → 分段导入文字(直传原始 markdown,不做 base64 编码)→ 逐张上传图片到 COS → 在正确位置插入 image block。

    降级方案 A(脚本无 token 时 — 通过 MCP connector 交替导入图文)

    md_to_page.py 因缺少 LEXIANG_TOKEN 无法运行时(如 mcp.json 中无 lexiang 配置,只有 connector 模式),改用以下流程。

    🚨 核心原则:交替导入,严禁先全文后补图!

    必须按「文字段→图片→文字段→图片→...」的顺序交替导入,这样每张图片自然落在正确位置。 绝对不能先把全部文字一次性导入,事后再补图——这会导致所有图片堆积在文档末尾,破坏图文混排。

    执行流程(照此逐步执行,不可跳步)

    步骤 A1:准备——按图片位置拆分 Markdown
      读取 article.md 内容
      按 ![xxx](images/yyy) 引用位置拆分为交替的 segments 数组:
        [("text", "第一段文字..."), ("image", "img_01.png"), ("text", "第二段文字..."), ("image", "img_02.png"), ...]
      如果文字段超过 15000 字符,按段落 \n\n 二次拆分为多个 ≤15000 的子块
    
    步骤 A2:创建空白 page
      entry_create_entry(entry_type="page", parent_entry_id=<日期目录ID>, name="<原文标题>")
      记录 entry_id
    
    步骤 A3:交替导入(按 segments 数组顺序逐个处理)
      is_first = true
      for each segment in segments:
        if segment.type == "text":
          entry_import_content_to_entry(entry_id, content=segment.text, force_write=is_first)
          is_first = false
        
        elif segment.type == "image":
          img_path = images/<segment.filename>
          if 文件不存在 or 文件 < 1KB → 跳过
          
          // 三步上传图片
          1. block_apply_block_attachment_upload(entry_id, name, size, mime_type)→ session_id + upload_url
          2. curl -X PUT "<upload_url>" --data-binary @<img_path>  → 确认 HTTP 200
          3. block_create_block_descendant(entry_id, index="-1", descendant=[{block_type: "image", image: {session_id}}])
          
          // index="-1" 在这里是正确的!因为是交替执行,当前末尾就是刚导入的文字段之后
    

    ⚠️ 为什么 index=-1 在交替导入中是正确的? 因为文字和图片严格交替执行:先导入文字(追加到末尾),再在末尾插入图片,图片自然位于刚导入的文字之后。 只有「先全文导入完毕→事后补图」的场景下,index=-1 才会导致图片堆积在末尾。

    ⚠️ 小图片(<50KB 的 icon/分隔线/SVG 装饰图)可跳过,只上传有信息量的关键图片(图表/截图/概念图)。

    ⚠️ 得到 APP 文章特殊情况:得到文章通常有 80-100+ 张图片,其中大部分是公式渲染图(3-10KB),真正有信息量的数据图表约 5-10 张(>50KB)。对得到文章,必须上传 >50KB 的关键图片(概念图、流程图、案例配图等),不需要逐张上传所有小图。

    ⚠️ 得到文章完整转存流程(2026-05-09 实战验证)

    1. fetch_article.py --cdp 抓取全文 → 本地 article.md + images/
    2. 提取纯文字版(去掉 ![](images/...) 引用 + 去掉得到APP UI噪声如"展开"、"分享"、点赞数、用户留言等)
    3. 创建 page → 分块导入纯文字(每块 ≤4000 chars)
    4. 筛选 >50KB 的关键图片(用 find images/ -size +50k
    5. 排除 SVG 格式的 UI 图标(查看文件头是否为 <?xml
    6. 逐张上传关键图片并插入文档对应位置

    降级方案 B(最终降级):如果以上都失败 → 调用 scripts/md_to_pdf.py 转为 PDF,再通过三步上传流程上传:

    1. file_apply_upload(参数:parent_entry_id=<日期目录ID>, name="<原文标题>.pdf", size=<文件字节数>, mime_type="application/pdf", upload_type="PRE_SIGNED_URL"
    2. 使用 curl -X PUT 将 PDF 文件上传到返回的 upload_url
    3. file_commit_upload(参数:session_id=<上一步返回的session_id>

    🚨 绝对禁止:不要用 file_apply_upload 直接上传 .md 文件!.md 上传后在乐享中会丢失所有图片信息,用户看到的只是含 ![](images/xxx) 引用的纯文本,毫无可读性。

  • 无图片(纯文本文章) → 使用 entry_import_content 创建为在线文档(page 类型)

    • 参数:space_id=<config 中的 SPACE_ID>, parent_id=<日期目录ID>, name="<原文标题>", content=<Markdown文件内容>, content_type="markdown"
    • 在线文档支持在乐享中直接编辑
  • 通过 web_fetch 抓取的文章(无本地图片文件) → 直接使用 entry_import_content 创建在线文档,Markdown 内容中的外链图片在乐享中可能无法显示,但文字内容完整可编辑、可检索。

步骤 5:输出结果

  • config.json 中的 lexiang.access_domain.page_url_template 格式拼接文档链接,告知用户
  • 示例:https://lexiangla.com/pages/<entry_id>?company_from=<company_from>(域名和 company_from 从配置读取,不要硬编码)
  • ⚠️ 链接必须包含 company_from 参数,否则用户打开页面会跳转到登录页或显示无权限

注意事项

  • 配置初始化是前置条件:首次使用时会自动通过对话引导完成知识库配置,无需手动编辑文件
  • MCP 连接是前置条件:必须先确认 lexiang MCP 已连接才能执行操作。不同 Agent 的连接方式不同,参见上方「乐享 MCP 工具的调用方式」
  • 访问链接域名:展示给用户的链接一律按 config.jsonpage_url_template 格式生成(含 company_from 参数),不要使用 mcp.lexiang-app.com不要省略 company_from
  • 上传前自动去重:按「文档名称 + 文档类型」在目标日期目录下查重,避免重复上传
  • 默认使用在线文档(page)格式:所有文章(含图文)统一以在线文档格式上传,支持编辑、检索、评论。PDF 仅作为最终降级方案
  • 纯文本文章直接用 entry_import_content,图文文章优先用 md_to_page.py(图片内嵌),降级用 entry_import_content(图片不内嵌但文字完整)
  • PDF 转换依赖 pymupdf 库(pip3 install pymupdf),仅在前两种方式都失败时使用
  • 如果同一天多次处理不同文章,它们会归入同一个日期目录下
  • 使用 _mcp_fields 参数可以减少返回数据量,如 _mcp_fields=["id", "root_entry_id", "name"]

脚本文件

文件用途
scripts/fetch_article.py通用文章抓取脚本(Chrome cookies + Playwright)。支持付费墙/登录墙、微信公众号、得到、**Webflow SPA(claude.com/anthropic.com)**等多种站点,自动识别内容容器,输出 Markdown + 图片 + 元信息 JSON。Webflow 支持为内置自动检测,无需额外参数
scripts/md_to_pdf.pyMarkdown 转 PDF 脚本(使用 pymupdf,嵌入本地图片,正确渲染中文,支持标题回退和拆行标题修复)
scripts/md_to_page.py【推荐】 Markdown 图文导入乐享在线文档脚本。按图片位置将 markdown 拆分为 text/image 交替段落,分段导入到乐享 page(文字用 entry_import_content_to_entry 直传原始 markdown,图片用 block_apply_block_attachment_upload + curl PUT + block_create_block_descendant 三步上传)。⚠️ 脚本通过 HTTP JSON-RPC 直连乐享 MCP API,content 字段不需要 base64 编码(直传原始 markdown 字符串)。支持任意长度文章,图片内嵌到正文对应位置,生成可编辑、可划词评论的在线文档。用法:python3 scripts/md_to_page.py <md_file> --entry-id <ID> --token <TOKEN> --company-from <CF>--parent-id <PID> --name "标题" 创建新页面
scripts/yt_download_transcribe.pyYouTube 视频下载 + Whisper 转录 + AI 翻译脚本(yt-dlp 下载、ffmpeg 提取音频、Whisper 转录、OpenAI 翻译为中英对照 Markdown)。也可用于播客音频转录(跳过视频下载步骤)
scripts/translate_gemini.py使用 Gemini API 将英文 Markdown 翻译为中英对照格式。按 ~4K 字符分段翻译,每段间隔 2 秒避免限频。模型:gemini-2.5-flash。需要 GEMINI_API_KEY 环境变量。用法:python3 scripts/translate_gemini.py(翻译后生成 _translated.md 文件)

注意:乐享知识库操作不再通过独立脚本(save_to_lexiang.sh/upload_yt_to_lexiang.sh)完成,而是由大模型通过 lexiang MCP 工具直接执行。不同 Agent 产品(OpenClaw、CodeBuddy、Claude Desktop 等)各自管理 MCP 连接,但调用的工具名称和参数完全一致。

经验总结

在线文档图文导入(md_to_page.py)

核心方案:Python 脚本通过 HTTP JSON-RPC 直连乐享 MCP API,按图片位置将 markdown 拆分为 text/image 交替段落,逐段导入。

为什么不走 IDE 的 MCP 工具调用

  • IDE MCP 工具调用有参数长度限制,45K 字符的 markdown base64 编码后 62K,无法一次性传递
  • Python 脚本直连 HTTP JSON-RPC 没有此限制,按 ~15K 字符分段传输即可

关键踩坑(⚠️ 重要)

  1. 不要做 base64 编码:通过 HTTP JSON-RPC 直连时,entry_import_content_to_entry 的 content 字段直传原始 markdown 字符串。如果做了 base64 编码,乐享会把 base64 字符串当成纯文本存储,页面显示为乱码。只有通过 IDE MCP 协议调用时才需要 base64
  2. 图片需逐张插入block_create_block_descendant 一次传多张图片的 block 会失败,必须一张一张来
  3. 文字分段追加:第一段用 force_write=true 覆盖,后续段用 force_write=false 追加到末尾
  4. 图片位置要正确:先按原文中 ![](images/xxx.jpg) 的位置拆分 markdown,确保文字和图片按原文顺序交替插入
  5. 乐享文档名称:要与文章原标题一致,创建时通过 --name 指定,或创建后用 entry_rename_entry 修改

翻译注意事项

  • 所有英文文章默认必须翻译为中英对照格式再归档,不可跳过
  • 翻译脚本 translate_gemini.py 使用 Gemini API(模型:gemini-2.5-flash),按 ~4K 字符分段翻译
  • Gemini API gemini-2.0-flash 已下线,务必使用 gemini-2.5-flash 或更新的模型
  • 翻译完成后用 md_to_page.py --entry-id <ID> 覆盖更新在线文档
  • 如果没有 Gemini API Key 也没有 OpenAI API Key,由 AI 助手在对话中翻译后写入文件

自测清单(发布前必须完成):

  • 通过 entry_describe_ai_parse_content 验证文字内容可读(非 base64 乱码)
  • 通过 block_list_block_children 验证图片 block 存在且有 file_id
  • 验证文档名称与文章标题一致
  • 验证中英对照格式(英文在前,中文翻译紧跟其后)

YouTube 视频下载与转录

核心方案:yt-dlp 下载 → ffmpeg 提取音频 → Whisper 本地转录 → OpenAI API 翻译

为什么不用 NotebookLM / summarize.sh

  1. NotebookLM 需要 Google 账号且有额度限制,部分视频可能因版权限制无法提取
  2. summarize.sh 依赖外部 API(Apify/YouTube 字幕 API),部分视频无字幕时无法工作
  3. Whisper 本地转录不依赖字幕,直接从音频波形识别语音,覆盖率 100%

yt-dlp 版本与安装(关键!)

  • 必须使用 brew install yt-dlp 安装,不要用 pip3 install yt-dlp
  • 原因:pip 版本受限于系统 Python 版本(macOS 自带 Python 3.9),无法安装 yt-dlp 的 nightly 版本(需要 Python 3.10+)。而 YouTube 频繁更新反爬策略,旧版 yt-dlp 会遇到 HTTP 403 Forbidden 错误
  • brew 安装的 yt-dlp 自带独立 Python 环境,始终能获取最新版本
  • 脚本中调用方式:直接用 yt-dlp 命令,不要python3 -m yt_dlp

YouTube DASH 格式 403 错误(重要!)

  • YouTube 正在强制使用 SABR(Streaming ABR)流媒体协议,传统 DASH 分片下载(bestvideo+bestaudio)会触发 HTTP 403 Forbidden
  • 解决方案:优先使用 HLS(m3u8)格式下载,不会被 SABR 拦截
  • 脚本中的格式选择顺序:95-1/94-1/93-1/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best
    • 95-1: 720p HLS(推荐,画质和文件大小的最佳平衡)
    • 94-1: 480p HLS
    • 93-1: 360p HLS
    • 后面是传统 DASH 格式作为回退
  • HLS 格式下载的视频文件会比 DASH 大一些(720p HLS 约 500-600MB vs DASH 约 200-300MB)
  • 注意--extractor-args "youtube:player_client=android" 不支持 cookies,不是可靠的 403 解决方案

Whisper 转录最佳实践

  • 音频预处理:16kHz 采样率、单声道 WAV(ffmpeg -ar 16000 -ac 1),减少文件大小且是 Whisper 推荐格式
  • 段落合并策略:相邻 segment 间隔 <2s 且总时长 <60s 则合并,句号/问号结尾时倾向断开
  • 模型选择:默认用 base(速度和精度的最佳平衡),重要内容用 smallmedium

翻译策略

  • 使用 OpenAI gpt-4o-mini,分批翻译(每批 10 段),避免 token 超限
  • 翻译 prompt 要求"自然流畅的中文表达,专业术语保留英文并附中文注释"
  • 中英对照格式:每段先展示英文原文,紧跟中文翻译,段间用空行分隔(不加分隔线和国旗 emoji)
  • 如果没有 OPENAI_API_KEY:脚本会跳过翻译步骤,输出纯英文文字稿。此时可以由 AI 助手在对话中直接翻译全文,然后用 md_to_page.py --entry-id 更新乐享文档

上传乐享的关键决策

  • 文字稿使用 在线文档(page)格式而非文件上传,原因:支持在乐享中按块维度编辑更新,可以逐段修正翻译或补充注释
  • 视频使用 文件(file)格式上传,因为视频不需要在线编辑
  • 上传成功后自动删除本地视频文件,避免占用磁盘空间

视频上传到乐享的正确方式(重要!)

  • 通过 lexiang MCP 工具完成,使用三步上传流程:
    1. file_apply_upload:申请上传凭证(传入 parent_entry_id=日期目录 ID、upload_type=PRE_SIGNED_URL、mime_type=video/mp4、size=文件字节数)
    2. curl -X PUT 上传文件到返回的 upload_url(预签名 URL,直传 COS)
    3. file_commit_upload:确认上传完成(传入 session_id
  • 518MB 视频的 PUT 上传约需 30-60 秒

播客音频转录

核心方案:yt-dlp(generic extractor)下载音频 → ffmpeg 转 WAV → Whisper 转录 → opencc 繁简转换

yt-dlp 对小宇宙的支持

  • yt-dlp 没有小宇宙专用 extractor,但 generic extractor 完全够用
  • 小宇宙页面中嵌入了 <audio> 标签,音频直链在 media.xyzcdn.net
  • 下载不需要 cookies,直接用 yt-dlp --no-playlist -o "%(title)s.%(ext)s" <URL> 即可
  • 下载速度约 7MB/s,63 分钟播客(59MB)仅需 8 秒

Whisper 中文转录的繁体问题(重要!)

  • Whisper base 模型对中文普通话倾向输出繁体字(如「歡迎」→ 应为「欢迎」)
  • 这是 Whisper 的已知行为,因为训练数据中繁体中文比重较大
  • 解决方案:转录后用 opencc-python-reimplementedt2s(Traditional to Simplified)模式批量转换
  • 安装:pip3 install opencc-python-reimplemented
  • 用法:opencc.OpenCC("t2s").convert(text)

中文播客 vs 英文 YouTube 的流程差异

  • 中文播客不需要翻译,但需要繁简转换
  • 播客音频是直接的 m4a/mp3 文件,不需要从视频中提取音频(但仍需 ffmpeg 转为 WAV 格式给 Whisper)
  • Whisper 转录时指定 language='zh' 可以提高中文识别准确率
  • 上传乐享时 MIME 类型用 audio/mp4(m4a)或 audio/mpeg(mp3),不是 video/mp4

转录性能参考

  • 63 分钟中文播客 → Whisper base 模型在 CPU 上转录耗时约 115 秒
  • 产出 2496 个 segments,合并后 65 个段落

微信公众号图文抓取

核心问题web_fetch 工具无法获取微信公众号文章的图片(懒加载 + 防盗链),必须使用 fetch_article.py

技术原理

  1. 懒加载机制:微信图片的真实 URL 存放在 data-src 而非 src,依赖 IntersectionObserver 在元素进入视口时才加载。Playwright 无头浏览器通过 window.scrollBy(0, 300) 配合 asyncio.sleep(0.2) 模拟慢速滚动,逐步触发所有图片的懒加载观察器
  2. 兜底策略:滚动完成后,通过 page.evaluate() 遍历所有 img[data-src],将未被触发的 data-src 强制复制到 src
  3. 高清图优先:提取图片 URL 时优先使用 data-src(高清原图),而非 src(可能是低分辨率占位图)
  4. 格式识别:微信图片 URL 无常规扩展名(如 mmbiz.qpic.cn/...?wx_fmt=png),需解析 wx_fmt 查询参数推断文件格式
  5. 防盗链绕过:通过 Playwright 页面上下文的 page.request.get() 下载图片,自动携带正确的 Referer 头
  6. 专用选择器:微信文章有固定 DOM 结构(#js_content#activity-name#js_name#publish_time),使用专用选择器比通用选择器更精准可靠

关键决策

  • 微信文章是公开可读的,跳过登录检测和 Cookie 注入流程
  • 滚动参数(300px 步长、200ms 间隔)经实测可平衡速度与懒加载触发成功率
  • Markdown 转换时 imageMap 同时匹配 srcdata-src,确保无论 HTML 中引用哪个属性都能正确替换

验证标准:抓取完成后检查 article_meta.json 中的 image_count 字段,与原文图片数量比对,确认无遗漏。

新平台适配思路

适配新平台时,需依次识别和处理以下 4 个维度:

  1. 懒加载机制 — 图片是否用 data-srcdata-lazy 等延迟加载?需要怎样的滚动策略触发?
  2. 专用 DOM 结构 — 正文、标题、作者、日期的选择器是什么?
  3. 图片 URL 格式 — 扩展名是否在路径中?是否需要从查询参数推断?
  4. 防盗链策略 — 是否需要正确的 Referer?是否有其他鉴权机制?

微信公众号文章处理(mp.weixin.qq.com)

首选方案:乐享 MCP file_create_hyperlink(2026-05-09 验证 ✅)

乐享后端原生支持微信公众号文章的抓取与解析,一步到位,无需本地抓取和手动上传图片。

mcp__lexiang__file_create_hyperlink(
  url = "https://mp.weixin.qq.com/s/...",
  parent_entry_id = "<目标目录 entry_id>",
  name = "<文章标题>"  // 可选,不传会自动从微信提取
)

返回值

  • finished: true — 后端抓取完成
  • entry.id — 新创建的知识条目 ID
  • entry_type: "flink" — 外部链接类型
  • extension: "wechat" — 自动识别微信来源

后端自动完成的事情

  1. 抓取微信文章全文(正文 + 图片)
  2. 图片保存到乐享 COS(/assets/xxx 格式)
  3. OCR 识别图片中的文字(用于全文检索和 AI 解析)
  4. 自动提取标题、作者、发布时间等元信息

优势

  • 一步完成,省去 fetch_article.py + 分块导入 + 逐张上传图片的复杂流程
  • Token 消耗从 ~50K 降到 <1K
  • 图片质量由乐享后端保证,无需本地下载和上传
  • 支持乐享的全文检索和 AI 解析(RAG)

如需附加用户评价/评论

  • 创建 hyperlink 后,可用 entry_import_content_to_entry(force_write=false)追加评价内容
  • 或用 block_create_block_descendant 在文档末尾插入评价 block

降级方案(当 file_create_hyperlink 失败时)

  • 如果返回 finished: false 或错误码,改用 fetch_article.py 本地抓取 + 降级方案 A 导入
  • 某些被限制的微信文章(如已删除、需付费等)可能无法通过此接口抓取

注意事项

  • 产出的 entry_type 是 flink(外部链接),而非 page(在线文档)
  • flink 类型在乐享中以原始文章格式展示,支持全文检索和 AI 解析
  • 如果用户明确要求以「在线文档/page」格式存储(需要后续编辑),才使用 fetch_article.py 降级方案

得到 APP 文章抓取(dedao.cn)

核心问题:得到 APP(www.dedao.cn)的文章内容是付费内容 + SPA 动态渲染web_fetchfetch_article.py 的通用提取逻辑都无法直接获取正文。

技术原因

  1. SPA 架构:得到网页版是 React SPA,文章正文通过 JS 异步渲染,web_fetch 只能拿到空白壳页面
  2. 付费墙:文章属于付费专栏内容,必须有已登录且已订阅的账号才能查看全文
  3. DOM 结构特殊:正文容器使用 .iget-articles 类名,不在 fetch_article.py 的默认选择器列表(article.post-content 等)中。通用 article 选择器只匹配到极少内容(~167 字符),而真正的正文在 .iget-articles 中有 6000+ 字符
  4. 内容区混杂:正文容器中混入了标题重复、音频时长、"划重点"、用户评论等非正文内容,需要清理

抓取方案:使用 CDP 模式连接已登录得到的 Chrome 浏览器:

# 前提:用户已在 Chrome 中登录得到 APP 且有文章阅读权限
python scripts/fetch_article.py fetch "https://www.dedao.cn/course/article?id=<ID>" --output-dir <目录> --cdp

已知限制

  • fetch_article.py 的通用内容提取逻辑对得到 DOM 结构匹配不佳,抓取结果可能不完整
  • 正确做法是通过 Playwright CDP 连接后,手动指定 .iget-articles 选择器提取正文:
# 通过 CDP 连接后,用专用选择器提取得到文章正文
content_el = await page.query_selector('.iget-articles')
if content_el:
    text = await content_el.inner_text()  # 完整正文

内容清理要点

  • 去掉正文开头的标题重复、日期、音频时长等元信息(通常在 凡哥杂谈,你好 或类似开场白之前)
  • 去掉正文末尾的"划重点"、"添加到笔记"、"首次发布"、"用户留言"等非正文内容
  • 如果是多篇系列文章(如上/下篇),合并时用 ## 上篇 / ## 下篇 分隔
  • 作者信息需要手动确认(通用提取器可能抓错)

得到文章转存乐享完整流程(2026-05-09 实战验证 ✅)

以下流程已在实际操作中验证通过,确保图文完整转存。

  1. 抓取python scripts/fetch_article.py fetch "<URL>" --output-dir articles/dedao_<ID短码> --cdp

    • 产出:article.md + images/ 目录(通常 80-100+ 张图,大部分是小于 10KB 的公式/icon 图)
  2. 提取纯文字版(去除图片引用和得到 UI 噪声):

    # 去除图片引用 ![](images/...)
    # 去除得到 APP 特有 UI 噪声:
    #   - "展开"/"收起" 按钮文字
    #   - 点赞数、评论数、分享按钮(如 "25"、"8"、"218"、"分享")
    #   - "关注" 按钮
    #   - 用户昵称 + 日期行(如 "Christy\n05-05")
    #   - "划重点" / "添加到笔记" / "写笔记划线删除划线复制" 等功能按钮
    #   - "首次发布: ..." 行
    #   - "我的留言" / "用户留言" / "全部 精选 筛选" 等区域标记
    # 保留正文 + 注释引用
    
  3. 创建在线文档 + 分块导入文字

    • entry_create_entry(entry_type="page", parent_entry_id=日期目录, name="<文章标题>(来源描述)")
    • 将纯文字版分块(≤4000 chars/块),第一块 force_write=true,后续 force_write=false 追加
    • 验证导入结果(spot check 关键段落)
  4. 筛选并上传关键图片

    # 找出 >50KB 的关键图片
    find images/ -size +50k -type f | sort
    
    # 排除 SVG/UI 图标(检查文件头)
    file images/img_04_*.png  # 如果是 SVG XML 则跳过
    
    # 查看图片内容(确认哪些有信息价值)
    # 典型有价值的:概念图、流程图、人物照片、数据图表
    # 典型无价值的:SVG 格式的得到 APP logo/icon
    
  5. 逐张上传图片到文档对应位置(每张图3步):

    ① block_apply_block_attachment_upload(entry_id, name, size, mime_type) → session_id + upload_url
    ② curl -X PUT "<upload_url>" -H "Content-Type: <mime>" -H "Content-Length: <size>" --data-binary @<file>
    ③ block_create_block_descendant(entry_id, parent_block_id=page_block_id, index=<位置>, descendant=[{block_type:"image", image:{session_id, caption, align:"center"}}])
    

    图片位置确定

    • 先用 block_list_block_children(entry_id, with_descendants=false)获取所有一级 block
    • 根据原文 article.md 中 ![](images/xxx) 的位置,找到对应文字段落的 block_id
    • 用 index 参数插入(注意:每插入一张图,后面的 block index 都会 +1)
    • 如果精确位置难以确定,也可以用 index=-1 追加到末尾(所有图集中放在文末也可接受)

适用场景:得到 APP 专栏文章(www.dedao.cn/course/article?id=xxx

TODO:考虑在 fetch_article.py 中增加得到专用检测和选择器(类似微信公众号的 _is_wechat_article 机制),自动使用 .iget-articles 提取正文。

SPA 网站 Playwright 直接出 PDF(正文隔离方案)

适用场景

  • fetch_article.py 抓取后正文为空或极少(< 200 字符),说明网站是 SPA 动态渲染,通用 Markdown 提取器无法工作
  • 批量抓取帮助中心/文档站(如 Guru help.getguru.com、readme.io 托管站、GitBook 等)
  • 已知案例:vcsmemo.com(Nuxt.js SPA)、help.getguru.com(readme.io)

核心方案:用 Playwright 无头浏览器直接访问页面 → 等待 SPA 渲染完成 → 隔离正文区域 → page.pdf() 生成 PDF。

关键步骤

1. 加载与等待

await page.goto(url, { waitUntil: "networkidle", timeout: 60000 });
await page.waitForTimeout(5000); // SPA 需要额外等待 JS 渲染

2. 滚动触发懒加载图片

await page.evaluate(async () => {
  const delay = (ms) => new Promise(r => setTimeout(r, ms));
  for (let i = 0; i < document.body.scrollHeight; i += 300) {
    window.scrollBy(0, 300);
    await delay(200);
  }
  window.scrollTo(0, 0);
});
await page.waitForTimeout(3000);

3. 正文隔离(⚠️ 最关键的一步)

问题:直接 page.pdf() 会把整个页面打进 PDF,包括导航栏、侧边栏、相关推荐、页脚等非正文内容。必须在生成 PDF 前隔离正文区域

正文隔离策略(三步法)

Step A:定位正文容器 — 找到包含文章核心段落的最小公共祖先节点

// 用文章中的关键句子定位正文 <p> 标签
const articleParagraphs = [];
document.querySelectorAll("p").forEach(p => {
  if (p.textContent.includes("文章中的某段独特文字")) {
    articleParagraphs.push(p);
  }
});

// 计算所有正文段落的最小公共祖先
let commonAncestor = articleParagraphs[0];
for (let i = 1; i < articleParagraphs.length; i++) {
  // ... 向上遍历 DOM 树找公共祖先
}

Step B:替换 body — 将整个 document.body 的内容替换为正文容器的克隆

const articleContent = commonAncestor.cloneNode(true);
document.body.innerHTML = "";
document.body.appendChild(articleContent);

Step C:清理残余 — 从正文容器内部移除混入的非正文元素

// 移除正文容器内可能混入的非内容元素
articleContent.querySelectorAll(
  '[class*="related"], [class*="sidebar"], [class*="comment"], ' +
  '[class*="share"], [class*="subscribe"], nav, header, footer'
).forEach(el => el.remove());

// 按文本内容移除(如"相关文章"、"登录"等中文导航项)
articleContent.querySelectorAll("*").forEach(el => {
  const t = el.textContent.trim();
  if (t === "相关文章" || t === "登录" || t.startsWith("Signal, not noise")) {
    const wrapper = el.closest("section, div, aside");
    wrapper ? wrapper.remove() : el.remove();
  }
});

4. 样式优化

articleContent.style.maxWidth = "750px";
articleContent.style.margin = "0 auto";
articleContent.style.padding = "30px 20px";
articleContent.style.fontSize = "15px";
articleContent.style.lineHeight = "1.8";

articleContent.querySelectorAll("img").forEach(img => {
  img.style.maxWidth = "100%";
  img.style.height = "auto";
});

5. 生成 PDF

await page.pdf({
  path: outputPath,
  format: "A4",
  printBackground: true,
  margin: { top: "15mm", bottom: "15mm", left: "15mm", right: "15mm" },
});

常见需要移除的非正文元素

元素类型典型选择器/文本说明
左侧导航nav, [class*="sidebar"], 包含"首页/快讯/登录"等文本网站主导航
右侧推荐[class*="related"], 包含"相关文章"文本相关文章推荐
顶部搜索[class*="search"], header搜索栏和网站 header
底部页脚footer, [class*="footer"]版权信息等
作者卡片[class*="author-card"], 包含头像+简介的独立区块如果在正文外部
订阅入口[class*="subscribe"], [class*="newsletter"]CTA 按钮

调试技巧

  • page.pdf() 之前先 page.screenshot({ path: "debug.png", fullPage: true }) 截图确认隔离效果
  • 如果首次隔离不干净,根据截图调整选择器,迭代优化

已验证的 SPA 网站

网站框架正文定位方式
vcsmemo.comNuxt.js通过文章段落文本找公共祖先,class left 内的 section
help.getguru.comreadme.io移除 .rm-Sidebar + nav + header + footer
dedao.cnReact SPACDP 模式 + .iget-articles 专用选择器

Python 兼容性

脚本使用 from __future__ import annotations 以兼容 Python 3.9(str | None 联合类型语法在 3.9 中不可用)。

常见问题

问题原因修复方法
YouTube 视频下载 HTTP 403 Forbiddenyt-dlp 版本过旧 + YouTube 强制 SABR 流媒体协议,传统 DASH 分片下载被拦截brew install yt-dlp 升级到最新版(不要用 pip);② 脚本已配置优先使用 HLS(m3u8) 格式(95-1/94-1/93-1),自动回退
pip3 install --upgrade yt-dlp 无法安装最新版macOS 自带 Python 3.9,yt-dlp nightly 版需要 Python 3.10+改用 brew install yt-dlp,brew 版自带独立 Python 环境
脚本中 python3 -m yt_dlp 调用失败pip 安装的旧版 yt-dlp 与 brew 安装的新版不一致脚本已修改为直接调用 yt-dlp 命令(brew 安装的版本)
视频上传乐享报"不支持的文件格式"旧版 COS API(/kb/files/upload-params)不识别视频格式通过 lexiang MCP 工具使用三步上传流程:file_apply_uploadcurl PUTfile_commit_upload
Whisper 转录速度极慢模型太大或音频太长换用 tinybase 模型;对于长视频(>1h),考虑用 --whisper-model tiny 先快速预览
翻译结果为空未设置 OPENAI_API_KEY 环境变量export OPENAI_API_KEY=sk-xxx;或使用 --skip-translate 跳过翻译,由 AI 助手在对话中直接翻译全文后用 md_to_page.py --entry-id 更新乐享文档
中英对照格式段落错位AI 翻译返回的段落数与原文不匹配脚本已有容错处理(缺少翻译的段落会跳过),可手动补充翻译
视频上传乐享超时视频文件过大(>500MB)使用 MCP 的 file_apply_upload 预签名 URL 方式上传,518MB 文件约 30-60 秒即可完成
Whisper 中文转录输出繁体字Whisper base 模型对中文普通话倾向输出繁体opencc-python-reimplementedt2s 模式进行繁简转换:opencc.OpenCC("t2s").convert(text)
小宇宙播客下载提示 generic extractoryt-dlp 没有小宇宙专用 extractor正常现象,generic extractor 能自动从页面提取音频直链(media.xyzcdn.net),下载完全正常
微信文章图片丢失web_fetch 无法触发懒加载和绕过防盗链首选:使用 file_create_hyperlink 直接导入(乐享后端自动处理图文)。降级:使用 fetch_article.py(脚本自动检测微信域名并启用专用处理策略)
乐享知识库操作失败MCP 连接异常或 Token 过期① 确认当前 Agent 的 lexiang MCP 已连接(CodeBuddy 检查 MCP 面板、OpenClaw 检查 skill 安装状态);② Token 过期时访问 https://lexiangla.com/mcp 获取新 Token 并更新 MCP 配置
文件上传到了知识库根目录而非日期目录跳过了步骤 2(创建日期目录)和步骤 3(去重检查),直接以 root_entry_id 作为 parent_entry_id 上传严格按照步骤 1→2→3→4 顺序执行,步骤 2 中先 entry_list_children 检查日期目录是否存在,不存在则创建
展示给用户的乐享链接无法访问使用了 MCP API 域名 mcp.lexiang-app.com 或缺少 company_from 参数所有展示给用户的链接必须按 config.jsonpage_url_template 格式生成:https://lexiangla.com/pages/<entry_id>?company_from=<company_from>company_from 不可省略,否则用户无法访问
PDF 中缺少标题fetch_article.pyprocessNode 将正文 <h1> 转为 # 标题,与手动拼接的元信息头标题重复;某些网站(如 Lenny's Newsletter)标题在 articleEl 外部导致 MD 文件第一行 # 为空已修复:(1) processNode 中自动去重正文中与已提取 title 相同的第一个 h1 (2) 标题提取增加 og:titlemeta[name="title"]document.title 多策略回退 (3) md_to_pdf.py 增加标题回退——当 MD 中无有效 h1 时从 article_meta.json 补充
PDF 中缺少子标题某些网站的 HTML 结构导致 ### # 从 Tab 到 Agents 被拆为两行:### #从 Tab 到 Agentsparse_markdown# 视为无效标题丢弃已修复:parse_markdown 增加拆行标题检测——当标题文字为 # 或空时,检查下一行是否为实际标题文字并合并
md_to_page.py 导入后文字显示为 base64 乱码脚本通过 HTTP JSON-RPC 直连乐享 MCP API 时,对 content 做了多余的 base64 编码。乐享 MCP 的 base64 要求仅针对 IDE 侧 MCP 协议已修复:去掉 import_content 函数中的 base64.b64encode(),直传原始 markdown。⚠️ 通过 HTTP JSON-RPC 直连时永远不要做 base64 编码
md_to_page.py 批量插入图片 block 失败block_create_block_descendant 一次传多张图片的 descendant 数组会超时或报错改为逐张插入,每次只传一个 image block 的 descendant + children
Gemini API 调用报 404 模型不存在gemini-2.0-flash 模型已下线使用 gemini-2.5-flash 替代。可通过 curl "https://generativelanguage.googleapis.com/v1beta/models?key=$GEMINI_API_KEY" 查看当前可用模型
英文文章未翻译就归档跳过了步骤 3.5 的语言检测和翻译所有英文文章必须翻译为中英对照后再归档,这是强制步骤不可跳过。使用 translate_gemini.py(Gemini API)或 translate_article.py(OpenAI API)翻译,翻译完用 md_to_page.py --entry-id 覆盖更新
translate_gemini.py 报错 FileNotFoundError脚本硬编码了源文件路径,不读取命令行参数已修复:改用 sys.argv[1] 读取输入文件,sys.argv[2] 读取输出文件,默认输出 _translated.md
md_to_page.py 执行报错 IndentationError添加 --evaluation/--evaluation-file 参数时缩进不一致已修复:参数定义须与上方 --base-url 对齐;Python 严禁混用 tab 和空格
fetch_article.py 下载的图片在 md_to_page.py 中提示 NOT FOUND下载保存的文件名与写入 Markdown 的引用不一致(如 img_06_1c1cfc4c.gif vs img_06_1c1cfc42.giffetch_article.pyprocess_images 函数中,保存到 images/ 的文件名与替换 Markdown src 时的文件名必须完全一致;建议统一使用 hash[:8] + 原始扩展名,并在替换后打印映射表方便排查
乐享 MCP 更新 token 后工具仍报 "not found"mcp.json 配置已更新,但 MCP 服务未重新加载必须重启 WorkBuddy(或禁用再重新启用 MCP 服务),新的 token 才能生效
md_to_page.py 新增评价信息功能需要在文档顶部插入用户评价(callout 组件)已添加 --evaluation(短文本)和 --evaluation-file(文件路径)两个参数;评价内容会以 blockquote 格式插入文档顶部,乐享会自动渲染为 callout 组件
播客文字稿章节标题重复出现几十次在 Whisper segment 级别(1-5秒粒度)插入章节标题,且用宽松时间容差匹配 abs(start - ts) < 5,导致多个 segment 都命中同一标题必须先合并 segments 为段落(gap<2s, duration<60s),再在段落级别插入标题。用 inserted_headers = set() 跟踪已插入标题,每个标题只插入一次
日期目录重复创建直接调用 entry_create_entry 而不先查询目录是否已存在必须先用 entry_list_children 查询根目录,匹配到同名 folder 则复用其 ID,不存在才创建。已在步骤 2 中加强约束
得到文章转存后无图片只导入了纯文字,未执行图片上传步骤得到文章必须在文字导入后,逐张上传 >50KB 的关键图片(概念图/流程图/配图),流程:block_apply_block_attachment_uploadcurl PUTblock_create_block_descendant(image block)。详见"得到 APP 文章抓取"章节
图片上传后显示不出来block_create_block_descendant 的 image block 未正确传入 session_idimage block 的 session_id 必须来自同一个 block_apply_block_attachment_upload 返回值,且 curl PUT 必须返回 HTTP 200 才表示文件上传成功
fetch_article.py 抓取 Webflow SPA 站点(如 claude.com)正文为空旧版通用选择器列表中无 Webflow 容器,且内嵌 <style> 标签污染提取已修复:fetch_article.py 内置 _is_webflow_blog() 检测,自动使用 .u-rich-text-blog / .w-richtext 选择器,并在提取前移除内嵌 <style> 标签。无需使用独立脚本
md_to_page.py 无法匹配某些图片引用旧版 img_pattern 只匹配 img_XX_HASH.ext 格式(fetch_article.py),不匹配其他命名格式已修复:img_pattern 改为通用 !\[[^\]]*\]\(images/([^)]+)\),兼容所有图片命名格式
新建目录排到末尾而非顶部md_to_page.py 和手动调用 entry_move_entry 时使用 after=<第一个条目ID>(排第二)或 after=""(排末尾)已修复:统一使用 before=<第一个条目ID> 实现真正置顶。after="" 的 API 文档描述不准确,实测是排末尾
images/ 有图片但乐享文档无图片entry_import_content 导入 Markdown 时,本地 ![](images/xxx) 引用不会自动上传图片entry_import_content 只处理文字,不会上传本地图片。有本地图片必须走 md_to_page.py(自动处理图文)或降级方案 A(交替导入文字和图片,严禁先全文后补图)。步骤 4 开头的「图片判断 Checklist」是必查项
图片全部堆积在文档末尾先一次性导入全部文字,事后用 index=-1 补图必须交替导入:按 ![](images/xxx) 位置拆分 Markdown 为 segments,先导入一段文字 → 上传图片(index=-1 追加到末尾,此时末尾就是正确位置)→ 导入下一段文字 → 上传下一张图片... 详见降级方案 A 的执行流程