larksnap-fetch

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

larksnap-fetch

larksnap-fetch

在任意项目的 Claude Code 里「扔一个飞书链接 → 落到本地目录」。本技能不直接请求飞书,而是把任务 交给已登录的 larksnap 浏览器扩展(它持有 cookie 与导出引擎)。
CC ──fetch.mjs(一次性)──HTTP /command──▶ daemon ──WebSocket──▶ Chrome 扩展
                                          ▲ 127.0.0.1:19925     └ 后台开标签页跑导出
                                          └ 流式回传进度/产物 ──▶ 解包到 <输出目录>/<文档名>/
本技能自包含:
daemon
与协议代码已随技能打包在
scripts/bridge/
里,
fetch.mjs
拉起的是 技能内部那份 daemon,不依赖 larksnap 仓库的目录结构,因此可从任何项目调用。唯一的外部依赖是 那个装在 Chrome 里、已登录飞书的扩展(扩展无法塞进技能,只能在浏览器里加载一次,见「首次安装」)。
每篇文档落到自己的子文件夹(以文档标题命名),不和其它文件平铺混在一起:
<输出目录>/
└── 无监督数据修复/          ← 文档标题命名的文件夹
    ├── 无监督数据修复.md
    └── images/              ← 图片用相对路径内联引用
        └── xxx.png
  • 扩展是 WS 客户端,主动连出到本地 daemon —— 无 native messaging、无系统级安装。
  • daemon 由本技能按需自动拉起、持久存活(空闲 30 分钟自退),只绑
    127.0.0.1
  • fetch.mjs 一次性:跑完即退,不常驻。
In Claude Code of any project, "drop a Feishu link → save to local directory". This skill does not directly request Feishu, but delegates the task to the logged-in larksnap browser extension (which holds cookies and the export engine).
CC ──fetch.mjs(一次性)──HTTP /command──▶ daemon ──WebSocket──▶ Chrome 扩展
                                          ▲ 127.0.0.1:19925     └ 后台开标签页跑导出
                                          └ 流式回传进度/产物 ──▶ 解包到 <输出目录>/<文档名>/
This skill is self-contained: The
daemon
and protocol code are packaged with the skill in
scripts/bridge/
. The daemon launched by
fetch.mjs
is the one included in the skill, it does not depend on the directory structure of the larksnap repository, so it can be called from any project. The only external dependency is the logged-in larksnap browser extension installed in Chrome (the extension cannot be packed into the skill and can only be loaded once in the browser, see "First-time Installation").
Each document is saved to its own subfolder (named after the document title), without mixing with other files:
<输出目录>/
└── 无监督数据修复/          ← Folder named after document title
    ├── 无监督数据修复.md
    └── images/              ← Images are referenced inline with relative paths
        └── xxx.png
  • The extension is a WS client that actively connects out to the local daemon — no native messaging, no system-level installation.
  • The daemon is automatically launched on demand by this skill and runs persistently (exits automatically after 30 minutes of idle time), bound only to
    127.0.0.1
    .
  • fetch.mjs is one-time: Exits immediately after completion, does not run in the background.

用法

Usage

bash
node ~/.claude/skills/larksnap-fetch/scripts/fetch.mjs <飞书链接> <输出目录> [--format md|pdf|html] [--profile <code>]
  • <输出目录>
    父目录;产物会落到
    <输出目录>/<文档名>/
    子文件夹里(每篇文档独立成夹,互不混淆)。
  • --format
    缺省
    md
    md
    /
    html
    会把图片下载并以相对路径内联到该子文件夹的
    images/
    ;
    pdf
    视租户能力而定。
  • 进度打到 stderr,结果路径打到 stdout(
    ✓ 已导出到
    后是该文档的子文件夹绝对路径)。
  • --profile <code>
    :当有多个浏览器 profile 同时连到 daemon 时,指定用哪一个(code 见扩展弹窗的 Profile,可点 Copy 复制)。只有一个时无需指定。
退出码:
0
成功 |
1
失败 |
2
用法错 |
3
需登录 |
4
需授权域名 |
5
桥接未就绪。
bash
node ~/.claude/skills/larksnap-fetch/scripts/fetch.mjs <Feishu link> <output directory> [--format md|pdf|html] [--profile <code>]
  • <output directory>
    is the parent directory; the output will be saved to the subfolder
    <output directory>/<document name>/
    (each document is stored independently to avoid confusion).
  • --format
    defaults to
    md
    . For
    md
    /
    html
    , images will be downloaded and referenced inline with relative paths in the
    images/
    subfolder of that directory;
    pdf
    availability depends on tenant capabilities.
  • Progress is output to stderr, and the result path is output to stdout (the absolute path of the document's subfolder follows
    ✓ Exported to
    ).
  • --profile <code>
    : When multiple browser profiles are connected to the daemon simultaneously, specify which one to use (the code can be found in the Profile section of the extension popup and copied via the Copy button). Not required when only one profile exists.
Exit codes:
0
Success |
1
Failure |
2
Usage error |
3
Login required |
4
Domain authorization required |
5
Bridge not ready.

编辑飞书文档(写入 Markdown 内容)

Edit Feishu Documents (Write Markdown Content)

