feishu-doc-copy - 飞书文档完整复制技能
技能描述
功能:完整复制飞书云文档到同一账号的云空间,保持文字、表格、代码、格式 100% 一致(图片除外)。
适用场景:
- 有文档查看权限但无复制/下载权限
- 需要备份飞书文档
- 需要创建文档的完整副本
- 长文档(>10,000 字符)的完整复制
前提条件:
- 对源文档有读取权限(能打开查看)
- 有自己的云空间写入权限
- 已安装 OpenClaw 并配置飞书渠道
限制:
- 图片无法复制(飞书 API token 权限限制)
- 单次复制最大支持 100,000 字符
核心方法
分块读取 + 分块写入 + 严格验证
这是唯一可行的路径,因为:
- 飞书文档 API 单次读取有长度限制(limit 参数)
- 飞书文档编辑 API 单次写入有长度限制
- 文档无复制权限时,无法使用客户端复制粘贴
- 必须通过 API 分块搬运
完整实现流程
步骤 1:获取源文档信息
# 读取源文档前 50 字符,获取 total_length
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 50
返回值示例:
{
"doc_id": "ABC123xyz",
"title": "文档标题",
"total_length": 43548,
"has_more": true,
"length": 50,
"markdown": "..."
}
关键信息:
total_length:原文档总字符数(用于计算分块数量)doc_id:源文档 ID
步骤 2:计算分块策略
# 每块大小:9000 字符(推荐值,避免切断表格/代码块)
BLOCK_SIZE=9000
# 计算分块数量
BLOCK_COUNT = ceil(TOTAL_LENGTH / BLOCK_SIZE)
示例:
- 原文档 43,548 字符
- 每块 9,000 字符
- 分块数量:ceil(43548/9000) = 5 块
分块偏移量:
块 0: offset 0
块 1: offset 9000
块 2: offset 18000
块 3: offset 27000
块 4: offset 36000
步骤 3:创建临时目录
# 创建临时目录存储分块文件
TEMP_DIR="/tmp/feishu_copy_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$TEMP_DIR"
步骤 4:分块读取源文档
循环读取每一块:
OFFSET=0
BLOCK_NUM=0
while [ $OFFSET -lt $TOTAL_LENGTH ]; do
echo "读取块 $BLOCK_NUM(offset: $OFFSET)..."
# 读取当前块
openclaw feishu fetch-doc \
--doc_id "$SOURCE_DOC_ID" \
--limit $BLOCK_SIZE \
--offset $OFFSET \
> "$TEMP_DIR/chunk_${BLOCK_NUM}.json"
# 提取 markdown 内容
cat "$TEMP_DIR/chunk_${BLOCK_NUM}.json" | jq -r '.markdown' > "$TEMP_DIR/chunk_${BLOCK_NUM}.md"
# 检查是否还有更多
HAS_MORE=$(cat "$TEMP_DIR/chunk_${BLOCK_NUM}.json" | jq -r '.has_more')
if [ "$HAS_MORE" = "false" ]; then
echo "最后一块读取完成"
break
fi
OFFSET=$(( $OFFSET + $BLOCK_SIZE ))
BLOCK_NUM=$(( $BLOCK_NUM + 1 ))
# 避免 API 限流
sleep 0.5
done
关键验证点:
- 每次读取后检查
has_more标志 - 最后一块
has_more必须为false - 每块文件必须成功保存
步骤 5:创建新文档
# 创建新文档(初始内容可以是任意占位符)
openclaw feishu create-doc \
--title "<新文档标题>" \
--markdown "# <新文档标题>\n\n正在搬运中..."
返回值示例:
{
"doc_id": "XYZ789abc",
"doc_url": "https://www.feishu.cn/docx/XYZ789abc",
"message": "文档创建成功"
}
记录新文档 ID:NEW_DOC_ID
步骤 6:分块写入新文档
写入规则:
- 第 1 块(块 0):使用
overwrite模式 - 后续块(块 1-N):使用
append模式
TOTAL_BLOCKS=$BLOCK_NUM # 块编号从 0 开始,所以总块数 = 最大块编号 + 1
for i in $(seq 0 $BLOCK_NUM); do
CHUNK_FILE="$TEMP_DIR/chunk_${i}.md"
if [ $i -eq 0 ]; then
MODE="overwrite"
echo "写入块 $i($MODE 模式)..."
else
MODE="append"
echo "追加块 $i($MODE 模式)..."
fi
# 写入当前块
openclaw feishu update-doc \
--doc_id "$NEW_DOC_ID" \
--mode $MODE \
--markdown @"$CHUNK_FILE"
# 避免 API 限流
sleep 1
done
关键验证点:
- 第 1 块必须用
overwrite - 后续块必须用
append - 每次写入后检查返回状态
- 写入间隔至少 1 秒(避免限流)
步骤 7:验证结果
# 获取新文档信息
openclaw feishu fetch-doc \
--doc_id "$NEW_DOC_ID" \
--limit 50 \
> "$TEMP_DIR/new_doc_info.json"
# 提取新文档总长度
NEW_LENGTH=$(cat "$TEMP_DIR/new_doc_info.json" | jq -r '.total_length')
# 计算差异
DIFF=$(( $TOTAL_LENGTH - $NEW_LENGTH ))
PERCENT=$(echo "scale=2; $NEW_LENGTH * 100 / $TOTAL_LENGTH" | bc)
echo "原文档:$TOTAL_LENGTH 字符"
echo "新文档:$NEW_LENGTH 字符"
echo "差异:$DIFF 字符($PERCENT% 完整)"
# 验证通过条件:差异 < 2%
if [ $(echo "$PERCENT < 98" | bc) -eq 1 ]; then
echo "⚠️ 差异较大,请手动检查"
else
echo "✅ 验证通过"
fi
预期结果:
- 差异 < 2%:正常(主要是图片 token 占位符差异)
- 差异 > 5%:异常,需要检查哪块写入失败
步骤 8:清理临时文件
rm -rf "$TEMP_DIR"
完整自动化脚本
将以上流程整合为可执行脚本:
#!/bin/bash
# 飞书文档完整复制脚本
# 用法:./feishu-doc-copy.sh <源文档 ID> <新文档标题>
set -e # 遇到错误立即退出
SOURCE_DOC_ID="$1"
NEW_DOC_TITLE="$2"
BLOCK_SIZE=9000 # 每块字符数
if [ -z "$SOURCE_DOC_ID" ] || [ -z "$NEW_DOC_TITLE" ]; then
echo "用法:$0 <源文档 ID> <新文档标题>"
echo "示例:$0 ABC123xyz 文档标题 - 副本"
exit 1
fi
# 创建临时目录
TEMP_DIR="/tmp/feishu_copy_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$TEMP_DIR"
echo "📋 开始复制飞书文档..."
echo " 源文档 ID: $SOURCE_DOC_ID"
echo " 新文档标题:$NEW_DOC_TITLE"
echo " 临时目录:$TEMP_DIR"
echo ""
# 步骤 1: 获取源文档信息
echo "📊 步骤 1: 获取源文档信息..."
DOC_INFO=$(openclaw feishu fetch-doc --doc_id "$SOURCE_DOC_ID" --limit 50)
TOTAL_LENGTH=$(echo "$DOC_INFO" | jq -r '.total_length')
SOURCE_TITLE=$(echo "$DOC_INFO" | jq -r '.title')
echo " 源文档标题:$SOURCE_TITLE"
echo " 原文档总长度:$TOTAL_LENGTH 字符"
# 步骤 2: 计算分块数量
BLOCK_COUNT=$(( ($TOTAL_LENGTH + $BLOCK_SIZE - 1) / $BLOCK_SIZE ))
echo " 分块大小:$BLOCK_SIZE 字符/块"
echo " 分块数量:$BLOCK_COUNT 块"
echo ""
# 步骤 3: 分块读取
echo "📥 步骤 3: 分块读取源文档..."
OFFSET=0
BLOCK_NUM=0
while [ $OFFSET -lt $TOTAL_LENGTH ]; do
echo " 读取块 $BLOCK_NUM(offset: $OFFSET)..."
openclaw feishu fetch-doc \
--doc_id "$SOURCE_DOC_ID" \
--limit $BLOCK_SIZE \
--offset $OFFSET \
> "$TEMP_DIR/chunk_${BLOCK_NUM}.json"
# 提取 markdown 内容
cat "$TEMP_DIR/chunk_${BLOCK_NUM}.json" | jq -r '.markdown' > "$TEMP_DIR/chunk_${BLOCK_NUM}.md"
# 检查是否还有更多
HAS_MORE=$(cat "$TEMP_DIR/chunk_${BLOCK_NUM}.json" | jq -r '.has_more')
if [ "$HAS_MORE" = "false" ]; then
echo " ✅ 最后一块读取完成"
break
fi
OFFSET=$(( $OFFSET + $BLOCK_SIZE ))
BLOCK_NUM=$(( $BLOCK_NUM + 1 ))
sleep 0.5
done
TOTAL_BLOCKS=$BLOCK_NUM
echo " ✅ 共读取 $(( $TOTAL_BLOCKS + 1 )) 块"
echo ""
# 步骤 4: 创建新文档
echo "📝 步骤 4: 创建新文档..."
NEW_DOC=$(openclaw feishu create-doc \
--title "$NEW_DOC_TITLE" \
--markdown "# $NEW_DOC_TITLE\n\n正在搬运中...")
NEW_DOC_ID=$(echo "$NEW_DOC" | jq -r '.doc_id')
NEW_DOC_URL=$(echo "$NEW_DOC" | jq -r '.doc_url')
echo " 新文档 ID: $NEW_DOC_ID"
echo " 新文档链接:$NEW_DOC_URL"
echo ""
# 步骤 5: 分块写入
echo "📤 步骤 5: 分块写入新文档..."
for i in $(seq 0 $TOTAL_BLOCKS); do
CHUNK_FILE="$TEMP_DIR/chunk_${i}.md"
if [ $i -eq 0 ]; then
MODE="overwrite"
else
MODE="append"
fi
echo " 写入块 $i($MODE 模式)..."
openclaw feishu update-doc \
--doc_id "$NEW_DOC_ID" \
--mode $MODE \
--markdown @"$CHUNK_FILE"
sleep 1
done
echo " ✅ 所有块写入完成"
echo ""
# 步骤 6: 验证
echo "🔍 步骤 6: 验证结果..."
NEW_DOC_INFO=$(openclaw feishu fetch-doc --doc_id "$NEW_DOC_ID" --limit 50)
NEW_LENGTH=$(echo "$NEW_DOC_INFO" | jq -r '.total_length')
DIFF=$(( $TOTAL_LENGTH - $NEW_LENGTH ))
PERCENT=$(echo "scale=2; $NEW_LENGTH * 100 / $TOTAL_LENGTH" | bc)
echo " 原文档:$TOTAL_LENGTH 字符"
echo " 新文档:$NEW_LENGTH 字符"
echo " 差异:$DIFF 字符($PERCENT% 完整)"
if [ $(echo "$PERCENT >= 98" | bc) -eq 1 ]; then
echo " ✅ 验证通过(差异 < 2%,主要是图片 token)"
VERIFICATION_STATUS="✅ 通过"
else
echo " ⚠️ 差异较大,请手动检查"
VERIFICATION_STATUS="⚠️ 需检查"
fi
echo ""
# 步骤 7: 清理
echo "🧹 步骤 7: 清理临时文件..."
rm -rf "$TEMP_DIR"
echo " ✅ 临时文件已清理"
echo ""
# 输出结果
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🎉 复制完成!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo " 源文档:$SOURCE_TITLE"
echo " 新文档:$NEW_DOC_TITLE"
echo " 新文档链接:$NEW_DOC_URL"
echo " 完整性:$VERIFICATION_STATUS"
echo " 原文档:$TOTAL_LENGTH 字符"
echo " 新文档:$NEW_LENGTH 字符"
echo " 差异:$DIFF 字符"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
使用方法
方法 A:使用自动化脚本
# 1. 保存脚本
# 将上面的完整脚本保存为 ~/.openclaw/scripts/feishu-doc-copy.sh
# 2. 添加执行权限
chmod +x ~/.openclaw/scripts/feishu-doc-copy.sh
# 3. 执行
~/.openclaw/scripts/feishu-doc-copy.sh <源文档 ID> <新文档标题>
# 示例
~/.openclaw/scripts/feishu-doc-copy.sh ABC123xyz 文档标题 - 副本
方法 B:手动执行(适合调试)
# 1. 获取源文档信息
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 50
# 2. 创建临时目录
mkdir -p /tmp/feishu_copy_$$
# 3. 分块读取(假设原文档 43548 字符)
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 9000 --offset 0 > /tmp/feishu_copy_$$/chunk_0.json
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 9000 --offset 9000 > /tmp/feishu_copy_$$/chunk_1.json
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 9000 --offset 18000 > /tmp/feishu_copy_$$/chunk_2.json
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 9000 --offset 27000 > /tmp/feishu_copy_$$/chunk_3.json
openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 9000 --offset 36000 > /tmp/feishu_copy_$$/chunk_4.json
# 4. 提取 markdown
for f in /tmp/feishu_copy_$$/chunk_*.json; do
cat "$f" | jq -r '.markdown' > "${f%.json}.md"
done
# 5. 创建新文档
openclaw feishu create-doc --title "<新文档标题>" --markdown "# <新文档标题>\n\n正在搬运中..."
# 6. 分块写入
openclaw feishu update-doc --doc_id <NEW_DOC_ID> --mode overwrite --markdown @/tmp/feishu_copy_$$/chunk_0.md
openclaw feishu update-doc --doc_id <NEW_DOC_ID> --mode append --markdown @/tmp/feishu_copy_$$/chunk_1.md
openclaw feishu update-doc --doc_id <NEW_DOC_ID> --mode append --markdown @/tmp/feishu_copy_$$/chunk_2.md
openclaw feishu update-doc --doc_id <NEW_DOC_ID> --mode append --markdown @/tmp/feishu_copy_$$/chunk_3.md
openclaw feishu update-doc --doc_id <NEW_DOC_ID> --mode append --markdown @/tmp/feishu_copy_$$/chunk_4.md
# 7. 验证
openclaw feishu fetch-doc --doc_id <NEW_DOC_ID> --limit 50
验证清单
复制完成后,请检查以下项目:
必检项目
- 字数验证:新文档字数 / 原文档字数 ≥ 98%
- 目录完整:所有 ## 章节标题都存在
- 表格完整:所有
<lark-table>标签完整 - 代码块完整:所有 ``` 代码块完整
- 结语完整:文档结尾没有截断
选检项目
- callout 格式正确
- 列表格式正确
- 粗体/斜体完整
- 链接完整
常见问题
Q1: 为什么新文档字数比原文档少?
A: 正常现象,主要原因:
- 图片 token 占位符差异(每张约 100-150 字符)
- callout 内部换行被压缩(不影响渲染)
- 多余空白字符被清理
只要差异 < 2%,文档内容就是完整的。
Q2: 表格显示不正常怎么办?
A: 检查 <lark-table> 标签是否完整:
- 必须有
rows、cols属性 - 所有
<lark-tr>和<lark-td>标签必须配对 - 分块时没有在表格中间切断
Q3: 如何获取文档 ID?
A: 从飞书文档 URL 中提取:
URL: https://xxx.feishu.cn/wiki/DOC_ID_HERE
↑
文档 ID
文档 ID 是 URL 最后一段字符串。
Q4: 脚本执行失败怎么办?
A: 排查步骤:
- 检查是否有源文档读取权限(能打开查看)
- 检查
openclaw feishu命令是否可用 - 检查临时目录是否有写入权限
- 查看错误输出,定位失败的步骤
Q5: 如何断点续传?
A: 如果写入中途失败:
- 不要删除临时目录
- 记录最后成功写入的块编号
- 从下一个块继续执行写入循环
- 完成后正常验证和清理
技术细节
分块大小选择
| 分块大小 | 优点 | 缺点 | 推荐场景 |
|---|---|---|---|
| 5,000 | 不易切断表格/代码 | API 调用次数多 | 复杂文档 |
| 9,000 | 平衡点 | - | 推荐 |
| 15,000 | API 调用次数少 | 可能超过限制 | 简单文档 |
API 限流处理
- 读取间隔:0.5 秒
- 写入间隔:1 秒
- 如遇 429 错误:增加间隔时间
错误处理
脚本使用 set -e,遇到错误立即退出。临时文件保留,便于调试。
版本历史
- v1.0 (2026-03-14): 初始版本
- 分块读取 + 分块写入核心方法
- 完整自动化脚本
- 验证流程
许可证
MIT License
本技能基于 OpenClaw 飞书 API 实现,适用于所有有查看权限但无复制权限的飞书文档。