Skill: keson1521/feishu-doc-copy (version 1.0.1)

# feishu-doc-copy - 飞书文档完整复制技能 ## 技能描述 **功能**：完整复制飞书云文档到同一账号的云空间，保持文字、表格、代码、格式 100% 一致（图片除外）。 **适用场景**： - 有文档查看权限但无复制/下载权限 - 需要备份飞书文档 - 需要创建文档的完整副本 - 长文档（>10,000 字符）的完整复制 **前提条件**： - 对源文档有读取权限（能打开查看） - 有自己的云空间写入权限 - 已安装 OpenClaw 并配置飞书渠道 **限制**： - 图片无法复制（飞书 API token 权限限制） - 单次复制最大支持 100,000 字符 --- ## 核心方法 **分块读取 + 分块写入 + 严格验证** 这是唯一可行的路径，因为： 1. 飞书文档 API 单次读取有长度限制（limit 参数） 2. 飞书文档编辑 API 单次写入有长度限制 3. 文档无复制权限时，无法使用客户端复制粘贴 4. 必须通过 API 分块搬运 --- ## 完整实现流程 ### 步骤 1：获取源文档信息 ```bash # 读取源文档前 50 字符，获取 total_length openclaw feishu fetch-doc --doc_id <SOURCE_DOC_ID> --limit 50 ``` **返回值示例**： ```json { "doc_id": "ABC123xyz", "title": "文档标题", "total_length": 43548, "has_more": true, "length": 50, "markdown": "..." } ``` **关键信息**： - `total_length`：原文档总字符数（用于计算分块数量） - `doc_id`：源文档 ID --- ### 步骤 2：计算分块策略 ```bash # 每块大小：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：创建临时目录 ```bash # 创建临时目录存储分块文件 TEMP_DIR="/tmp/feishu_copy_$(date +%Y%m%d_%H%M%S)" mkdir -p "$TEMP_DIR" ``` --- ### 步骤 4：分块读取源文档 **循环读取每一块**： ```bash 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 ``` **关键验证点**： 1. 每次读取后检查 `has_more` 标志 2. 最后一块 `has_more` 必须为 `false` 3. 每块文件必须成功保存 --- ### 步骤 5：创建新文档 ```bash # 创建新文档（初始内容可以是任意占位符） openclaw feishu create-doc \ --title "<新文档标题>" \ --markdown "# <新文档标题>\n\n正在搬运中..." ``` **返回值示例**： ```json { "doc_id": "XYZ789abc", "doc_url": "https://www.feishu.cn/docx/XYZ789abc", "message": "文档创建成功" } ``` **记录新文档 ID**：`NEW_DOC_ID` --- ### 步骤 6：分块写入新文档 **写入规则**： - 第 1 块（块 0）：使用 `overwrite` 模式 - 后续块（块 1-N）：使用 `append` 模式 ```bash 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. 第 1 块必须用 `overwrite` 2. 后续块必须用 `append` 3. 每次写入后检查返回状态 4. 写入间隔至少 1 秒（避免限流） --- ### 步骤 7：验证结果 ```bash # 获取新文档信息 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：清理临时文件 ```bash rm -rf "$TEMP_DIR" ``` --- ## 完整自动化脚本 将以上流程整合为可执行脚本： ```bash #!/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：使用自动化脚本 ```bash # 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：手动执行（适合调试） ```bash # 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**: 正常现象，主要原因： 1. 图片 token 占位符差异（每张约 100-150 字符） 2. callout 内部换行被压缩（不影响渲染） 3. 多余空白字符被清理 只要差异 < 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**: 排查步骤： 1. 检查是否有源文档读取权限（能打开查看） 2. 检查 `openclaw feishu` 命令是否可用 3. 检查临时目录是否有写入权限 4. 查看错误输出，定位失败的步骤 ### Q5: 如何断点续传？ **A**: 如果写入中途失败： 1. 不要删除临时目录 2. 记录最后成功写入的块编号 3. 从下一个块继续执行写入循环 4. 完成后正常验证和清理 --- ## 技术细节 ### 分块大小选择 | 分块大小 | 优点 | 缺点 | 推荐场景 | |---------|------|------|---------| | 5,000 | 不易切断表格/代码 | API 调用次数多 | 复杂文档 | | 9,000 | 平衡点 | - | **推荐** | | 15,000 | API 调用次数少 | 可能超过限制 | 简单文档 | ### API 限流处理 - 读取间隔：0.5 秒 - 写入间隔：1 秒 - 如遇 429 错误：增加间隔时间 ### 错误处理 脚本使用 `set -e`，遇到错误立即退出。临时文件保留，便于调试。 --- ## 版本历史 - **v1.0** (2026-03-14): 初始版本 - 分块读取 + 分块写入核心方法 - 完整自动化脚本 - 验证流程 --- ## 许可证 MIT License --- *本技能基于 OpenClaw 飞书 API 实现，适用于所有有查看权限但无复制权限的飞书文档。*


Security scan verdicts — ClawScan: malicious | static analysis: clean | VirusTotal: clean | SkillSpector:  ()