edit.mjs
是 fetch 的姊妹动作,共用同一个 daemon 与扩展:扩展在后台标签页里像用户一样把内容 粘贴进飞书编辑器(CDP 可信输入:真实剪贴板 + 可信鼠标点击/按键),协同保存由飞书前端自己完成。 前提:用户在浏览器里对该文档有编辑权限。两个副作用要让用户知道:编辑期间会占用系统剪贴板 (写入内容会覆盖用户当前复制的东西);任务执行的几秒内浏览器顶部会出现"正在调试此浏览器"横幅,结束即消失。
bash
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> new-doc [<md文件>] --name "<标题>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs new-doc [<md文件>] --name "<标题>" [--host <已授权域名>]  # 免链接
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs hosts [--profile <code>]   # 已授权域名清单(只读)
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> append <md文件>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> insert-after "<标题文本>" <md文件>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> list-blocks
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> find-blocks "<关键词>" [--regex] [--type <类型前缀>] [--limit N]
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> replace-block <块ID> <md文件> --expect "<内容摘要>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> delete-block <块ID> --expect "<内容摘要>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> insert-after-block <块ID> <md文件>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <链接> replace-all <md文件> --expect-first "<首块内容摘要>"
  • 写入内容一律先写进本地 md 文件再把文件路径传给命令(不走命令行参数,避免转义和长度问题);上限 2MB。
  • new-doc
    新建文档
    :在我的空间根目录建一篇 docx。这里的
    <链接>
    只用来定位"在哪个租户/ 域名下建"——传该租户的网盘首页(
    /drive/me/
    )或任意一篇文档链接都行。
    <md文件>
    可选:给了就 建完自动把内容写进去(复用 append),不给就建一篇空文档。
    --name
    设标题(不给为未命名)。成功 输出
    {ok:true, url}
    ,
    url
    是新文档地址,可直接接着
    fetch
    或继续编辑。只建 docx(公私云通用)。
  • new-doc
    可以不喂链接
    :省略
    <链接>
    时自动用扩展的「已授权域名清单」定位在哪建—— 只有一个已授权域名直接用它;多个则必须
    --host <域名>
    指定(收完整基础域或唯一简写, 多义报
    host_ambiguous
    、不在清单报
    host_not_authorized
    ,错误里都附完整可选清单)。
    --host
    只配合免链接的 new-doc;其他操作(append/insert-after/…)仍必须提供文档链接。 私有化域名比公有云稳:用户在侧边栏授权私有域名的那一刻,扩展会记下真实租户入口;公有云 (feishu.cn 等)没有授权动作可捕获,只有正开着某个
    *.feishu.cn
    标签时才能兜底定位。报
    need_landing_url
    时按提示办:这一次按旧用法喂个该域名下的链接,或让用户先在浏览器打开 一个该域名页面(或重新授权一次)再重跑。个人版
    my.feishu.cn
    接口有差异,
    --host my.feishu.cn
    不保证一次成功,失败按提示走旧用法。
  • hosts
    查看已授权域名清单
    (只读):输出
    {ok:true, domains:[{host,kind,sampleUrl}]}
    ——
    kind
    是 builtin(公有云内置)/trusted(用户授权的私有域名),
    sampleUrl
    非空表示该域名 可免链接建档。多浏览器 profile 时加
    --profile <code>
    。若报
    extension_outdated
    ,是浏览器 里的扩展版本太旧(桥接协议 <v4),让用户重新构建/更新并在 chrome://extensions 重载扩展。
  • 只支持 docx / wiki 文档;电子表格、多维表格不支持编辑。
  • 块级操作先定位再动手:知道要找什么内容(尤其长文档)优先
    find-blocks
    ,只回命中的块; 需要看整体结构才用
    list-blocks
    全量输出
    {blocks:[{id,parentId,type,depth,childCount,summary}]}
    ,
    summary
    是块内容前 80 字。
    replace-block
    /
    delete-block
    必须带
    --expect "<摘要>"
    (原样复制 list-blocks / find-blocks 输出里目标块的
    summary
    )——扩展执行前比对该块当前内容,对不上报
    block_changed
    ,防止文档被并发修改后改错/删错块。删除容器块(列表/引用等)会连同其子块一起删, 动手前用
    parentId
    /
    depth
    /
    childCount
    确认不会误删一整节。
  • find-blocks
    按内容检索块
    (只读):在块的完整纯文本上匹配(不受 summary 80 字限制), 默认「去空白+忽略大小写」子串,
    --regex
    按 JS 正则(
    iu
    旗标)原文匹配;
    --type <前缀>
    按块 类型过滤(如
    --type heading
    只搜标题);
    --limit N
    控制最多返回条数(默认 20)。输出
    {total,shown,blocks:[{...,summary,snippet}]}
    :
    total > shown
    说明命中被截断,换更具体的 关键词;
    summary
    与 list-blocks 逐字一致可直接当
    --expect
    ,
    snippet
    是命中处 前后各 40 字上下文(命中词用【】标出)只用于确认块找对了,不要拿去填
    --expect
    。 零命中返回
    total: 0
    ,退出码 0,不算错误。要搜以
    --
    开头的内容,用
    --regex
    加转义。 若报「未知编辑操作」,是浏览器里的扩展版本太旧,让用户重载/更新扩展。
  • insert-after
    按标题文本精确匹配:找不到报
    anchor_not_found
    ,多于一个报
    anchor_ambiguous
    (此时改用
    insert-after-block
    以块 ID 定位)。
  • replace-all
    是最危险的操作(整篇正文替换,文档标题保留)
    ,流程必须是: ① 先
    fetch.mjs
    把原文档导出到本地做备份;②
    list-blocks
    取输出第一个块
    summary
    ; ③ 原样作为
    --expect-first
    传入。扩展执行前比对当前文档首块,对不上报
    block_changed
    (说明文档已被改过,重新走 ①②)。没有备份就不要执行 replace-all。
  • 图片写入:md 里的
    ![](图片)
    支持三种来源——本地路径(相对路径以 md 文件所在目录为 基准)、外链 http(s)(自动下载后内嵌)、data URI。格式只收 png/jpg/jpeg/gif/webp/bmp (svg/ico 飞书会静默丢弃,CLI 直接报
    image_unsupported
    )。图片内嵌后整体超 2MB 报
    image_too_large
    (压缩图片或拆分写入);外链下载失败报
    image_fetch_failed
    ;本地文件 缺失报
    image_not_found
    含图片的写入自动切换为 HTML 粘贴口味(飞书只认 HTML 粘贴 里的 data URI 图片),副作用:同一次写入里的 md 表格会贴成内嵌电子表格而非原生表格—— 图片和表格尽量分两次写入。纯图片写入(没有文字)的回读校验用 image 块数量净增判定。
  • 写入成功的判定是回读校验:编辑后重新导出全文,确认新内容已落地才回成功。报
    save_unconfirmed
    时内容可能已进文档,不要盲目重跑(可能写两次),让用户打开文档人工确认。
  • 编辑成功后想继续做块级操作,先重新
    list-blocks
    。飞书的块按 ID 寻址,没被动过的块 ID 编辑后依然有效,但有两类引用一定作废:①
    replace-block
    会换块 ID(飞书按"删旧块+ 插新块"处理),旧 ID 直接失效;② 刚写入的内容你手里没有它的块 ID。拿旧清单硬做会撞上
    block_not_found
    /
    block_changed
    ,重跑一次
    list-blocks
    最省事。
  • 编辑专属退出码:
    6
    = 已登录但对该文档只读(
    need_edit_permission
    ),让用户在浏览器里确认/申请 编辑权限后重跑;
    7
    = 扩展缺少调试权限(
    need_edit_grant
    ,一般是装了不带编辑功能的旧版扩展), 让用户更新/重载扩展。其余退出码与 fetch.mjs 相同。
  • 编辑专属错误 subtype(退出码 1):
    anchor_not_found
    /
    anchor_ambiguous
    /
    block_not_found
    /
    block_changed
    (重新 list-blocks 后重试) /
    save_unconfirmed
    (人工确认) /
    inject_failed
  • extension_outdated
    (退出码 5):扩展版本太旧不支持编辑,让用户更新/重新构建扩展。
