ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

飞书批量上传Markdown全攻略:API、图片处理与断点续跑

飞书批量上传Markdown全攻略:API、图片处理与断点续跑 手里攒了几百个 markdown 文件想把它们整批搬进飞书云文档这个需求听起来特别简单——不就是上传吗。真动手才知道飞书批量上传 markdown 这件事的难点从来不在上传两个字上而在于图片路径怎么办、目录层级怎么还原、中文文件名会不会出问题、跑到第 187 个文件突然报错之后要不要从头再来。这篇就把我自己做过两轮、累计导入了七百多篇 markdown 的完整链条摊开讲从选型一直讲到断点续跑和事后维护。适合三类人看手上有一整个文档仓库要迁移的、想给团队搭本地写作 云端发布流水线的、以及单纯想摸清飞书云文档接口脾气的开发者。1. 批量上传之前先把路线这件事定下来1.1 三条能走通的路以及它们各自的天花板把 markdown 弄进飞书业界常见的做法就三条没有第四条。第一条是纯手动。飞书客户端和网页端都有导入文档的入口支持选本地文件甚至支持一次选多个。文件少的时候这条最快十分钟能搞定五十篇。但它的天花板很明显一次能选的数量有限导入后的文档会散落在默认位置你要一个个手动拖进对应文件夹更麻烦的是文件名即标题如果你的文件名是01-intro.md这种带序号的导入进去的文档标题就是01-intro还得一篇篇改。文件超过一百个之后这条路的时间成本会迅速失控。第二条是浏览器自动化。用脚本驱动浏览器模拟点击导入按钮、选文件、等待完成。这条路的好处是完全不依赖开放平台权限个人账号就能跑不需要企业管理员给你开应用。坏处是脆弱到离谱——前端 DOM 一改脚本全废上传大文件时页面加载慢等待时间全靠猜而且飞书前端对这种高频模拟操作是有风控的跑着跑着弹个验证就停住了。我第一轮就是用这条路跑到六十多篇开始随机失败最后放弃。第三条是开放平台的云文档接口。核心链路是先把 markdown 当成素材上传拿到一个file_token再用这个 token 创建一个导入任务飞书在后台把素材转成云文档你轮询任务状态成功后拿到新文档的 token 和 URL。这条路前期配置麻烦——要建应用、开权限、把应用加进文件夹但一旦跑通稳定性和可扩展性完全不是一个量级。路线前期成本百篇以上可行性可重复执行图片处理手动导入极低差不可自动抓外链浏览器自动化中差差自动抓外链开放平台接口高好好需自己先处理1.2 为什么最后都收敛到素材上传 导入任务有人在第一次看飞书云文档接口文档时会困惑为什么不能直接传一个 markdown 文件让它变成一篇文档为什么非要先上传素材、再创建任务、再轮询这不是绕远路吗。关键在于转换是异步的。markdown 到飞书文档不是简单的格式映射中间要做语法树解析、样式重建、图片抓取、表格重排复杂文档可能要好几秒。如果接口设计成同步阻塞一个请求挂十几秒双方都不好受。所以飞书把它拆成了接收素材和执行转换两段中间用 task ticket 串起来。理解了这一点后面所有的为什么要轮询为什么会有 pending 状态就都顺了。这个设计还有个隐藏好处素材和任务分离意味着你可以在任务失败后只重试任务创建不用重新上传素材。跑大批量的时候这一条能省掉大量重复流量。1.3 三个不解决就没法开工的硬约束在写第一行代码之前有三件事必须先确认否则跑到一半一定卡住。应用类型。你用的应该是企业自建应用因为它可以用tenant_access_token以应用身份调用不依赖任何人的登录态。用user_access_token也不是不行但那个 token 有效期短、需要走授权码流程不适合跑批处理脚本。目标文件夹。导入任务必须指定一个挂载点也就是一个云空间文件夹。这个文件夹可以是根目录也可以是某个子目录。但注意它必须是应用有权限写入的文件夹这一条后面会专门讲。图片的归宿。这是最容易低估的一项。markdown 里的图片如果是相对路径导入后一定是断的。你必须提前决定是先用对象存储把图片传上去拿公网 URL还是干脆接受图片丢失。这个决定会直接改变你预处理脚本的复杂度。2. 应用凭证与云空间权限403 几乎都出在这一层2.1 tenant_access_token 的获取、缓存与过期所有接口调用的第一步都是拿 token。请求很直接POST 到/open-apis/auth/v3/tenant_access_token/internalbody 里带app_id和app_secret返回里除了 token 还有一个expire字段单位是秒一般是 7200。这里有两个容易犯的错。第一个是每次请求都重新拿 token。单文件测试的时候无所谓跑三百个文件就是三百次额外的鉴权请求直接把你的配额浪费掉一大半而且鉴权接口本身也有频控。正确做法是在内存里缓存快到期前再刷新。第二个是把过期时间当成绝对安全线。expire是 7200 秒但你不能等到 7199 秒才刷新中间的网络延迟、时钟漂移都可能让你在边界上被拒。我习惯减掉 300 秒也就是提前五分钟刷新。import os, time, requests BASE https://open.feishu.cn/open-apis def fetch_token(): r requests.post( f{BASE}/auth/v3/tenant_access_token/internal, json{ app_id: os.environ[FS_APP_ID], app_secret: os.environ[FS_APP_SECRET], }, timeout10, ) data r.json() if data.get(code) ! 0: raise RuntimeError(f取 token 失败: {data}) # 提前 300 秒过期避开边界 return data[tenant_access_token], time.time() data[expire] - 300注意app_secret绝对不要硬编码进仓库。用环境变量或者本地配置文件并且把配置文件加进.gitignore。这套凭证等于你整个企业租户里这个应用的全部权限。2.2 权限组怎么勾才算够飞书开放平台的后台权限是按权限组来勾的不是按接口。云文档相关的至少需要这几项查看、评论、编辑和管理云空间中的所有文件对应drive:drive这一类这是核心不勾什么都做不了。上传图片、文件等素材素材上传接口需要。如果后续还要建文件夹文件夹创建接口同样归在云空间权限下。很多人勾完权限就以为完事了但飞书的权限体系有个特点权限勾选后需要发布版本才生效。自建应用改权限后要在后台点创建版本并等待审核通过企业内部应用一般是管理员秒批。如果你勾完权限立刻跑脚本报 403八成是版本还没发布。这个坑我踩过盯着代码看了半小时最后发现是后台一个按钮没点。另外一个细节权限是能做什么但能不能操作某个具体文件是另一套逻辑也就是下一节要说的协作者机制。2.3 把应用请进目标文件夹这是整个项目里最容易漏、也最让人抓狂的一步。应用拿了tenant_access_token有了云空间权限但这不代表它能往你的文件夹里写东西。飞书的云空间权限是按文件/文件夹粒度授予的应用默认只对自己创建的资源有权限。你要把目标文件夹共享给这个应用。具体做法在飞书里找到你要作为导入挂载点的那个文件夹点分享在协作者里搜索你的应用名称不是你的用户名是应用本身把它加为可编辑。应用名称就是你在开放平台注册时起的名字。加完之后再跑脚本403 立刻消失。我在第一次做这个项目时一直以为是权限组没开反复检查后台配置其实根因就是文件夹没共享。提示如果你要导入的目录树很深别指望在根目录共享一次就够。飞书云空间的协作者权限不自动继承到你新建的子文件夹——但实际上子文件夹是应用自己创建的创建者对自建资源天然有权限所以只需要把最顶层那个起始文件夹共享给应用即可往下都由应用自己建链路是通的。2.4 一个判断权限问题的通用排查顺序遇到权限类报错时按这个顺序查基本能定位报错是什么 code403 是权限不足401 是 token 无效404 通常是资源不存在或者你没权限看不见它飞书对无权限资源的返回经常伪装成 404。token 是不是过期了打印一下剩余有效期。权限组勾了吗版本发布了吗目标文件夹共享给应用了吗这个文件夹是不是在别人的个人空间里个人空间的资源共享规则和企业空间不一样容易出幺蛾子。把这份清单存下来能省下大量瞎猜的时间。3. 导入前的 markdown 体检图片、frontmatter 和会变形的语法3.1 本地图片必须先变成公网可访问的链接这是整条链路里最脏的活没有之一。飞书的导入器在处理![](https://example.com/a.png)这类外链图片时会主动去抓取并把图片转存到自己的云空间最终文档里的图片是稳定的。但如果你的 markdown 写的是![](./images/a.png)或者![](/static/img/a.png)导入器抓不到结果就是一张断图或者干脆什么都没有。所以预处理的核心任务就一句话把所有本地图片上传到一个公网可访问的地方然后把 markdown 里的相对路径替换成对应的 URL。具体上传到哪儿看你现有的基础设施图片去处优点缺点已有对象存储S3 兼容等量大不花钱路径可控需要 bucket 公读或签名 URL代码托管平台的 raw 链接零成本和仓库同源有频率限制私有仓库不可用图床服务接入快有额度上限长期稳定性看运气反向代理到自己的域名最可控需要配置和维护对个人项目来说如果这个 markdown 仓库本来就托管在公开仓库里直接用它提供的 raw 链接最省事。我常用的一种做法是先把图片统一 push 到仓库的assets/目录然后用仓库的 raw URL 作为前缀做字符串替换。全程不涉及额外服务脚本也就十几行。如果图片太多或者仓库是私有的那就老老实实走对象存储。上传脚本用boto3之类的 SDK 批量传记得控制并发别一次性开五百个线程。3.2 用正则批量改写图片路径时踩到的坑看起来re.sub(r!\[.*?\]\((.*?)\), ...)就完事了实际上有三个坑。坑一是 URL 里带括号。很多图片 URL 会带(1).png这种或者 CDN 的链接里有括号参数。非贪婪匹配到第一个)就停了剩下半截留在正文里。稳妥一点的写法是同时处理带标题的语法import re # 匹配 ![alt](src) 和 ![alt](src title) 两种形式 IMG_RE re.compile(r!\[([^\]]*)\]\(\s*([^)\s])(?:\s[^]*)?\s*\)) def rewrite_images(text, mapper): def repl(m): alt, src m.group(1), m.group(2) if src.startswith((http://, https://, data:)): return m.group(0) # 已经是外链不动 return f![{alt}]({mapper(src)}) return IMG_RE.sub(repl, text)坑二是引用式图片。markdown 还支持![alt][ref]加底部[ref]: ./a.png的定义式写法。上面的正则完全覆盖不到。如果是自己写的文档可以要求团队统一用行内式如果是接手别人的仓库就得额外写一段处理引用定义的逻辑或者干脆用 markdown 解析库来做比如 Python 的markdown-it-py、Node 的unifiedremark。我个人建议批量迁移场景直接用解析库别跟正则较劲正则省下的那点时间最后都会还回去。坑三是 HTML 图片标签。有些文档里写的是img src./a.png /。这个也得单独处理而且因为它混在正文里解析库大概率会把它当成原始 HTML 保留下来需要在解析之后再扫一遍。3.3 frontmatter 与站点专用标记的剥离如果你迁移的是静态站点或者笔记软件导出的 markdown文件开头大概率有 YAML frontmatter--- title: 快速开始 date: 2024-03-11 tags: [guide, setup] ---导入飞书之后这段---包裹的内容不会被识别成元数据而是当成普通正文渲染出来一篇原本干净的文档开头就多了几行莫名其妙的键值对。必须在预处理阶段剥掉。FM_RE re.compile(r^---\s*\n.*?\n---\s*\n, re.DOTALL) def strip_frontmatter(text): return FM_RE.sub(, text, count1)顺带要处理的还有几类站点专用标记!-- more --这种摘要分隔符、VuePress / Docusaurus 的自定义容器语法::: tip、{{ }}模板变量、以及{% include %}之类的标签。这些进去之后全是噪声。我的做法是列一份要删的模式清单在预处理里统一过一遍比导入之后再去文档里改省事一百倍。3.4 表格、公式、mermaid、软换行的兼容性清单markdown 到飞书文档不是一一对应的下面这张表是我实测下来比较稳的结论markdown 语法导入后表现建议标题#~######映射为文档标题层级基本正常直接用段落间单换行多数情况被合并成一段想断段就空一行行尾两空格硬换行表现不稳定时灵时不灵改用空行或brpipe 表格基础表格可用合并单元格丢失宽表提前拆列代码块 语言标注保留为代码块高亮基本在直接用mermaid 代码块变成普通代码块不渲染提前导出成图片数学公式$...$/$$...$$支持程度视导入器版本而定复杂公式转图片最保险本地图片相对路径断图换成公网 URL锚点链接#某标题失效导入后手动补内嵌 HTML 标签大多被丢弃或转义不要依赖它排版重点说两个。mermaid是重灾区很多技术文档里都有流程图导入飞书后会退化成一堆代码文本读者体验很差。如果图不多可以提前用命令行工具把 mermaid 渲染成 png然后把代码块替换成图片引用。软换行则是所有从 markdown 迁移到富文本的人的共同记忆——你以为写完一行敲个回车就是换行实际上 markdown 规范里单换行是空格必须空一行才是新段落。这个认知差异导致的排版事故我在每个迁移项目里都会遇到一次。4. 单文件链路跑通从素材上传到导入任务的四个动作4.1 素材上传接口里的 parent_type 与 size先看上传接口。它是一个 multipart 表单字段包括file_name文件名带扩展名。parent_type素材的用途类型。这一步极关键用于文档导入的场景要传ccm_import_open传成别的话后面创建导入任务时会提示 token 不可用。parent_node父节点 token。导入场景一般留空因为素材还没有归属。size文件字节数必须和实际大小一致不一致会被拒。file二进制内容。返回里最有用的是file_token形如boxcn开头的一串。它只在有限时间内有效所以上传完应该尽快创建导入任务不要攒着。def upload_material(client, path): name os.path.basename(path) size os.path.getsize(path) with open(path, rb) as f: r requests.post( f{BASE}/drive/v1/medias/upload_all, headersclient.headers(), data{ file_name: name, parent_type: ccm_import_open, parent_node: , size: str(size), }, files{file: (name, f, text/markdown)}, timeout60, ) data r.json() if data.get(code) ! 0: raise RuntimeError(f上传失败 {name}: {data}) return data[data][file_token]注意size用的是字节数不是字符数。中文文件名和中文内容都按 UTF-8 字节算用os.path.getsize拿的是磁盘上的真实大小一般没问题但如果文件被做过换行符转换CRLF ↔ LF大小会变。4.2 创建导入任务file_extension、type 和 point拿到file_token之后POST 到/open-apis/drive/v1/import_tasks{ file_extension: md, file_token: boxcnxxxxxxxx, type: docx, file_name: 快速开始, point: { mount_type: 1, mount_key: fldcnxxxxxxxx } }四个参数的语义分别是file_extension源文件的扩展名markdown 就是md。这个值决定了飞书用哪个解析器处理你的素材。具体支持的枚举以当前接口文档为准不同时期会有增减。file_token上一步上传得到的 token。type目标文档类型。markdown 转成新版的云文档就是docx如果你想转成表格、多维表格那是给 csv、xlsx 用的markdown 走不通。file_name目标文档的标题和源文件名可以不一样。这是解决文件名带序号很难看问题的关键——你可以在这一步把01-intro.md改写成第一章 快速开始。point挂载点。mount_type为 1 表示挂载到云空间文件夹mount_key就是那个文件夹的 token。成功返回一个ticket这就是你后面轮询用的凭据。4.3 轮询结果与失败判定的边界查询接口是 GET/open-apis/drive/v1/import_tasks/{ticket}返回体里的result对象包含status、token、url等字段。status的语义大致是一个表示成功若干个中间态表示还在初始化或处理中其余表示失败。这里最危险的做法是把非成功一律当失败——文档刚创建时一定是中间态你立刻判失败重试就会产生一大堆重复文档。正确逻辑是中间态就 sleep 一下再来成功就收工其他值才抛异常。轮询节奏我一般用 2 秒一次。导入一篇普通 markdown 通常 2 到 6 秒完成复杂的十几秒。超时阈值给 120 秒足够超过基本是真出问题了。def wait_import(client, ticket, timeout120, interval2): deadline time.time() timeout while time.time() deadline: r requests.get( f{BASE}/drive/v1/import_tasks/{ticket}, headersclient.headers(), timeout15, ) data r.json() if data.get(code) ! 0: raise RuntimeError(f查询失败: {data}) result data[data][result] status result.get(status) if status 0: # 成功 return result if status in (1, 2): # 初始化 / 处理中 time.sleep(interval) continue raise RuntimeError(f导入失败 status{status}: {result}) raise TimeoutError(f导入超时: {ticket})提示status的枚举值以接口文档为准不同接口版本可能调整。稳妥起见把中间态写成一个集合常量改的时候只改一处。4.4 单文件跑通意味着什么把上面三个函数串起来一个 markdown 文件的完整生命周期就跑通了读文件、剥 frontmatter、替换图片路径得到处理后的临时文件。上传素材拿file_token。创建导入任务拿ticket。轮询直到成功拿新文档的token和url。先别急着批量。拿三个不同类型的文件测一个纯文字无图的、一个带十几张图的、一个表格特别宽的。这三个跑通说明你的预处理和接口调用都没问题再考虑规模化。跳过这一步直接上三百个文件出错的时候你连是哪一层的问题都不知道。5. 目录树还原与断点续跑把几百个文件变成几百篇云文档5.1 先建文件夹再往里灌文档飞书的导入任务只能指定一个已有的文件夹作为挂载点它不会帮你创建目录。所以要想还原本地目录结构你得自己先建文件夹。建文件夹接口是 POST/open-apis/drive/v1/files/create_folder参数是name和folder_token父文件夹返回新文件夹的 token。于是流程变成两阶段第一阶段遍历本地目录把每一级目录在云端建出来。用os.walk自上而下遍历维护一个本地相对路径 - 云端 folder_token的映射。根目录映射到你一开始共享给应用的那个起始文件夹。每遇到一个新目录用它的父目录的云端 token 作为folder_token创建把结果塞进映射。第二阶段遍历所有 markdown 文件找到它所在目录对应的云端 folder_token走导入链路。def build_folder_tree(client, local_root, cloud_root): mapping {: cloud_root} for dirpath, dirnames, _ in os.walk(local_root): dirnames.sort() # 保证顺序稳定 rel os.path.relpath(dirpath, local_root) rel if rel . else rel parent_rel os.path.dirname(rel) parent_token mapping[parent_rel] for d in dirnames: child_rel os.path.join(rel, d) if rel else d token client.create_folder(d, parent_token) mapping[child_rel] token return mappingdirnames.sort()这行看着多余其实很重要。os.walk的遍历顺序依赖文件系统不排序的话两次运行顺序可能不一样做断点续跑对比的时候会很痛苦。5.2 folder_token 缓存与重复文件夹问题上面的写法有个隐患如果脚本跑到一半挂了你重跑它会对已经建好的目录再建一遍云端就出现一堆同名文件夹。解决办法是把mapping持久化到本地跑之前先加载跑完之后再写回。更进一步可以在创建之前先列一下父文件夹下已有子文件夹按名字匹配复用。列文件夹的接口在云空间文件管理那一组里可以按folder_token列出子项。我的选择是前者——本地维护一个 JSON 缓存简单可靠。因为这套脚本的定位是一次性迁移工具不是长期运行的同步服务用不着做实时比对。缓存文件长这样{ folders: { : fldcnROOTxxxx, guide: fldcnAxxxx, guide/advanced: fldcnBxxxx }, files: { guide/intro.md: { status: done, doc_token: doxcnxxxx, url: https://example.feishu.cn/docx/doxcnxxxx } } }有了这个文件断点续跑就变成了跳过 status 已经是 done 的条目。5.3 清单文件与失败重跑大批量运行一定会有一部分失败原因五花八门图片 URL 挂了、文件太大、网络抖动、频控被拒。关键不是不失败而是失败之后能精准重跑。我的做法是每次运行都写两份记录state.json完整的处理状态用于断点续跑。failed.txt本次运行失败的文件相对路径列表一行一个。重跑的时候给脚本加个--only-failed开关只处理failed.txt里的条目。有了这个处理三百个文件时哪怕是第 200 个挂了也不需要从头再来。另外强烈建议每个文件处理完就立刻落盘一次状态而不是等全部跑完再统一写。虽然写 JSON 有点 IO 开销但相对于网络请求的耗时可以忽略不计。崩一次从头再来才是真的浪费时间。6. 真正跑起来之后才暴露的问题6.1 频率限制、并发与越跑越慢单线程串行跑一个文件从上传到导入完成大约 5 到 10 秒三百个文件就是半小时到一小时。这个速度能接受但你会忍不住想上并发。上并发之前先想清楚飞书对素材上传和导入任务都有频控。官方文档里会标注每个接口的 QPS 上限实际体感是单个应用每秒两三个请求比较安全超过就开始零星返回限流错误。如果你把并发开到 10看着快实际上大量请求被拒后重试总耗时反而更长。这就是越跑越慢的典型症状。我的经验值并发度控制在 3 到 5并且在客户端做一层简单的令牌桶把 QPS 压在 3 以内。这个配置下三百个文件大概十几分钟跑完且几乎没有限流报错。超时和重试也要设计好。网络类错误连接超时、5xx适合指数退避重试三次为上限业务类错误参数不对、权限不足重试没意义直接记进失败清单。混在一起无脑重试只会让日志变成一锅粥。6.2 幂等同一次运行跑两遍会发生什么导入接口不是幂等的。同一个file_token创建两次任务会生成两篇云文档。同一个 markdown 文件重新上传再导入更是必然生成新文档。所以批量脚本必须自己保证幂等靠的就是上一节的state.json。每次处理前先查状态已完成的直接跳过。这一点在调试阶段尤其重要——你改了个预处理规则想看看效果结果把整个仓库重导了一遍云端瞬间多出一倍文档清理起来非常痛苦。调试期我的建议是先在一个独立的测试文件夹里跑小样本确认效果满意再动正式的。云端删文档虽然也能通过接口做但批量删除的麻烦程度不比批量上传低。6.3 大文件、超时和重试策略素材上传对文件大小是有限制的具体上限以文档为准印象中在几十 MB 这个量级。markdown 文件本身很难超过这个数但如果你的 markdown 里嵌了 base64 图片文件会瞬间膨胀——一张 200KB 的图转成 base64 大约 270KB几十张就上兆了。所以第一件要检查的事是正文里有没有data:image/png;base64,这种内联图片。如果有导入前一定要抽出来变成文件再上传到外链否则文件体积不可控飞书的转换也可能出问题。另一个超时重灾区是单篇文档特别长的情况。一篇两万字的 markdown飞书转换可能超过 60 秒。轮询的超时阈值要按最长的那篇来设别用固定值。我一般按文件大小动态算小于 100KB 给 90 秒更大的给 180 秒。6.4 导入完成后图片和附件的去向这是导入成功后最常见的一个疑问飞书把外链图片抓走之后存在哪儿了答案是存在云空间里和你的文档关联但不占你指定的那个目标文件夹的位置。文档里引用的图片由飞书自己管理你在文件夹里看不到散落的图片文件这是好事。但要注意两点一是如果外链图片后续失效了飞书里的图不受影响因为已经转存了。这也是为什么必须用可访问的链接——抓取是一次性的抓不到就永远没有。二是附件非图片的[下载](./file.zip)链接不会被自动抓取。飞书只处理图片其他类型的文件链接会原样保留成文本链接指向你原来的路径。如果那些路径在公网上不可达团队同事点开就是 404。附件要么单独上传到云空间再补链接要么在迁移说明里写清楚。7. 导入之后索引表与后续维护7.1 用多维表格给这批文档建索引文档进云空间之后是一盘散沙尤其当你有几百篇的时候找起来非常痛苦。我的做法是顺手生成一张多维表格索引一行对应一篇文档字段包括标题、原文件路径、云文档链接、导入时间、所属模块、状态。多维表格的写入接口和云文档是一套凭证体系可以循环调用把state.json里的记录灌进去。跑完之后你拿到的不仅是一张清单还是一个可以按模块筛选、按时间排序的导航页。如果导入时你在file_name里保留了模块前缀比如统一格式化成模块名 - 文档标题索引表会更整齐。这一点最好在写预处理脚本时就规划好事后补很麻烦。7.2 让脚本可以反复用而不是一次性消耗品迁移完成之后这套脚本大概率不会立刻退休。团队还在用 markdown 写文档你希望新文档也能一键上云或者每周同步一次。要做到这一点有几件事值得提前做把配置抽成文件。根文件夹 token、图片 CDN 前缀、需要剥离的 frontmatter 字段全部放到一个config.yaml里别散在代码里。把转换和上传分开。预处理产出一份处理后的临时文件目录上传阶段只负责传。这样调整预处理规则时不用重跑上传调试效率高得多。给文件名做规范化。用正则把所有非法字符/ \ : * ? |替换成中划线或下划线。云文档标题对字符的限制比本地文件系统宽松但保留这些字符仍可能在 URL 拼接时出问题。记录每个文件的源文件哈希。有了哈希第二次运行就能判断哪些文件真的改了只同步变化的部分。这就是从一次性迁移工具变成持续同步工具的关键一跳。7.3 我个人的一点体会整套流程拆开看没有一个环节是特别难的。建应用、拿 token、传素材、建任务、轮询——每一步都是标准的 API 调用。真正消耗时间的是那些不在文档正文里的细节权限发布会生效、文件夹要单独共享、图片必须先变成外链、frontmatter 会污染正文、行尾两个空格的换行不生效。这些细节没法靠读文档提前知道只能靠跑一遍、看到不对劲、再回头查。所以如果你正准备做这件事我的建议是先拿十个各具特色的文件有图、有表、有公式、有 frontmatter、有长文档跑一轮完整流程把问题全暴露出来再动手写批处理框架。用小样本试错成本是可控的用三百个文件试错成本是几个小时的等待加一轮清理。另外把这套东西的中间状态设计好比把流程写得多优雅重要得多。一个能断点续跑、能只看失败清单、能重复执行不产生脏数据的土脚本价值远高于一个跑一次就报废的漂亮脚本。
返回列表