说实话,我一开始对“文档自由”这四个字没什么感觉。直到某天我数了一下自己一天里到底干了多少件复制粘贴的活儿:把AI生成的周报从网页里粘到飞书文档,把多维表格里的数据截图贴到群里,把项目进展从聊天记录里扒出来再整理成文档。一天下来,真正用来思考的时间没多少,搬运活儿反倒占了大半。后来我索性写了个小工具,把“AI生成内容+上传飞书”这个流程压缩成一个命令:一键生成、一键上传、一键通知。这篇文章就记录一下这个工具的完整搭建过程,包括飞书API的选型、机器人Webhook的调用方式、多维表格的写入技巧,以及我在Windows上用Claude Code辅助开发时踩过的一堆坑。适合被文档搬运折磨的运营、研发、产品,以及刚上手飞书开放平台的开发者。
1. 项目整体设计与思路拆解
1.1 痛点:AI生成的内容卡在最后一公里
很多人用AI的姿势是:网页上等结果,复制,切到飞书,粘贴,调格式。这套流程看起来没什么,但一旦内容高频更新,痛点就出来了。我写过日报、周报、竞品分析、会议纪要,几乎每天都要经历三四次完整搬运。最烦的不是复制粘贴本身,而是飞书文档的排版总要手动调——标题层级、加粗、表格、代码块,AI在网页端生成得很规整,粘到飞书后全部打回原形。
更夸张的是群里的同步。文档写好了,还得把链接手动甩到群里,附带一句“这是今天的报告,大家看下”。如果哪天忘了发,就有人来问“报告呢”。这类高频率、低技术含量的操作,机械执行太浪费,所以我一开始就把它定义成一个“自动化脚本”项目:输入一个需求描述,输出一个已上传飞书且已经通知到群的文档。整个过程不需要打开浏览器,不需要复制粘贴,也不需要手动排版。
我接触到很多人的第一反应是“用飞书机器人就能发吧”。没错,机器人只是发送消息的出口,但完整的链路远不止这一环。要生成文档内容,需要接大模型;要创建飞书文档,需要走云文档API;要把文档写进知识库或合作空间,需要指定folder_token;要让群里的人直接看到,需要Webhook。把这些串起来,才叫完整方案。
1.2 整体架构:三层协作,脚本只做编排
这个项目我最终分成三层来看。
最底层是大模型API。我用的DeepSeek和通义千问的OpenAI兼容接口,因为两者在国内可直接调用、价格便宜,而且能处理结构化输出。换模型的话,只要保持messages格式不变,替换base_url和api_key就能切换。
中间层是飞书开放平台的三类接口:第一类是用tenant_access_token调用的云文档API,负责创建文档和执行导入任务;第二类是群机器人Webhook,负责把消息或卡片发到群里;第三类是多维表格API,负责把结构化记录写入表格。
最上层就是编排层,一个Python脚本。它负责读取配置、调大模型生成内容、调用飞书API上传、最后发通知。脚本本身不包含业务逻辑,逻辑全在“先做什么再做什么”这条主流程里。这样设计的理由很直接:一旦某个环节出问题,可以单独重试,不会影响其他步骤。
为什么用Python而不是Node或者Go?说实话,Python写这类胶水脚本是最快的,requests库一把梭,JSON处理也方便。而且大模型SDK对Python的支持最成熟。我身边也有人用Node做,但从零到能用,Python能少花一半时间。
1.3 方案取舍:飞书 vs 本地文件 vs 其他协作平台
做“文档自由”的时候,身边同事问过我两个问题:为什么不直接存本地?为什么不用其他协作平台?
本地文件当然是最简单的,AI生成完保存成Markdown发群里就行。但问题是,团队协作时你的本地文件等于不存在。飞书的价值在于三点:文档天然带链接,分享成本为零;权限体系可以让不同人看到不同内容;文档可以挂在知识库下,实现长期沉淀。我需要的正是“生成—传达—沉淀”闭环,本地文件完全覆盖不了。
至于钉钉和企业微信,Webhook能力和API开放程度都不差。但我个人最终选了飞书,原因有三:云文档的导入API支持Markdown,这省了我大量排版工作;多维表格对API写入的兼容度很高,常见的字段类型都能直接落库;飞书的卡片交互样式更适合做“文档已生成”这类通知。每个团队选型维度不同,但对我这个场景,飞书确实是最省事的。
这里顺便说明一个选型教训:开始不要追求大而全的Agent框架,先做一条“生成—上传—通知”的最小闭环,跑通之后再加事件订阅、多机器人协作这些能力。这个项目的前身就是一堆散落的代码片段,后来才整理成结构化脚本,经验就是——先有闭环,再谈架构。
2. 核心细节解析与实操要点
2.1 飞书文档导入API:一条命令把Markdown变成在线文档
很多第一次接触飞书开放平台的人会以为创建文档得用docx接口,然后一块一块地建block。我一开始也这么想,看到block文档时头皮发麻——一篇带标题、表格、代码块的报告,可能要建几十个block,不仅代码量大,顺序错一个整篇内容就乱了。
后来我发现了drive/v1/import_tasks这个导入接口,思路一下子通了。它本质上是一个“文件转换任务”,支持把Markdown内容直接转成飞书云文档。你只需要传file_extension为md,file_name给个标题,file_content放上整篇Markdown正文,再把point里的mount_key指定成某个文件夹的token,就会在后台异步执行导入,然后返回一个ticket。用ticket轮询导入结果,拿到最终文档的URL和token。
这个接口最大的好处是免去了逐行建block的问题。我实测标题、加粗、引用、代码块、表格这些Markdown元素都能转到飞书文档,基本不用二次排版。但需要注意的是,它要求file_content必须有完整的Markdown正文,不能只给一句话;另外要注意异步轮询,导入不是立刻返回结果,需要间隔几秒查一次ticket状态。我在设计脚本时把这个循环写成了30秒超时,超过就报错,实测90%的导入都在10秒内完成。
2.2 群机器人Webhook:把文档卡片送进群聊
群机器人是飞书里最简单的接入点,创建方式很顺手:在群里打开设置,添加自定义机器人,拿到Webhook地址。飞书支持文本、富文本、交互卡片三种消息,我实际使用下来,最合适的是交互卡片。因为发卡片不只是通知“文档传好了”,还能直接在卡片里放文档标题、摘要、链接,点击就能打开,视觉上比纯文本清爽得多。
调用方式就是一个POST请求,把JSON发到Webhook地址。卡片的字段结构稍微有点绕——header里放标题,elements里放正文内容,正文可以用lark_md标记语言,支持加粗、超链接、@人。我习惯把文档链接做成“ 文档标题 ”格式,这样群里的人点一下就能进去,不用再复制链接。
安全设置这里必须多说一句。飞书自定义机器人支持三种安全设置:关键词、签名校验、IP白名单。我强烈建议至少开启签名校验,因为Webhook地址一旦泄露,任何人都能往你的群里发消息。签名算法不复杂:把timestamp和密钥拼接做HMAC-SHA256,再Base64编码,具体字段官方文档写得很清楚。我第一次做的时候没开签名,结果一次测试时地址被无关脚本扫描到,群里涌入了一堆垃圾消息,后来老老实实把签名校验加上,世界清净了。
2.3 多维表格写入:结构化数据的落库路径
文档适合叙事,但有些数据天然是结构化的。比如我每天用AI跑出来的项目“风险清单”,每条记录包含风险等级、描述、负责人、状态,这种内容放文档里检索起来很痛苦,适合放进多维表格。
多维表格的API模型是app_token加table_id。一个多维表格是一个app,里面可以有多张表。写入记录走的是POST /open-apis/bitable/v1/apps/{app_token}/tables/{table_id}/records,body里直接传fields对象,键是字段名,值是字段值。
听起来很简单,但细节坑不少。最典型的是日期字段。飞书多维表格的日期字段接收的是毫秒时间戳,不是“2025-04-03”这种字符串。我第一次写入时传了字符串,API返回200,但表格里那列全是空的。排查半天发现是我把日期写成字符串了,改成时间戳后立刻正常。还有单选字段,传入的值必须和字段的选项完全一致,多一个字就写入失败。这些知识点接口文档里都有,但文档分散,不看够一定数量根本想不到。我把自己的约定放在config里,所有日期都统一由脚本转时间戳,单选枚举都做一层映射,这才彻底解决。
2.4 权限、Token与安全边界:先想清楚再动手
飞书开放平台的权限体系比想象中严格,也是最容易在一开始劝退人的地方。创建应用后,开发阶段可以用“测试企业”模式,但真正要让脚本跑得顺畅,必须给应用开通一堆权限,且需要管理员审核。
我用的核心权限包括:创建文档(docx:document)、导入云文档(drive:import)、发送机器人消息(im:message:send_as_bot)、读写多维表格记录(bitable:app)。在开放平台的权限管理里搜索对应的英文标识并开通即可。这里建议一次性把所有要用的权限都开好,因为每改一次权限都要重新发布应用版本,审核走流程也要时间,反复改真的很烦。
Token方面要分清tenant_access_token和user_access_token。前者是应用身份,适合我这个脚本场景,因为它不需要用户登录态,每次调用接口前带上即可。它的有效期是2小时,所以我做了缓存:全局变量记录获取时间和token,过期才重新请求,避免每次调用都刷一次token。调用量小的时候无所谓,但批量导入10个文档时,每篇都去重新获取token就完全没有必要了。
安全边界上还要注意:不要把app_secret硬编码在代码里,我放在config.yaml里,而且这个配置文件加了权限限制,不提交到Git仓库。飞书的权限最小化原则也适用:只申请脚本必需的那些权限,别图省事申请一堆用不到的,审核过不了还是小事,权限面扩大才是风险。
3. 实操过程与核心环节实现
3.1 环境准备:开放平台建应用 + 本地依赖
整个环境准备我只做了三件事。
第一件是在飞书开放平台创建企业自建应用。登录后进入开发者后台,点创建应用,填名称和描述,就有了app_id和app_secret。然后进权限管理,把前面说的docx、import、bot、bitable几类权限搜出来开通。最后在版本管理里创建版本并发布,等管理员审核通过。这一步其实不复杂,但很多人会卡在这里,因为不发布应用拿不到正式权限,调用接口时会报“权限不足”。
第二件是群里加机器人。打开目标飞书群,在设置里添加自定义机器人,复制Webhook地址,打开签名校验,保存密钥。如果你不想签名,也可以选关键词校验,但我不推荐。
第三件是本地Python环境。我用的Python 3.11,装三个库就够了:requests、pyyaml、openai。openai这个库其实只用到了最基础的chat.completions接口,你完全可以用requests直接调,但它封装好了,省事。
装完依赖后,我把config.yaml按以下结构写好:
feishu: app_id: "cli_xxxxxxxx" app_secret: "your-secret-here" webhook: "https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxx" webhook_sign_secret: "your-sign-secret" import_folder_token: "fldxxxxxxxx" bitable_app_token: "bascnxxxxx" bitable_table_id: "tblxxxxx" llm: base_url: "https://api.deepseek.com/v1" api_key: "sk-xxx" model: "deepseek-chat" temperature: 0.3说明一下这些token去哪里找:应用凭证页有app_id和app_secret;群机器人设置页有webhook和签名密钥;云空间里想存放文档的文件夹,在浏览器地址栏URL中能找到folder_token;多维表格的链接里则能解析出bascn开头的app_token和tbl开头的table_id。第一次接触的人会觉得头大,按这个对应关系去填就好。
3.2 核心代码:AI生成内容并导入飞书文档
先实现第一步:拿用户给的prompt,让大模型生成Markdown正文。
from openai import OpenAI client = OpenAI(base_url=cfg["llm"]["base_url"], api_key=cfg["llm"]["api_key"]) def generate_markdown(prompt: str) -> str: resp = client.chat.completions.create( model=cfg["llm"]["model"], messages=[ {"role": "system", "content": "你是一个严谨的文档助手,只输出规范的Markdown正文,不要输出解释性语言。"}, {"role": "user", "content": prompt}, ], temperature=cfg["llm"]["temperature"], stream=False, ) return resp.choices[0].message.content这里有个我把控得很死的地方:system prompt里要求模型只输出Markdown正文。如果不加这个约束,模型经常会在文档前后加“好的,以下是生成的文档”这类废话,导入飞书后这些废话全得手工删。加一句“不要输出解释性语言”能明显减少返工。
接下来是获取tenant_access_token并调用导入接口:
import requests, time, base64, hashlib _token_cache = {"token": None, "expire_at": 0} def get_tenant_token() -> str: if _token_cache["token"] and time.time() < _token_cache["expire_at"]: return _token_cache["token"] resp = requests.post( "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", json={"app_id": cfg["feishu"]["app_id"], "app_secret": cfg["feishu"]["app_secret"]}, timeout=10, ).json() _token_cache["token"] = resp["tenant_access_token"] _token_cache["expire_at"] = time.time() + int(resp["expire"]) - 120 return _token_cache["token"] def import_markdown(title: str, markdown: str) -> dict: token = get_tenant_token() payload = { "file_extension": "md", "file_name": title, "file_content": markdown, "point": {"mount_type": 1, "mount_key": cfg["feishu"]["import_folder_token"]}, } headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} resp = requests.post( "https://open.feishu.cn/open-apis/drive/v1/import_tasks", headers=headers, json=payload, timeout=15, ).json() ticket = resp["data"]["ticket"] for _ in range(10): time.sleep(2) r = requests.get( "https://open.feishu.cn/open-apis/drive/v1/import_tasks/" + ticket, headers=headers, timeout=10, ).json() if r["data"]["result"]["job_status"] == 0: return r["data"]["result"]["tokens"] raise TimeoutError("import task timeout")解释一下两个细节。expire减去120秒,是给token留了2分钟的余量,防止正好卡在服务端过期时间附近。导入任务返回的是tokens字段,里面一般是一个数组,通常会包含原始token和转换后的token,我取最后一项作为正式的文档token,再拼接成文档URL。
生成标题也顺便讲一下,我让AI在文档正文之前额外输出一行“title: 某某报告”,脚本里解析这个字段来决定文件名。比起传死标题,这样更灵活,每天生成的报告标题都会自动带上日期。
3.3 核心代码:机器人发文档卡片到群
文档创建完成后,下一步是把文档卡片发到群里。我封装了一个函数:
def send_doc_card(title: str, doc_url: str, summary: str): sign = gen_sign(cfg["feishu"]["webhook_sign_secret"]) card = { "config": {"wide_screen_mode": True}, "header": { "title": {"tag": "plain_text", "content": f"文档已生成:{title}"}, "template": "blue" }, "elements": [ {"tag": "div", "text": {"tag": "lark_md", "content": f"**[查看文档]({doc_url})**"}}, {"tag": "hr"}, {"tag": "div", "text": {"tag": "lark_md", "content": summary}}, ], } requests.post( cfg["feishu"]["webhook"], json={"timestamp": sign["ts"], "sign": sign["sign"], "msg_type": "interactive", "card": card}, timeout=10, )签名函数是官方算法,我直接按文档实现:
def gen_sign(secret: str) -> dict: ts = str(int(time.time())) string_to_sign = f"{ts}\n{secret}" h = hashlib.sha256(string_to_sign.encode("utf-8")).digest() sign = base64.b64encode(h).decode("utf-8") return {"ts": ts, "sign": sign}这里有一个容易踩的坑:开启签名校验后,timestamp和sign要放在请求体顶层,和msg_type、card平级,而不是塞进card里。我一开始放错位置,飞书一直报签名错误,排查了十几分钟才反应过来。
摘要summary怎么来?我让大模型在生成正文之前先输出一段百字以内的摘要,脚本提取后传给卡片。这样群成员不用打开文档就知道大概内容,体验接近人工发消息。实际效果比我预期好很多,群里的反馈是“至少能先瞄一眼再决定要不要点进去”。
3.4 一键封装与定时任务:让流程真正自动化
所有模块都完成后,就是“一键”了。我用click写了一个命令行入口:
python feishu_uploader.py --prompt "生成今天的数据日报,重点突出异常指标" --title-prefix "日报" python feishu_uploader.py --from-file weekly_template.md在脚本里,主函数分了五步:解析参数、调generate_markdown生成正文、解析标题和摘要、调import_markdown导入文档、调send_doc_card发卡片。每一步都有try/except,出错时会在本地日志里写下当前步骤和错误信息,而不是直接崩溃。这样排障时能直接知道是哪一环挂了。
做完命令入口后,我又加了一步定时任务。在Windows上,我通过任务计划程序让脚本每天早上9点自动跑一次日报。命令行长这样:
schtasks /create /tn "feishu-daily-report" /tr "C:\Users\me\.venvs\feishu\Scripts\python.exe C:\scripts\feishu_uploader.py --prompt-file daily_report.txt" /sc daily /st 09:00 /f这里有个大坑:任务计划程序里如果直接用python,不加全路径,很可能因为找不到解释器而失败,尤其是用了虚拟环境的时候。我一开始用任务计划界面配置,一直提示“操作成功但任务未运行”,查日志才发现是指向了全局Python而不是虚拟环境里的Python。后来改成虚拟环境里的绝对路径并设置“起始于”目录为脚本所在目录,就稳定了。
还有一步值得提的是AI辅助开发。这个脚本从零到能跑通,大部分代码其实是Claude Code帮我写的。我给它描述清楚需求,它直接输出带注释的版本,我再根据飞书API文档校对关键字段。开发过程中遇到Webhook签名问题时,我把官方文档丢给它,它也能给出修正代码。顺带说一句,身边也有用Codex接入飞书场景做插件尝试的朋友,思路大同小异,都是让AI理解飞书API结构,减少人工翻文档的时间。用AI编程工具的意义不是完全不用人,而是把“查文档、写样板代码”这部分时间砍掉,人把精力留给业务逻辑和排障。
4. 常见问题与排查技巧实录
4.1 创建文档时报权限不足
这是最常见的拦路虎。表现为调用导入接口返回错误码99991672,提示“权限不足”或“操作无权限”。
我排查的思路分三步。第一步,确认应用是否已经发布并通过审核,开发态下很多权限不生效;第二步,检查权限管理里是否真的开通了对应权限,比如导入文档要开通“导入云文档”,消息要用“获取与发送单聊、群组消息”的机器人权限;第三步,如果应用是刚加权限的,必须在版本管理里再走一次发布流程,让新权限生效。
值得一提的还有一个隐蔽问题:即使应用有权限,调用导入接口时指定的folder_token必须在应用可见范围内。如果文件夹没授权给应用,接口照样报权限错误。解决办法是在云空间里把文件夹分享给这个应用的“管理员”,或者直接把应用加为文件夹协作者。
4.2 导入API一直返回400或解析失败
导入接口返回400时,八成是file_content本身有问题。我遇到过两类情况。
一类是中英文混合的非法JSON字符,比如AI生成内容里带了控制字符或异常换行,导致整个请求体无法被服务端解析。解决办法是发送前做一次清洗:把连续空白字符压缩、把异常换行替换成正常换行。另一类情况是Markdown内容包含不支持的语法,比如某些扩展表格写法,导入后个别块会渲染失败,但通常不影响整体文档生成。我的策略是在脚本里加了一个简单的“后处理”:把AI输出中的多余空行删掉,把英文引号统一成中文引号,实测能减少大半解析错误。
说到底,飞书导入接口的核心限制是它把Markdown当作文本内容去解析,所以对内容格式的容错不算强。遇到解析失败,最直接的排查方式就是先用postman发一段最简单的Markdown测试,排除接口和权限问题后,再逐步增加内容复杂度,定位到具体是哪段语法出了问题。
4.3 Webhook消息发不出去或签名报错
Webhook的问题通常集中在三类。
第一类是网络层,请求超时或者返回403。这种情况先确认webhook URL是否完整,是否多了空格或换行。第二类是签名错误,报错信息里会出现“invalid sign”。如果你的机器人开启了签名校验,记住我前面说的:timestamp和sign要放在顶层。我发现很多人是把签名放在了card对象里,飞书根本找不到,自然校验失败。第三类是内容格式错误,卡片里文本标签写错也会发不出去。
还有一个小概率的情况:同一个Webhook被多个脚本共用,一个脚本的消息被另一个脚本的签名覆盖了,但这种情况很少见。我后来把Webhook按用途拆分,日报群、告警群、文档归档群各用各的机器人,排障更清晰。
4.4 多维表格写入失败:字段类型与格式坑
多维表格写入报错,最常见的是字段类型不匹配。比如多选字段传了字符串,日期字段传了日期字符串而不是毫秒时间戳,数字字段传了带千分位的字符串。
我的建议是写一个字段类型映射表:文本字段传字符串、数字字段传数字、日期字段传毫秒时间戳、单选/多选字段传选项值,所有布尔值传true/false。另外,如果要写入的字段是非必填的,干脆不要出现在fields里,省得为它拼一个空值。初期写完记录后,多去表格里看一眼实际渲染效果,比看接口返回更直观。API返回200不代表数据落对了,只有表格里显示符合预期才算真成功——这句话是我的血泪经验。
4.5 我的踩坑总结
整理一下我累计修复过的问题,做成一个速查:
| 问题现象 | 常见原因 | 快速处理办法 |
|---|---|---|
| 导入返回权限不足 | 应用未发布或文件夹无权限 | 发布版本并给应用授权文件夹 |
| 导入返回400 | Markdown含非法控制字符 | 发送前做内容清洗 |
| 文档导入成功但内容乱 | 扩展表格语法不兼容 | 简化Markdown表格结构 |
| Webhook签名报错 | sign放错位置 | 放在请求体顶层 |
| 消息发送403 | 校验未通过 | 检查签名算法和密钥 |
| 多维表格日期为空 | 传了日期字符串 | 转毫秒时间戳后再写入 |
| 日报任务没跑 | 计划任务用了错误Python | 使用虚拟环境绝对路径 |
这个表格不是标准答案,但每一条都是真实线上跑出来的问题。技术文档会教你怎么做对,但很少教你做错之后怎么找原因,这也是我坚持记录的原因。
5. 扩展玩法与个人体会
5.1 从单点到Agent:让飞书机器人自己干活
目前这个脚本还是“被动执行”:我给它一个prompt,它跑完整条链路。要做到真正的“对话即操作”,就要把它升级成飞书Agent。飞书开放平台支持事件订阅,机器人可以接收群里的消息,当有人@它时说“生成日报”,它就把任务丢给脚本,完成后把结果和链接发回群里。
这意味着要把脚本包成一个可被事件回调的服务,加一层消息解析逻辑。听起来很复杂,但它本质上是给现有脚本加一个入口:把收到的文本当prompt,走原有生成和上传流程。我在测试环境里跑过一版,体验很爽——完全不用自己打开终端,群里说一句话,文档自己就出现在协同空间里,链接也自动弹出。
还有一个方向是接入知识库。飞书的知识库本质上是文件夹权限和文档结构的组合。你可以按项目建文件夹,把导入的文档按规则自动分流到对应目录,配合多维表格做索引,就有一个简易的“AI知识库”了。不用买昂贵的知识管理软件,一套脚本加上飞书开放API就能支撑小团队使用。
5.2 写在最后:AI替我实现文档自由的含义
“文档自由”这个词,很多人以为是自动写文档。其实真正自由的是不再被工具流程绑住。我不用再记着“写完了要粘贴”“发完了要通知”,这些杂事脚本接管之后,我能把时间花在真正需要判断的地方:内容是否准确、逻辑是否严密。
最后分享一个我至今受用的小习惯:所有自动化脚本,无论多简单,一定要在开头打印一行当前步骤和时间,失败时把上下文带出来。一开始我觉得多余,日志文件也懒得看,后来跑了三周准时任务,发现排障时有这一行能少掉九成的猜测时间。AI生成的代码再聪明,跑在真实环境里总会碰见权限、格式、网络这类的意外,人还是要掌握最基本的排障能力。一个项目真正落地,靠的不是某次灵光一现,而是把每个环节的不确定都变成可控,这大概也是“文档自由”背后最快的路径。