edit.mjs
is the sibling action of fetch, sharing the same daemon and extension: the extension pastes content into the Feishu editor like a user in a background tab (CDP trusted input: real clipboard + trusted mouse clicks/keystrokes), and collaborative saving is handled by the Feishu frontend itself. Prerequisite: The user has edit permissions for the document in the browser. Two side effects should be noted to the user: the system clipboard will be occupied during editing (the written content will overwrite the user's current copied content); a "Debugging this browser" banner will appear at the top of the browser for a few seconds during task execution and disappear after completion.
bash
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> new-doc [<md file>] --name "<title>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs new-doc [<md file>] --name "<title>" [--host <authorized domain>]  # No link required
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs hosts [--profile <code>]   # Read-only list of authorized domains
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> append <md file>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> insert-after "<title text>" <md file>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> list-blocks
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> find-blocks "<keyword>" [--regex] [--type <type prefix>] [--limit N]
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> replace-block <block ID> <md file> --expect "<content summary>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> delete-block <block ID> --expect "<content summary>"
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> insert-after-block <block ID> <md file>
node ~/.claude/skills/larksnap-fetch/scripts/edit.mjs <link> replace-all <md file> --expect-first "<first block content summary>"
  • Content to be written must first be saved to a local md file, then pass the file path to the command (do not use command line parameters to avoid escaping and length issues); maximum size is 2MB.
  • new-doc
    Create new document
    : Creates a docx in the root directory of My Space. The
    <link>
    here is only used to locate "which tenant/domain to create under" — you can pass the tenant's drive homepage (
    /drive/me/
    ) or any document link.
    <md file>
    is optional: if provided, the content will be written automatically after creation (reusing append); if not provided, an empty document will be created.
    --name
    sets the title (defaults to "Untitled" if not provided). On success, outputs
    {ok:true, url}
    , where
    url
    is the address of the new document, which can be used for subsequent
    fetch
    or editing operations. Only docx is supported (works for both public and private clouds).
  • new-doc
    can be used without a link
    : When
    <link>
    is omitted, it automatically uses the extension's "authorized domain list" to locate where to create the document — if there is only one authorized domain, it will be used directly; if there are multiple, you must specify it with
    --host <domain>
    (accepts the full base domain or a unique abbreviation; if ambiguous, it reports
    host_ambiguous
    ; if not in the list, it reports
    host_not_authorized
    , and the error message includes the full list of optional domains).
    --host
    only works with link-free new-doc; other operations (append/insert-after/…) still require a document link. Private domains are more stable than public clouds: When the user authorizes a private domain in the sidebar, the extension records the real tenant entry; public clouds (feishu.cn, etc.) have no authorization action to capture, and can only be located as a fallback when a
    *.feishu.cn
    tab is open. If
    need_landing_url
    is reported, follow the prompt: use the old method to provide a link under that domain this time, or ask the user to open a page under that domain in the browser (or re-authorize) before re-running. The personal version
    my.feishu.cn
    has different interfaces, and
    --host my.feishu.cn
    is not guaranteed to succeed on the first try; if it fails, follow the prompt to use the old method.
  • hosts
    View authorized domain list
    (read-only): Outputs
    {ok:true, domains:[{host,kind,sampleUrl}]}
    kind
    is builtin (public cloud built-in)/trusted (user-authorized private domain), and
    sampleUrl
    is non-empty indicating that the domain supports link-free document creation. Add
    --profile <code>
    when multiple browser profiles exist. If
    extension_outdated
    is reported, the extension in the browser is too old (bridge protocol <v4); ask the user to rebuild/update and reload the extension in chrome://extensions.
  • Only supports docx / wiki documents; spreadsheets and multidimensional tables are not supported for editing.
  • Block-level operations require locating first: If you know what content to find (especially in long documents), use
    find-blocks
    first, which only returns matching blocks; use
    list-blocks
    to output the full structure
    {blocks:[{id,parentId,type,depth,childCount,summary}]}
    only when you need to see the overall structure, where
    summary
    is the first 80 characters of the block content.
    replace-block
    /
    delete-block
    must include
    --expect "<summary>"
    (copy the
    summary
    of the target block from the list-blocks / find-blocks output exactly) — the extension compares the current content of the block before execution, and reports
    block_changed
    if it does not match, preventing incorrect modification/deletion of blocks after concurrent changes to the document. Deleting a container block (list/quote, etc.) will delete its child blocks together; use
    parentId
    /
    depth
    /
    childCount
    to confirm that you won't accidentally delete an entire section before proceeding.
  • find-blocks
    Search blocks by content
    (read-only): Matches against the full plain text of the block (not limited by the 80-character summary), defaults to "whitespace-removed + case-insensitive" substring match;
    --regex
    matches using JS regular expressions (
    iu
    flag);
    --type <prefix>
    filters by block type (e.g.,
    --type heading
    only searches for headings);
    --limit N
    controls the maximum number of returned results (default 20). Outputs
    {total,shown,blocks:[{...,summary,snippet}]}
    : if
    total > shown
    , it means hits are truncated; use more specific keywords;
    summary
    is exactly the same as in list-blocks and can be directly used as
    --expect
    , while
    snippet
    is the context of 40 characters before and after the hit (the hit word is marked with 【】) only for confirming that the correct block is found, do not use it to fill in
    --expect
    . If no hits are found, returns
    total: 0
    with exit code 0, which is not considered an error. To search for content starting with
    --
    , use
    --regex
    with escaping. If "Unknown edit operation" is reported, the extension in the browser is too old; ask the user to reload/update the extension.
  • insert-after
    matches exactly by title text: reports
    anchor_not_found
    if not found, and
    anchor_ambiguous
    if multiple matches are found (in this case, use
    insert-after-block
    to locate by block ID).
  • replace-all
    is the most dangerous operation (replaces the entire body of the document, keeping the document title)
    , and the process must be: ① First use
    fetch.mjs
    to export the original document to local for backup; ② Get the
    summary
    of the first block from the
    list-blocks
    output; ③ Pass it as
    --expect-first
    exactly. The extension compares the first block of the current document before execution, and reports
    block_changed
    if it does not match (indicating the document has been modified; repeat steps ①②). Do not execute replace-all without a backup.
  • Image writing:
    ![](image)
    in md supports three sources — local path (relative path is based on the directory of the md file), external http(s) link (automatically downloaded and embedded), and data URI. Only png/jpg/jpeg/gif/webp/bmp formats are accepted (svg/ico will be silently discarded by Feishu, and the CLI will directly report
    image_unsupported
    ). If the overall size exceeds 2MB after image embedding,
    image_too_large
    is reported (compress the image or split the writing); if external link download fails,
    image_fetch_failed
    is reported; if the local file is missing,
    image_not_found
    is reported. Writing with images automatically switches to HTML paste mode (Feishu only recognizes data URI images in HTML paste), with the side effect: md tables in the same write operation will be pasted as embedded spreadsheets instead of native tables — try to write images and tables separately. For pure image writing (no text), the read-back verification is determined by the net increase in the number of image blocks.
  • Success of writing is determined by read-back verification: After editing, the full text is re-exported to confirm that the new content has been saved before returning success. If
    save_unconfirmed
    is reported, the content may have been written to the document; do not blindly re-run (it may write twice), ask the user to open the document and confirm manually.
  • If you want to perform block-level operations after successful editing, re-run
    list-blocks
    first
    . Feishu blocks are addressed by ID; the IDs of blocks that have not been modified remain valid after editing, but two types of references will definitely become invalid: ①
    replace-block
    will change the block ID (Feishu handles it as "delete old block + insert new block"), and the old ID will become invalid directly; ② You do not have the block ID of the newly written content. Using the old list will result in
    block_not_found
    /
    block_changed
    ; re-running
    list-blocks
    is the easiest solution.
  • Edit-specific exit codes:
    6
    = Logged in but read-only for the document (
    need_edit_permission
    ), ask the user to confirm/apply for edit permissions in the browser before re-running;
    7
    = Extension lacks debugging permissions (
    need_edit_grant
    , usually an old version of the extension without editing functionality is installed), ask the user to update/reload the extension. Other exit codes are the same as fetch.mjs.
  • Edit-specific error subtypes (exit code 1):
    anchor_not_found
    /
    anchor_ambiguous
    /
    block_not_found
    /
    block_changed
    (retry after re-running list-blocks) /
    save_unconfirmed
    (manual confirmation) /
    inject_failed
    .
  • extension_outdated
    (exit code 5): The extension version is too old to support editing; ask the user to update/rebuild the extension.

