name: feishu-cli-markdown description: >- 飞书云盘原生 Markdown 文件操作(与 doc import/export 互补)。 markdown create 上传新 .md 到云盘;markdown fetch 下载为本地 .md; markdown overwrite 用本地 .md 覆盖云盘已有文件(保 file_token 不变,分享链接持久); markdown diff 在本地比对远端最新/历史版本,不改远端。 当 doc 文档不适合(图床、密集代码块、版本管理)走 .md 原生文件路径。 Drive upload_all + 自拼 Formdata multipart(SDK 不暴露 file_token field)。 当用户请求"上传 markdown"、"下载 md"、"覆盖云盘 md"、"md diff"、"比对 markdown 版本"时使用。 argument-hint: create | fetch | overwrite | diff user-invocable: true allowed-tools: Bash(feishu-cli markdown:), Bash(feishu-cli drive:), Bash(feishu-cli auth:*), Read
飞书云盘原生 Markdown(markdown create/fetch/overwrite/diff)
markdown 命令组把 Drive 上的 .md 当作普通文件整体读写,保留原始 Markdown 源码,不做 Markdown ↔ 飞书 docx 块的转换。
feishu-cli:如尚未安装,请前往 riba2534/feishu-cli 获取安装方式。
核心概念:与 doc import / doc export 的区别
| 命令 | 行为 | 创建出的类型 | 适用场景 |
|---|---|---|---|
doc import |
Markdown → 飞书 docx 块(标题/列表/表格/Callout/Mermaid 画板…) | docx(在线协同文档) | 给人读、要排版、要团队评论 |
doc export |
docx → Markdown(块解析回 markdown 源码) | 本地 .md |
从飞书 docx 落盘到 Git |
markdown create/fetch/overwrite |
把 .md 整体上传/下载,不转换 |
file(Drive 普通文件) | AI agent 直接落盘原汁原味 .md、保留 fenced code block 缩进、当图床/密集代码块/版本管理用 |
判断走哪条:
- 想要飞书 docx 渲染(人读、排版、表格分块、画板渲染)→
doc import - 想要原始
.md文本 unchanged 保留在云盘(AI agent 读回完全一致;docx 块解析有损)→markdown create - 想反复覆盖同一份
.md、保持 file_token 不变(分享链接持久)→markdown overwrite
前置条件
- 认证:所有
markdown命令默认走 User Access Token(执行feishu-cli auth login登录) - 预检:
feishu-cli auth check --scope "drive:file:upload drive:file:download"
命令速查
1. markdown create — 上传新 .md 到云盘
# 从字符串内容创建
feishu-cli markdown create --name plan.md --content "# Plan\n\n- todo 1"
# 从本地文件创建(--name 缺省时取本地 basename)
feishu-cli markdown create --content-file ./local.md
feishu-cli markdown create --file ./local.md
# 指定目标文件夹
feishu-cli markdown create --name draft.md --content-file ./tmp.md --folder-token fldxxx
# JSON 输出(拿 file_token 给后续步骤)
feishu-cli markdown create --name plan.md --content-file ./plan.md -o json
关键 flag:
| flag | 说明 |
|---|---|
--name |
远端文件名,必须 .md 结尾。create:--content 时必填、--content-file 时可省取本地 basename。overwrite:--content 时必填、--content-file 时可省取本地 basename;显式传入 = 同时改名 |
--content |
字符串内容(与 --content-file 二选一) |
--content-file |
本地 .md 文件路径 |
--file |
官方 lark-cli 兼容别名,等价于 --content-file |
--folder-token |
目标文件夹(缺省 Drive 根目录) |
-o json |
JSON 输出(含 file_token / file_name / size_bytes) |
--user-access-token |
覆盖登录态 |
校验:空内容直接报错(不允许创建空 .md)。
2. markdown fetch — 下载云盘 .md
# 打印到 stdout(缺省 --output-path 时,行为与 lark-cli markdown +fetch 一致)
feishu-cli markdown fetch --file-token boxcnxxx
# 落盘到本地路径
feishu-cli markdown fetch --file-token boxcnxxx --output-path ./local.md
# 路径已存在时强制覆盖
feishu-cli markdown fetch --file-token boxcnxxx --output-path ./local.md --overwrite
# JSON 输出(含 content 字符串)
feishu-cli markdown fetch --file-token boxcnxxx -o json
关键 flag:
| flag | 说明 |
|---|---|
--file-token |
Markdown 文件 token(必填) |
--output-path |
本地保存路径;目录时拼 <fileToken>.md;缺省打印 stdout |
--overwrite |
本地文件已存在时覆盖(缺省直接报错) |
-o, --output json |
JSON 输出;不传 --output-path 时包含 content 字符串 |
3. markdown overwrite — 覆盖已有 .md(保 file_token)
# 字符串覆盖
feishu-cli markdown overwrite --file-token boxcnxxx --name existing.md --content "新内容"
# 本地文件覆盖
feishu-cli markdown overwrite --file-token boxcnxxx --content-file ./new.md
feishu-cli markdown overwrite --file-token boxcnxxx --file ./new.md
# 覆盖 + 改名(必须 .md 结尾)
feishu-cli markdown overwrite --file-token boxcnxxx --content-file ./new.md --name renamed.md
关键 flag:
| flag | 说明 |
|---|---|
--file-token |
目标文件 token(必填) |
--content / --content-file |
新内容(二选一) |
--name |
覆盖后文件名(.md 结尾;--content 时必填;--content-file 缺省使用本地 basename) |
-o json |
JSON 输出 |
核心价值:file_token 保持不变 → 分享链接持久、其他人收藏的链接不失效;多次迭代场景(AI agent 每天更新同一份 .md)优于"删了重建"。
4. markdown diff — 本地比对远端最新/历史版本(只读,不改远端)
下载远端 Markdown 内容并在本地计算 unified diff,不修改远端文件。三种比对模式(由参数组合决定,互斥):
# 模式 1:远端最新 vs 本地文件
feishu-cli markdown diff --file-token boxcnxxx --file ./local.md
# 模式 2:远端某版本 vs 远端最新
feishu-cli markdown diff --file-token boxcnxxx --from-version 3
# 模式 3:远端版本 A vs 版本 B(--to-version 需配合 --from-version)
feishu-cli markdown diff --file-token boxcnxxx --from-version 2 --to-version 5 --context-lines 1
# 结构化输出 + 内置 jq 过滤(--format/--jq 与其它新命令一致;-o json 作兼容别名)
feishu-cli markdown diff --file-token boxcnxxx --file ./local.md --format json
feishu-cli markdown diff --file-token boxcnxxx --file ./local.md --jq '.added_lines'
feishu-cli markdown diff --file-token boxcnxxx --file ./local.md --format table --jq '{identical,added_lines,removed_lines}'
feishu-cli markdown diff --file-token boxcnxxx --file ./local.md -o json # 兼容写法,等价 --format json
关键 flag:
| flag | 说明 |
|---|---|
--file-token |
目标 Markdown 文件 token(必填) |
--file |
本地 .md 路径(模式 1:与远端最新比对) |
--from-version |
起始远端版本号(模式 2/3) |
--to-version |
目标远端版本号(模式 3,需配合 --from-version) |
--context-lines |
每个 hunk 上下保留的未变更上下文行数(默认 3) |
--dry-run |
只打印比对计划,不下载/不比对 |
--format |
json|pretty|table|ndjson|csv 结构化输出;缺省打印 unified diff 文本 |
--jq |
内置 gojq 过滤结构化输出(如 --jq '.added_lines',无需外部 jq) |
-o json |
兼容别名,等价 --format json |
结构化输出结构(--format json / -o json,顶层 9 字段,可直接 --jq 取改动量 / 判一致):
{
"detection": "local_vs_remote", // 比对模式:local_vs_remote(模式 1)/ remote_vs_remote(模式 2/3)
"from": "remote (latest)", // 左侧来源名(模式 1="remote (latest)";模式 2/3="remote@version=N")
"to": "local: ./local.md", // 右侧来源名(模式 1="local: <path>";模式 2="remote (latest)";模式 3="remote@version=N")
"size_bytes_before": 1024, // 左侧内容字节数
"size_bytes_after": 1088, // 右侧内容字节数
"identical": false, // 是否无差异(等价于 hunks 为空)
"added_lines": 5, // 新增行数(统计所有 hunk 中 op="+")
"removed_lines": 2, // 删除行数(统计所有 hunk 中 op="-")
"hunks": [ // unified diff 的 hunk 数组(无差异时为空数组)
{
"old_start": 10, "old_lines": 4, // 左侧起始行 / 行数
"new_start": 10, "new_lines": 7, // 右侧起始行 / 行数
"lines": [
{ "op": " ", "text": "上下文行" }, // op: " "=未变 / "-"=删除 / "+"=新增
{ "op": "-", "text": "旧内容" },
{ "op": "+", "text": "新内容" }
]
}
]
}
常用内置 --jq(无需外部 jq):--jq '.identical' 判是否一致、--jq '.added_lines, .removed_lines' 取改动量、--jq '.hunks[].lines[] | select(.op=="+") | .text' 拉所有新增行。
与
overwrite配合:覆盖前先diff --file ./local.md预览本地相对远端最新的改动,确认后再 overwrite,避免误覆盖。
底层实现 & 踩坑
1. SDK 不暴露 file_token field —— 自拼 multipart
飞书 Go SDK v3.5.3 的 UploadAllFileReqBody 只暴露 file_name / parent_type / parent_node / size / checksum / file,没有 file_token 字段。
但官方 API POST /open-apis/drive/v1/files/upload_all 是支持 file_token 的:带 file_token 时覆盖原文件保留 token、刷新 version/size;不带时按 parent_type+parent_node 在指定目录新建。
为绕开 SDK 限制,internal/client/markdown.go:OverwriteFileWithToken 用 client.Post + *larkcore.Formdata 自己拼 multipart(translator 检测到 *Formdata 会自动切到 FileUpload 多部分序列化路径,见 SDK core/reqtranslator.go:payload)。endpoint 仍是官方 upload_all,参考 lark-cli shortcuts/markdown/helpers.go:uploadMarkdownFileAll 的写法。
2. 文件大小上限 ≤ 20MB
upload_all 单次上传 API 上限 20MB,本命令组只走单次上传路径:
markdown create调client.UploadFileWithToken(> 20MB 时该函数会自动切到分片管线,但markdown create主场景是文本.md远小于 20MB)markdown overwrite只支持 ≤ 20MB 小文件,大文件覆盖需要分片接口,本命令组未实现。如果是几十 MB 的.md(罕见,比如海量日志贴)走drive upload分片管线创建新文件,无法保留原 file_token。
markdown diff 另有独立的行数/矩阵上限(与 20MB 上传上限无关,防 LCS 矩阵 OOM):
- 单侧 ≤ 20000 行:任一侧超过即报错
内容过大(N 行 / M 行),单侧超过行数上限 20000,建议用外部 diff 工具 - 两侧行数乘积 ≤ 2000 万单元:LCS 用完整
(n+1)×(m+1)int 矩阵,内存随两侧行数乘积增长;乘积超限报错内容过大(N × M 行),LCS 矩阵超过 20000000 单元上限(防 OOM),建议用外部 diff 工具 - 两条都在下载完成、计算 LCS 之前拦截(
cmd/markdown_diff.go:checkDiffSize)。长.md(海量日志贴、全量代码 dump)跑 diff 命中该报错时,改用本地diff/git diff等外部工具,或先用markdown fetch落盘两侧再外部比对。
3. .md 后缀强制校验
--name(create)和 --name(overwrite)都强制 .md 结尾(lowercase 检查后缀),非 .md 直接报错。如果想存 .markdown / .mdx 走 drive upload 老路径。
4. 空内容直接报错
--content "" / 空 --content-file 都被拒绝("Markdown 内容为空,不支持创建/覆盖为空文件")。需要"清空"语义的话走 markdown overwrite --content " " 写一个占位空格。
5. fetch 的路径输出与 JSON 输出分离
markdown fetch 使用 --output-path 保存到本地,使用 -o json 输出 JSON:
--output-path ./x.md→ 落盘到./x.md-o json→ 把{file_token, content, size_bytes}打到 stdout- 两者可叠加:落盘 + 同时 stdout JSON 摘要
6. --output-path 是目录时的兜底文件名
markdown fetch --output-path ./downloads/ 会拼成 ./downloads/<fileToken>.md(不从响应头解析原文件名,与 drive download 行为一致)。要保留原文件名请自己加查询步骤再拼路径。
何时转走 doc import/export
- 要飞书 docx 渲染体验(人读、表格分行、Mermaid 转画板、Callout 高亮)→
doc import(参feishu-cli-importskill) - 要从飞书 docx 落盘到 Git 仓库(块解析回 markdown)→
doc export(参feishu-cli-exportskill) - 要把 Markdown 转换前 sanity check(Mermaid 花括号、表格 9×9 限制、Callout 6 种类型)→
../feishu-cli-import/references/doc-guide.md
何时转走 drive upload
- 大文件 > 20MB(罕见,但比如全量代码 dump、长 log)→
feishu-cli drive upload --file xxx.md走分片,会创建新 file_token - 非
.md扩展名(.mdx/.markdown/.txt)→drive upload
权限要求
| 命令 | 所需 scope |
|---|---|
markdown create |
drive:file:upload(或 drive:drive) |
markdown fetch |
drive:file:download(或 drive:drive) |
markdown overwrite |
drive:file:upload + drive:drive.metadata:readonly;且 User Token 对目标文件有编辑权限 |
markdown diff |
drive:file:download(或 drive:drive) |
推荐做法:执行 feishu-cli auth login 登录后,由 auth check --scope "drive:file:upload drive:file:download" 预检;缺 scope 时按提示 auth login 补申请。
典型工作流
工作流 A:AI agent 每天迭代同一份 .md
# Day 1:创建一次拿到 file_token
FT=$(feishu-cli markdown create --name daily-summary.md --content-file ./summary.md -o json | jq -r '.file_token')
echo "$FT" > ~/.cache/daily-summary.token
# Day 2+:覆盖(file_token 不变,分享链接持久)
feishu-cli markdown overwrite --file-token "$(cat ~/.cache/daily-summary.token)" --content-file ./summary.md
工作流 B:从云盘 .md 落盘到本地
# 读回原汁原味 Markdown 源码
feishu-cli markdown fetch --file-token boxcnxxx --output-path ./local.md --overwrite
# 编辑后写回
feishu-cli markdown overwrite --file-token boxcnxxx --content-file ./local.md
工作流 C:与 doc import 协作(先落盘 .md 再批量 import)
# 上游产出 .md 到云盘(保留源码备份)
feishu-cli markdown create --name design.md --content-file ./design.md --folder-token fldxxx
# 下游再走 doc import 生成飞书 docx 给团队阅读
feishu-cli doc import ./design.md --title "设计稿" --upload-images
注意事项
- 默认 User Access Token:所有
markdown命令未登录时统一提示feishu-cli auth login - 不做 Markdown 转换:本命令组保留
.md字节流不变,不做飞书 docx 块转换,不触发图床、画板渲染、Callout 等增强逻辑——需要这些走doc import upload_all单次 ≤ 20MB:create/overwrite共用 endpoint,大文件覆盖未实现,绕路走drive upload(创建新 file_token)fetch输出参数:路径走--output-path,格式走-o/--output
.md后缀强制、空内容拒绝两条已经在「核心 flag」与「踩坑」章节出现,这里不再重复,详见overwriteflag 表 和踩坑 §3/§4。
常见错误
实际报错示例(参 cmd/markdown_overwrite.go + internal/client/markdown.go:OverwriteFileWithToken):
| 触发条件 | 错误信息 | 排查方向 |
|---|---|---|
| User Token 对目标文件无编辑权限 | 覆盖文件失败: code=<非 0>, msg=<飞书返回>(常见 1061004 forbidden、1061045 no permission) |
在飞书云盘右键文件 → 共享 → 给当前用户加「可编辑」;或换文件所有者的 token;或检查 scope drive:file:upload 是否已授予 |
文件大小超 20MB(--content) |
--content 大小 <N> 字节超过 20MB API 上限 |
切分文件或换 drive upload(会创建新 file_token,分享链接失效) |
文件大小超 20MB(--content-file) |
--content-file 大小 <N> 字节超过 20MB API 上限,请切分或用多次 fetch+overwrite |
同上;CLI 在读盘前 os.Stat 已拦截,不浪费上传带宽 |
--name 不以 .md 结尾 |
--name 必须以 .md 结尾,得到 "xxx.txt" |
改 .md 后缀;.mdx / .markdown 必须走 drive upload |
--content-file 路径不存在 |
读取本地文件失败: open <path>: no such file or directory |
检查路径是否相对当前目录;建议传绝对路径 |
--content-file 是目录 |
--content-file 必须指向文件,不是目录 |
指向具体 .md 文件 |
既给 --content 又给 --content-file |
--content 与 --content-file 不能同时使用 |
二选一 |
| 都没给 | 请提供 --content 或 --content-file |
至少传一个 |
--content 模式漏 --name |
使用 --content 时必须提供 --name 指定远端文件名(保留原名请加 --name <现有文件名>.md) |
显式 --name existing.md(不会自动 fallback fileToken.md) |
| 内容为空字节 | Markdown 内容为空,不支持把 .md 覆盖为空文件 |
要清空语义传 --content " "(占位空格) |
| HTTP 层异常 | 覆盖文件失败: HTTP <status>, body: <raw> |
网络/代理问题,附带原始 body 便于排查 |
| 响应解析失败 | 解析覆盖响应失败: <json error> |
飞书侧返回非 JSON(极罕见,通常网关错误页) |
权限不足时飞书 API 直接返回
code != 0的业务错(如1061004、1061045),CLI 层透传msg字段;若同时缺 scope 会在auth check阶段就拦截,建议执行前先跑feishu-cli auth check --scope "drive:file:upload"。
与老命令的对照
| 老命令 | 新 markdown 命令 | 差异 |
|---|---|---|
drive upload --file x.md |
markdown create --content-file x.md |
markdown 强制 .md 后缀 + 空内容校验 + AI agent 友好 |
drive download --file-token xxx |
markdown fetch --file-token xxx |
markdown 默认打印 stdout(文本场景)+ 目录路径自动拼 .md |
| 无 | markdown overwrite |
老命令没有覆盖语义(只能"删了重建",file_token 变化) |
老 drive upload/download 仍然可用(二进制、非 .md 走老路径),新能力集中在 markdown overwrite(保 file_token 覆盖)。
v1 PR quality-pass 加固
drive/v1/files/upload_all单次上传 ≤ 20MB(create / overwrite 共用 endpoint)。CLI 层在--content/--content-file都做 pre-check,超过直接报错不浪费带宽overwrite必须显式--name(用--content时):不再 fallback<fileToken>.md默默改名远端文件;保留原名请显式--name <existing>.md