markdown The command group treats the .md as an ordinary file for whole-file read/write, preserving the original Markdown source,does not perform Markdown ↔ Feishu docx block conversion.
feishu-cli: if not yet installed, please go to riba2534/feishu-cli to get the installation method.
Core concept: the difference from doc import / doc export difference
| Command | Behavior | Type created | Use case |
|---|---|---|---|
doc import |
Markdown → Feishu docx blocks (headings/lists/tables/Callout/Mermaid whiteboard…) | docx (online collaborative document) | For people to read, needs formatting, needs team comments |
doc export |
docx → Markdown (blocks parsed back to markdown source) | Local .md |
Save from Feishu docx to disk into Git |
markdown create/fetch/overwrite |
Upload/download the .md as a whole,without conversion |
file (ordinary Drive file) | AI agent writes the original .mddirectly to disk, preserving fenced code block indentation, for use as an image host/dense code blocks/version management |
Deciding which path to take:
- Want Feishu docx rendering (human-readable, formatting, table chunking, whiteboard rendering) →
doc import - Want the original
.mdtext preserved unchanged on Drive (AI agent reads back exactly the same; docx block parsing is lossy) →markdown create - Want to repeatedly overwrite the same
.md, keeping file_token unchanged (share link persists) →markdown overwrite
Prerequisites
- Authentication: all
markdowncommands default to User Access Token(runfeishu-cli auth loginlogin) - Precheck:
feishu-cli auth check --scope "drive:file:upload drive:file:download"
Command quick reference
1. markdown create — upload a new .md to Drive
# 从字符串内容创建
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
Key flag:
| flag | Description |
|---|---|
--name |
Remote filename,must .md end with。create:--content required when,--content-file can be omitted to use the local basename when.overwrite:--content required when,--content-file can be omitted to use the local basename when; explicitly passing it = also renames |
--content |
String content (choose one of the two with --content-file ) |
--content-file |
Local .md file path |
--file |
Official lark-cli compatible alias, equivalent to --content-file |
--folder-token |
Target folder (defaults to Drive root) |
-o json |
JSON output (including file_token / file_name / size_bytes) |
--user-access-token |
Override login state |
Validation: empty content errors out directly (creating an empty .md)。
2. markdown fetch — download a Drive .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
Key flag:
| flag | Description |
|---|---|
--file-token |
Markdown file token (required) |
--output-path |
Local save path;when a directory, append <fileToken>.md; defaults to printing to stdout |
--overwrite |
Overwrite when the local file already exists (defaults to erroring out directly) |
-o, --output json |
JSON output; when --output-path is not passed, includes content string |
3. markdown overwrite — overwrite existing .md (preserve 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
Key flag:
| flag | Description |
|---|---|
--file-token |
Target file token (required) |
--content / --content-file |
New content (choose one of two) |
--name |
Filename after overwrite (.md ending;--content required when;--content-file defaults to local basename) |
-o json |
JSON output |
Core value:file_token remains unchanged → share links persist, links bookmarked by others don't break; for multi-iteration scenarios (an AI agent updating the same .md) is better than "delete and recreate".
4. markdown diff — compare locally against the remote latest/historical version (read-only, does not modify the remote)
Download the remote Markdown content and compute a unified diff locally,does not modify the remote file. Three comparison modes (determined by parameter combinations, mutually exclusive):
# 模式 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
Key flag:
| flag | Description |
|---|---|
--file-token |
Target Markdown file token (required) |
--file |
Local .md path (mode 1: compare with remote latest) |
--from-version |
Starting remote version number (mode 2/3) |
--to-version |
Target remote version number (mode 3, requires --from-version) |
--context-lines |
Number of unchanged context lines to keep above and below each hunk (default 3) |
--dry-run |
Only print the comparison plan, do not download/compare |
--format |
json|pretty|table|ndjson|csv structured output; by default prints unified diff text |
--jq |
Built-in gojq to filter structured output (e.g. --jq '.added_lines', no external jq needed) |
-o json |
Compatible alias, equivalent to --format json |
Structured output structure(--format json / -o json, 9 top-level fields, can directly --jq get change count / determine consistency):
{
"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": "新内容" }
]
}
]
}
Common built-in --jq(no external jq needed):--jq '.identical' determine whether consistent,--jq '.added_lines, .removed_lines' get change count,--jq '.hunks[].lines[] | select(.op=="+") | .text' pull all added lines.
With
overwritecombined: before overwriting, firstdiff --file ./local.mdpreview local changes relative to remote latest, then overwrite after confirmation, to avoid accidental overwrites.
Underlying implementation & pitfalls
1. SDK does not expose file_token field —— build multipart yourself
Feishu Go SDK v3.5.3's UploadAllFileReqBody only exposes file_name / parent_type / parent_node / size / checksum / file,no file_token field.
But the official API POST /open-apis/drive/v1/files/upload_all does support file_token :With file_token it overwrites the original file preserving the token and refreshing version/size; without it, creates a new file in the specified directory by parent_type+parent_node Create in the specified directory。
To bypass the SDK limitation,internal/client/markdown.go:OverwriteFileWithToken uses client.Post + *larkcore.Formdata to build multipart itself (the translator detects *Formdata and automatically switches to the FileUpload multipart serialization path, see SDK core/reqtranslator.go:payload). The endpoint is still the official upload_all, referring to the lark-cli shortcuts/markdown/helpers.go:uploadMarkdownFileAll implementation.
2. File size limit ≤ 20MB
upload_all Single upload API limit 20MB, this command group only uses the single upload path:
markdown createcallsclient.UploadFileWithToken(when > 20MB this function automatically switches to the chunked pipeline, butmarkdown createmain scenario is text.mdfar smaller than 20MB)markdown overwriteonly supports small files ≤ 20MB. Overwriting large files requires the chunked upload API, which this command group does not implement. If it is a few tens of MB.md(rare, e.g., massive log posts), use thedrive uploadchunked pipeline to create a new file, which cannot preserve the original file_token.
markdown diff There is also a separate line count/matrix limit(unrelated to the 20MB upload limit, to prevent LCS matrix OOM):
- one side ≤ 20000 lines: if either side exceeds it, an error is reported
内容过大(N 行 / M 行),单侧超过行数上限 20000,建议用外部 diff 工具 - the product of the line counts of both sides ≤ 20 million cells: LCS uses a full
(n+1)×(m+1)int matrix, and memory grows with the product of the line counts of both sidesproductgrowth; if the product exceeds the limit, an error is reported内容过大(N × M 行),LCS 矩阵超过 20000000 单元上限(防 OOM),建议用外部 diff 工具 - both are intercepted after download completes and before computing the LCS before(
cmd/markdown_diff.go:checkDiffSize). Long.md(massive log posts, full code dumps) when running diff and hitting this error, switch to localdiff/git diffand other external tools, or first usemarkdown fetchto write both sides to disk and then compare externally.
3. .md suffix is strictly validated
--name(create) and --name(overwrite) both enforce a .md ending(lowercase suffix check); anything other than .md reports an error directly. If you want to store .markdown / .mdx use the drive upload old path.
4. Empty content reports an error directly
--content "" / empty --content-file are both rejected ("Markdown content is empty; creating/overwriting as an empty file is not supported"). If you need "clear" semantics, use markdown overwrite --content " " to write a placeholder space.
5. fetch path output and JSON output are separated
markdown fetch uses --output-path to save locally, and uses -o json to output JSON:
--output-path ./x.md→ write to disk at./x.md-o json→ print{file_token, content, size_bytes}to stdout- The two can be combined: write to disk + also output a JSON summary to stdout
6. --output-path fallback filename when it is a directory
markdown fetch --output-path ./downloads/ will be concatenated into ./downloads/<fileToken>.md(does not parse the original filename from the response headers, consistent with drive download behavior). To preserve the original filename, add a query step yourself and then build the path.
When to switch to doc import/export
- If you want the Feishu docx rendering experience(human-readable, table rows, Mermaid converted to boards, Callout highlighting) →
doc import(seefeishu-cli-importskill) - If you want to write Feishu docx to disk into a Git repository(block parsing back to markdown) →
doc export(seefeishu-cli-exportskill) - If you want to sanity check Markdown before conversion(Mermaid braces, table 9×9 limit, 6 Callout types) →
../feishu-cli-import/references/doc-guide.md
When to switch to drive upload
- Large files > 20MB(rare, but e.g. full code dumps, long logs) →
feishu-cli drive upload --file xxx.mduses chunking, which creates a new file_token - Non-
.mdextension(.mdx/.markdown/.txt)→drive upload
Permission requirements
| Command | Required scope |
|---|---|
markdown create |
drive:file:upload(or drive:drive) |
markdown fetch |
drive:file:download(or drive:drive) |
markdown overwrite |
drive:file:upload + drive:drive.metadata:readonly; and the User Token has edit permission on the target file |
markdown diff |
drive:file:download(or drive:drive) |
Recommended practice: after executing feishu-cli auth login to log in, use auth check --scope "drive:file:upload drive:file:download" for precheck; if a scope is missing, follow the prompt to auth login apply for it.
Typical workflow
Workflow A: An AI agent iterates on the same .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
Workflow B: From cloud drive .md write to local
# 读回原汁原味 Markdown 源码
feishu-cli markdown fetch --file-token boxcnxxx --output-path ./local.md --overwrite
# 编辑后写回
feishu-cli markdown overwrite --file-token boxcnxxx --content-file ./local.md
Workflow C: With doc import collaborate (first write .md then batch 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
Notes
- Default User Access Token: all
markdowncommands uniformly prompt when not logged infeishu-cli auth login - No Markdown conversion: this command group preserves
.mdbyte stream unchanged,does notperform Feishu docx block conversion,does nottrigger enhancement logic such as image hosting, whiteboard rendering, Callout, etc. — for these usedoc import upload_allsingle ≤ 20MB:create/overwriteshare the endpoint, large file overwrite is not implemented, work around withdrive upload(creates a new file_token)fetchoutput parameters: paths use--output-path, formats use-o/--output
.mdsuffix mandatory, empty content rejection are already covered in the "Core flags" and "Pitfalls" sections and will not be repeated here, seeoverwriteflag table and踩坑 §3/§4。
common errors
actual error examples (see cmd/markdown_overwrite.go + internal/client/markdown.go:OverwriteFileWithToken):
| trigger condition | error message | troubleshooting direction |
|---|---|---|
| User Token has no edit permission on the target file | 覆盖文件失败: code=<非 0>, msg=<飞书返回>(common 1061004 forbidden、1061045 no permission) |
In Feishu cloud drive, right-click the file → Share → grant the current user "Can edit"; or switch to the file owner's token; or check whether scope drive:file:upload has been granted |
File size exceeds 20MB (--content) |
--content 大小 <N> 字节超过 20MB API 上限 |
split the file or switch to drive upload(will create a new file_token, share link becomes invalid) |
File size exceeds 20MB (--content-file) |
--content-file 大小 <N> 字节超过 20MB API 上限,请切分或用多次 fetch+overwrite |
same as above; CLI os.Stat already intercepted before reading the disk, no upload bandwidth wasted |
--name does not end with .md ending |
--name 必须以 .md 结尾,得到 "xxx.txt" |
change to .md suffix;.mdx / .markdown must use drive upload |
--content-file path does not exist |
读取本地文件失败: open <path>: no such file or directory |
Check whether the path is relative to the current directory; passing an absolute path is recommended |
--content-file is a directory |
--content-file 必须指向文件,不是目录 |
point to a specific .md file |
both provided --content and provided --content-file |
--content 与 --content-file 不能同时使用 |
choose one |
| neither provided | 请提供 --content 或 --content-file |
provide at least one |
--content mode missing --name |
使用 --content 时必须提供 --name 指定远端文件名(保留原名请加 --name <现有文件名>.md) |
explicit --name existing.md(will not automatically fallback fileToken.md) |
| content is empty bytes | Markdown 内容为空,不支持把 .md 覆盖为空文件 |
for clearing semantics pass --content " "(placeholder space) |
| HTTP layer exception | 覆盖文件失败: HTTP <status>, body: <raw> |
network/proxy issue, includes raw body for troubleshooting |
| response parsing failed | 解析覆盖响应失败: <json error> |
Feishu side returned non-JSON (extremely rare, usually a gateway error page) |
when permission is insufficient the Feishu API directly returns
code != 0business error (e.g.1061004、1061045), CLI layer passes throughmsgfield; if scope is also missing it will be intercepted at theauth checkstage, it is recommended to runfeishu-cli auth check --scope "drive:file:upload"。
comparison with old commands
| Old command | New markdown command | Difference |
|---|---|---|
drive upload --file x.md |
markdown create --content-file x.md |
markdown mandatory .md suffix + empty content validation + AI agent friendly |
drive download --file-token xxx |
markdown fetch --file-token xxx |
markdown prints to stdout by default (text scenario) + directory path automatically joined .md |
| None | markdown overwrite |
Old commands have no overwrite semantics (can only "delete and recreate", file_token changes) |
Old drive upload/download is still available(binary, non- .md uses the old path), new capabilities are concentrated in markdown overwrite(preserves file_token for overwrite).
v1 PR quality-pass hardening
drive/v1/files/upload_allsingle upload ≤ 20MB(create / overwrite share the endpoint). The CLI layer does a pre-check in--content/--content-fileboth, and errors out directly if exceeded without wasting bandwidthoverwritemust explicitly--name(when using--content): no longer falls back to<fileToken>.mdsilently renaming the remote file; to keep the original name, explicitly--name <existing>.md