普通网页转 Markdown

Convert Regular Web Pages to Markdown

链接不是飞书文档时,自动走「普通网页」管线:后台开标签页 → 整页正文提取(Readability)→ 转 Markdown。
  • 只支持
    md
    :请求
    --format pdf/html
    会直接报错,不会开标签页。
  • 产物是单个
    .md
    文件
    ,落到
    <输出目录>/<页面标题>/<页面标题>.md
    ; 图片保留为外链绝对 URL(
    https://...
    ),不下载到本地(与飞书文档不同,没有
    images/
    目录;例外:小红书的图片会本地化,见下节)。
  • 未授权域名时退出码
    4
    ,提示用户在该网页的扩展侧边栏点「授权访问该域名」后重跑。
  • 前端渲染型页面(SPA)可能因页面加载策略抓到不完整正文,属已知局限;静态文章页效果最好。
When the link is not a Feishu document, it automatically uses the "regular web page" pipeline: open a background tab → extract full-page body (Readability) → convert to Markdown.
  • Only
    md
    is supported
    : Requesting
    --format pdf/html
    will directly report an error without opening a tab.
  • The output is a single
    .md
    file
    , saved to
    <output directory>/<page title>/<page title>.md
    ; Images are kept as absolute external URLs (
    https://...
    ) and not downloaded locally (unlike Feishu documents, there is no
    images/
    directory; exception: Xiaohongshu images will be localized, see the next section).
  • If the domain is not authorized, exit code
    4
    is returned, prompting the user to click "Authorize access to this domain" in the extension sidebar on that web page before re-running.
  • Frontend-rendered pages (SPA) may capture incomplete body content due to page loading strategies, which is a known limitation; static article pages work best.

小红书笔记(xiaohongshu.com)

Xiaohongshu Notes (xiaohongshu.com)

小红书走专属适配器(通用管线在小红书页面只能抓到导航壳),额外要求:
  • 用户需先在 Chrome 登录小红书(笔记详情页必须持登录态),未登录退出码
    3
    ,提示会带上
    xiaohongshu.com
    ;
  • 需授权
    xiaohongshu.com
    域名(首次退出码
    4
    按提示授权);
  • 必须用带
    xsec_token
    的完整分享链接
    (App/网页版「分享」复制出来的链接自带;裸
    /explore/<id>
    链接已不可达);
    xhslink.com
    短链可直接用,会自动跳到完整链接;
  • 产物 Markdown 含标题、作者、发布时间、互动数、正文、图片、标签;图片会下载到本地
    images/
    并改写为相对路径(小红书 CDN 链接有时效签名,放着会过期;单张下载失败会保留外链不影响整体);视频保留外链不下载,直链签名可能过期,需尽快另存。
Xiaohongshu uses a dedicated adapter (the general pipeline can only capture the navigation shell on Xiaohongshu pages), with additional requirements:
  • The user must first log in to Xiaohongshu in Chrome (the note detail page must have a login state); if not logged in, exit code
    3
    is returned, with the prompt including
    xiaohongshu.com
    ;
  • Authorization for the
    xiaohongshu.com
    domain is required (exit code
    4
    is returned for the first time, follow the prompt to authorize);
  • Must use the full sharing link with
    xsec_token
    (links copied from the "Share" function in the app/web version come with it; bare
    /explore/<id>
    links are no longer accessible);
    xhslink.com
    short links can be used directly and will automatically redirect to the full link;
  • The output Markdown includes title, author, publication time, interaction count, body, images, and tags; images will be downloaded to the local
    images/
    directory and rewritten with relative paths
    (Xiaohongshu CDN links have time-sensitive signatures and will expire if left as-is; if a single image fails to download, the external link will be retained without affecting the whole); videos are kept as external links and not downloaded, and the direct link signature may expire, so save them as soon as possible.

arXiv 论文下载(PDF + HTML + Markdown 一起落地)

arXiv Paper Download (PDF + HTML + Markdown Saved Together)

链接或 ID 指向 arXiv 论文时,不走 daemon/扩展(arXiv 完全公开,不需要登录态),改用独立脚本直接下载:
bash
node ~/.claude/skills/larksnap-fetch/scripts/arxiv.mjs <arXiv链接或ID> <输出目录> [--pdf-only|--html-only]
  • 链接和 ID 全兼容:裸 ID(
    2601.18226
    )、
    arXiv:
    前缀、
    arxiv.org
    的 abs/pdf/html 三种链接(含
    .pdf
    后缀、
    v2
    版本号)、老式 ID(
    math.GT/0309136
    ,做目录名时斜杠替换为下划线)。
  • 产物同样独立成夹,文件夹用 ID 命名:
    <输出目录>/2601.18226/2601.18226.pdf
    +
    .html
    +
    .md
  • HTML 里注入了
    <base>
    ,图片/样式解析回 arxiv.org 绝对地址,本地打开不裂图(图片本身不下载到本地)。
  • Markdown 由 HTML 就地转换,零外部依赖(turndown 打包在
    scripts/vendor/
    里,不需要 pandoc):公式用 LaTeXML 自带的 alttext 还原成
    $...$
    /
    $$...$$
    ,图片/引用链接为 arxiv.org 绝对地址;复杂表格以内嵌 HTML 保留。转换失败只影响
    .md
    ,PDF/HTML 照常落地。
  • 部分论文没有 HTML 版属正常(arXiv 只为有 LaTeX 源且转换成功的论文提供 HTML,老论文也可能已被回补):此时只落 PDF,退出码仍为 0,stderr 有
    提示——不要当成失败重试
  • 退出码:
    0
    成功 |
    1
    失败 |
    2
    用法错;错误契约与 fetch.mjs 相同(非 0 退出时 stderr 最后一行是一行 JSON,按
    hint
    分支)。
When the link or ID points to an arXiv paper, it does not use the daemon/extension (arXiv is completely public and does not require a login state), and instead uses an independent script to download directly:
bash
node ~/.claude/skills/larksnap-fetch/scripts/arxiv.mjs <arXiv link or ID> <output directory> [--pdf-only|--html-only]
  • Fully compatible with links and IDs: bare ID (
    2601.18226
    ),
    arXiv:
    prefix, three types of
    arxiv.org
    links (abs/pdf/html, including
    .pdf
    suffix and
    v2
    version number), and old-style IDs (
    math.GT/0309136
    , slashes are replaced with underscores when used as directory names).
  • The output is also saved to an independent folder named after the ID:
    <output directory>/2601.18226/2601.18226.pdf
    +
    .html
    +
    .md
    .
  • <base>
    is injected into the HTML, so images/styles are resolved to absolute addresses on arxiv.org, and local opening will not break images (images themselves are not downloaded locally).
  • Markdown is converted from HTML on-site with zero external dependencies (turndown is packaged in
    scripts/vendor/
    , no need for pandoc): formulas are restored to
    $...$
    /
    $$...$$
    using the alttext from LaTeXML, images/reference links are absolute addresses on arxiv.org; complex tables are retained as embedded HTML. Conversion failure only affects
    .md
    , while PDF/HTML are saved normally.
  • It is normal for some papers to not have an HTML version (arXiv only provides HTML for papers with LaTeX sources that have been successfully converted, and old papers may also have been retroactively converted): in this case, only the PDF is saved, exit code is still 0, and there is an
    prompt in stderr — do not retry as a failure.
  • Exit codes:
    0
    Success |
    1
    Failure |
    2
    Usage error; the error contract is the same as fetch.mjs (when exiting with non-0, the last line of stderr is a JSON object, follow the
    hint
    branch).

网页视频下载(daemon 顺带承载,不走 CLI)

Web Page Video Download (Carried by Daemon, No CLI Entry)

daemon(v1.4.0 / 协议 v3 起)还承载一条扩展发起的反向任务:用户在视频站点(B 站/YouTube/ 抖音/TikTok)的扩展侧边栏点「下载视频」,扩展把页面 URL 经 WS 交给 daemon,daemon 在本机跑
yt-dlp
下载合并,落到
~/Downloads/larksnap-video/
,进度推回侧边栏显示。
  • 本功能没有 CLI 入口,触发点只在扩展侧边栏;本技能的职责是把新版 daemon 分发出去。
  • 额外依赖
    yt-dlp
    ffmpeg
    (仅视频下载用到,抓取/编辑飞书文档不需要): macOS
    brew install yt-dlp ffmpeg
    ;Windows
    winget install yt-dlp.yt-dlp Gyan.FFmpeg
    。 缺失时侧边栏会收到明确的安装提示,不影响其他功能。
  • daemon 是旧版时,侧边栏的下载按钮会提示更新本技能;更新技能文件后杀掉旧 daemon (
    pkill -f bridge/daemon.mjs
    或等它空闲自退)即可。
Starting from daemon v1.4.0 / protocol v3, a reverse task initiated by the extension is also supported: when the user clicks "Download Video" in the extension sidebar on video sites (Bilibili/YouTube/Douyin/TikTok), the extension sends the page URL to the daemon via WS, and the daemon runs
yt-dlp
locally to download and merge the video, saving it to
~/Downloads/larksnap-video/
, with progress pushed back to the sidebar for display.
  • This function has no CLI entry, and can only be triggered from the extension sidebar; the responsibility of this skill is to distribute the new version of the daemon.
  • Additional dependencies on
    yt-dlp
    and
    ffmpeg
    (only used for video download, not required for capturing/editing Feishu documents): macOS:
    brew install yt-dlp ffmpeg
    ; Windows:
    winget install yt-dlp.yt-dlp Gyan.FFmpeg
    . If missing, the sidebar will receive a clear installation prompt, which does not affect other functions.
  • When the daemon is an old version, the download button in the sidebar will prompt to update this skill; after updating the skill files, kill the old daemon (
    pkill -f bridge/daemon.mjs
    or wait for it to exit automatically when idle).

错误契约(AI 按此分支,不要解析散文)

Error Contract (AI Branches According to This, Do Not Parse Prose)

任何非 0 退出时,stderr 的最后一行是一行 JSON,前面几行是给人读的散文:
json
{"ok":false,"error":{"type":"authentication","subtype":"need_login","message":"需要登录:浏览器里没有该域名的飞书登录态。","hint":"让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。","retryable":false}}
  • message
    = 哪里错了(给人读);
    hint
    = 下一步做什么(命令式,照着执行);
  • retryable: true
    = 不用改任何东西,直接重跑本命令就可能成功;
    false
    = 先按
    hint
    行动(通常需要用户操作),再重跑。
  • type
    /
    subtype
    是闭合枚举,退出码由 subtype 派生:
退出码typesubtype处理
2usage
bad_args
/
profile_not_found
/
profile_ambiguous
/
host_ambiguous
修正命令行参数(如加/改
--profile
--host
)后重跑
2edit
need_landing_url
该域名暂无可用租户入口:这一次喂个该域名下的链接,或让用户打开一个该域名页面后重跑
3authentication
need_login
让用户在 Chrome 登录该域名的飞书,确认后重跑
4authentication
need_domain_auth
让用户在扩展侧边栏点「授权该域名」,确认后重跑
4usage
host_not_authorized
--host
不在已授权清单:换清单里的域名,或让用户先授权该域名
5bridge
daemon_missing
/
daemon_spawn_failed
/
daemon_timeout
/
bridge_request_failed
/
extension_not_connected
/
extension_outdated
/
extension_timeout
/
signature_invalid
按 hint 修桥接(多为唤醒/更新扩展)后重跑
1export
export_failed
/
write_failed
/
no_result
/
unexpected
按 hint 处理
When exiting with any non-0 code, the last line of stderr is a JSON object, and the previous lines are prose for human reading:
json
{"ok":false,"error":{"type":"authentication","subtype":"need_login","message":"需要登录:浏览器里没有该域名的飞书登录态。","hint":"让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。","retryable":false}}
  • message
    = What went wrong (for human reading);
    hint
    = What to do next (imperative, follow it);
  • retryable: true
    = No changes needed, re-running the command directly may succeed;
    false
    = Take action according to
    hint
    first (usually requires user operation), then re-run.
  • type
    /
    subtype
    are closed enums, and exit codes are derived from subtypes:
Exit CodeTypeSubtypeHandling
2usage
bad_args
/
profile_not_found
/
profile_ambiguous
/
host_ambiguous
Correct command line parameters (e.g., add/modify
--profile
,
--host
) and re-run
2edit
need_landing_url
No available tenant entry for this domain: provide a link under this domain this time, or ask the user to open a page under this domain before re-running
3authentication
need_login
Ask the user to log in to Feishu for this domain in Chrome, confirm, then re-run
4authentication
need_domain_auth
Ask the user to click "Authorize this domain" in the extension sidebar, confirm, then re-run
4usage
host_not_authorized
--host
is not in the authorized list: use a domain from the list, or ask the user to authorize the domain first
5bridge
daemon_missing
/
daemon_spawn_failed
/
daemon_timeout
/
bridge_request_failed
/
extension_not_connected
/
extension_outdated
/
extension_timeout
/
signature_invalid
Fix the bridge according to the hint (mostly wake up/update the extension) and re-run
1export
export_failed
/
write_failed
/
no_result
/
unexpected
Handle according to the hint

示例输出

Example Output

成功(退出码 0,stdout):
✓ 已导出到 /Users/me/notes/无监督数据修复
   - 无监督数据修复/无监督数据修复.md
   - 无监督数据修复/images/boxcnAbc123.png
需登录(退出码 3,stderr):
✗ 需要登录:浏览器里没有该域名的飞书登录态。
  → 让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。
{"ok":false,"error":{"type":"authentication","subtype":"need_login","message":"需要登录:浏览器里没有该域名的飞书登录态。","hint":"让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。","retryable":false}}
扩展未连接(退出码 5,stderr):
✗ 扩展未连接:请确认 Chrome 已打开并加载 larksnap 扩展,点一下图标唤醒后台后重试。
  → 确认 Chrome 已打开并加载 larksnap 扩展,点一下扩展图标唤醒后台,然后重跑本命令。
{"ok":false,"error":{"type":"bridge","subtype":"extension_not_connected","message":"...","hint":"...","retryable":true}}
多 profile 需指定(退出码 2,stderr):
✗ 检测到多个浏览器 profile(a1b2c3, d4e5f6),请用 --profile <code> 指定其一。
  → 加 --profile <code> 指定用哪个浏览器 profile(code 见扩展弹窗,可点 Copy 复制),然后重跑本命令。
{"ok":false,"error":{"type":"usage","subtype":"profile_ambiguous","message":"...","hint":"...","retryable":false}}
Success (exit code 0, stdout):
✓ 已导出到 /Users/me/notes/无监督数据修复
   - 无监督数据修复/无监督数据修复.md
   - 无监督数据修复/images/boxcnAbc123.png
Login required (exit code 3, stderr):
✗ 需要登录:浏览器里没有该域名的飞书登录态。
  → 让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。
{"ok":false,"error":{"type":"authentication","subtype":"need_login","message":"需要登录:浏览器里没有该域名的飞书登录态。","hint":"让用户在 Chrome 中打开该文档域名并登录飞书,登录完成后重跑本命令。","retryable":false}}
Extension not connected (exit code 5, stderr):
✗ 扩展未连接:请确认 Chrome 已打开并加载 larksnap 扩展,点一下图标唤醒后台后重试。
  → 确认 Chrome 已打开并加载 larksnap 扩展,点一下扩展图标唤醒后台,然后重跑本命令。
{"ok":false,"error":{"type":"bridge","subtype":"extension_not_connected","message":"...","hint":"...","retryable":true}}
Multiple profiles need to be specified (exit code 2, stderr):
✗ 检测到多个浏览器 profile(a1b2c3, d4e5f6),请用 --profile <code> 指定其一。
  → 加 --profile <code> 指定用哪个浏览器 profile(code 见扩展弹窗,可点 Copy 复制),然后重跑本命令。
{"ok":false,"error":{"type":"usage","subtype":"profile_ambiguous","message":"...","hint":"...","retryable":false}}

执行流程(CC 按此操作)

Execution Flow (CC Operates According to This)

  1. 直接运行上面的命令(用户给的链接 + 用户指定的本地目录;用户没指定目录时,默认落到当前工作目录)。daemon 会被自动拉起。
  2. 非 0 退出 → 解析 stderr 最后一行 JSON,按
    hint
    执行,不要自行猜测:
    • retryable: true
      → 按 hint 做完(如点扩展图标唤醒)直接重跑同一条命令,同样的错误连续出现 2 次就停下问用户。
    • retryable: false
      → hint 里需要用户操作的(登录/授权域名),把 hint 转述给用户,等用户确认完成后重跑。
  3. 退出码 0 → 把 stdout 里列出的写入文件告诉用户(路径形如
    <文档名>/<文档名>.md
    )。 产物在
    <输出目录>/<文档名>/
    子文件夹里(
    md
    images/
    子目录用相对路径引用),每篇文档独立成夹。
  1. Run the command directly (user-provided link + user-specified local directory; if the user does not specify a directory, it defaults to the current working directory). The daemon will be automatically launched.
  2. Non-0 exit → Parse the last line of JSON in stderr, follow the
    hint
    to execute, do not guess:
    • retryable: true
      → After completing the hint (e.g., click the extension icon to wake it up), re-run the same command directly; if the same error occurs twice in a row, stop and ask the user.
    • retryable: false
      → If the hint requires user operation (login/authorize domain), relay the hint to the user, wait for the user to confirm completion, then re-run.
  3. Exit code 0 → Tell the user the written files listed in stdout (path format like
    <document name>/<document name>.md
    ). The output is in the subfolder
    <output directory>/<document name>/
    (md includes the
    images/
    subdirectory referenced with relative paths), and each document is stored independently.

完整工作流示例(未登录 → 自愈 → 成功)

Complete Workflow Example (Not Logged In → Self-Healing → Success)

第 1 步  node .../fetch.mjs https://xxx.feishu.cn/docx/AbCd... ./notes
        → 退出码 3,JSON: {"subtype":"need_login","hint":"让用户在 Chrome 中打开该文档域名并登录飞书…"}
第 2 步  对用户说:「需要先在 Chrome 里登录 xxx.feishu.cn 的飞书,登录好了告诉我。」
第 3 步  用户确认后,原样重跑第 1 步命令
        → 退出码 0,stdout 列出写入文件 → 告诉用户产物路径,结束。
Step 1  node .../fetch.mjs https://xxx.feishu.cn/docx/AbCd... ./notes
        → Exit code 3, JSON: {"subtype":"need_login","hint":"让用户在 Chrome 中打开该文档域名并登录飞书…"}
Step 2  Tell the user: "You need to log in to Feishu for xxx.feishu.cn in Chrome first. Let me know when you're done."
Step 3  After the user confirms, re-run the command from Step 1 exactly
        → Exit code 0, stdout lists written files → Tell the user the output path and end.

首次安装(仅一次,只需加载扩展,无系统级安装)

First-time Installation (Only Once, Just Load the Extension, No System-level Installation)

扩展来自 larksnap 仓库(本技能不含扩展本体,只含桥接 daemon):
  1. 在 larksnap 仓库根
    npm run build
  2. chrome://extensions
    开「开发者模式」→「加载已解压的扩展程序」选
    dist/
  3. 点一下扩展图标唤醒后台 Service Worker —— 它会自动连本地 daemon(daemon 由首次
    fetch.mjs
    拉起)。
不需要绑扩展 ID、不写任何系统清单。换机器只要重复这三步(再把本技能目录拷到新机器的
~/.claude/skills/
)。
The extension comes from the larksnap repository (this skill does not include the extension itself, only the bridge daemon):
  1. Run
    npm run build
    in the root directory of the larksnap repository.
  2. Go to
    chrome://extensions
    , enable "Developer mode" → "Load unpacked" and select the
    dist/
    folder.
  3. Click the extension icon to wake up the background Service Worker — it will automatically connect to the local daemon (the daemon is launched by the first
    fetch.mjs
    run).
No need to bind the extension ID or write any system manifest. To switch machines, just repeat these three steps (and copy this skill directory to the new machine's
~/.claude/skills/
).

排查

Troubleshooting

  • ~/.larksnap/daemon.log
    看 daemon 是否在 listen、扩展是否连上、任务是否派发;
    ~/.larksnap/daemon.pid
    是在监听实例的 PID。
  • 连不上:确认 Chrome 开着、扩展已加载;扩展后台休眠时第一条命令可能要等它经
    /ping
    重连(alarms ~24s),点一下扩展图标可立即唤醒。
  • 端口冲突:daemon 默认
    127.0.0.1:19925
    ,可用环境变量
    LARKSNAP_PORT
    改(需与扩展
    src/background/bridge.ts
    里的
    PORT
    一致)。
  • 图片缺失/某域名导出失败:多为未按「基础域通配」授权该域名(含图片 drive-stream 子域),在侧边栏重新授权。
  • 版本漂移:fetch.mjs 发现在跑的 daemon 版本和本技能不一致时会自动重启到自己带的版本(装在多个项目的旧技能不会悄悄坏掉,但旧技能副本建议也更新);扩展和 daemon 的 WS 握手互报协议版本,不一致时 popup 会提示更新。
  • 安全:daemon 只绑回环、校验 Origin(非
    chrome-extension://
    拒)+ 要求
    X-Larksnap
    头,挡掉网页 CSRF;CLI→daemon 的请求另有 HMAC-SHA256 签名(key 在
    ~/.larksnap/secret
    ,0600,首次自动生成;签名覆盖时间戳/method/path/body 摘要,60s 防重放,无回落),挡掉本机其他身份的冒充。扩展侧 WS 读不了 key 文件,维持 Origin 校验(已知局限)。
  • Check
    ~/.larksnap/daemon.log
    to see if the daemon is listening, if the extension is connected, and if tasks are dispatched;
    ~/.larksnap/daemon.pid
    is the PID of the listening instance.
  • Connection issues: Confirm Chrome is open and the extension is loaded; when the extension background is dormant, the first command may need to wait for it to reconnect via
    /ping
    (alarms ~24s), clicking the extension icon can wake it up immediately.
  • Port conflict: The daemon defaults to
    127.0.0.1:19925
    , which can be changed with the environment variable
    LARKSNAP_PORT
    (must be consistent with the
    PORT
    in the extension's
    src/background/bridge.ts
    ).
  • Missing images/failed export for a domain: Mostly due to not authorizing the domain with "base domain wildcard" (including the image drive-stream subdomain), re-authorize in the sidebar.
  • Version drift: When fetch.mjs detects that the running daemon version is inconsistent with this skill, it will automatically restart to the version included with this skill (old skill copies installed in multiple projects will not break silently, but it is recommended to update old skill copies as well); the extension and daemon report protocol versions to each other during WS handshake, and the popup will prompt for an update if they are inconsistent.
  • Security: The daemon only binds to the loopback interface, verifies the Origin (rejects non-
    chrome-extension://
    requests) and requires the
    X-Larksnap
    header to block web CSRF; requests from CLI→daemon have an additional HMAC-SHA256 signature (key is in
    ~/.larksnap/secret
    , 0600 permissions, automatically generated on first run; the signature covers timestamp/method/path/body digest, 60s anti-replay, no fallback) to block impersonation from other identities on the machine. The extension's WS cannot read the key file, so Origin verification is maintained (known limitation).