ARTICLE · 1239250
把 AI 回复变成飞书文档:使用方法与实现原理

在 ChatPage 里整理完一份调研或会议记录,接下来通常要把结果留给同事。复制回复、打开飞书、新建文档、重新排版,是一条常见的操作路径。
会话里的「创建飞书文档」把这一步接了起来。已经满意的一条 AI 回复,可以直接保存为飞书文档;需要继续整理的材料,也可以交给 Agent,让它组织正文、调用创建工具,再把真实文档链接带回来。
这两个入口看起来接近,执行方式却不同。本文先讲怎么用,再沿当前源码说明:内容怎样离开聊天框,Markdown 怎样变成飞书内容块,权限在哪一层检查,以及为什么报错后不能简单地再创建一遍。
本文按 2026 年 10 月 16 日的 ChatPage 开发工作区核对,版本字段为 0.92.22。其中原生表格转换、表格子树写入与转换前置属于本地尚未提交的改动,不代表正式版已交付。文中材料和调用参数为示例,插画用于解释界面与机制;本次没有向真实飞书租户创建文档。
两个入口:保存这条回复,或交给 Agent 整理后创建
第一种,是把已经生成的回复保存下来。 等回复结束,将鼠标移到回复下方的操作栏,找到「创建飞书文档」。如果尚未配置飞书应用,点击会进入设置;配置可用时,按钮开始创建,成功后变成可打开文档的链接图标。
这个按钮传入的是当前这条回复的正文,不会自动汇总整场对话。若你想保存多轮讨论的结论,可以先让 ChatPage 整理成一条完整回复,核对后再点按钮。
标题也有规则:优先取正文里的第一个 Markdown 标题,截取最多 50 个字符;没有标题时取压缩空白后的正文开头,超过 24 个字符加省略号;仍没有内容时,才使用带时间的默认标题。想让标题明确,先让回复以一个合适的标题开始。
第二种,是在会话里直接交付创建任务。 例如:
根据刚才确认的讨论,新建一篇飞书文档,标题为「项目评审纪要」。正文分成背景、已确认结论、待确认问题、行动项四部分。没有确定的负责人和时间写「待确认」,不要补猜。使用默认文件夹,不覆盖已有文档,不发送群消息。创建后返回文档链接,并读取文档核对正文与行动项。
这时,Agent 可以先整理内容,再调用 feishu_doc 的 create 操作。创建、读取、核对是几次有先后关系的工具操作,不是把一句回复写得像“已经完成”。
使用前,需要在设置里启用飞书集成并配置自建应用的 App ID、App Secret;可以设置默认文件夹,并按需要授权个人飞书账号。目标文件夹与文档权限仍以实际请求身份为准。本文这条内置创建链路不依赖本机服务或本机 lark-cli。

图 1 · 按钮直接使用一条回复的内容;Agent 先形成工具参数。两条路径最终调用同一个创建函数,图中是机制示意。
按钮路径:一次明确的界面操作
src/sidepanel/components/MessageItem.tsx 把消息正文传给 CreateDocButton。后者在 src/sidepanel/components/CreateDocButton.tsx 中维护四种状态:
idle ──点击且配置可用──→ loading ──创建完成──→ done(文档链接)
└─失败──→ error(错误与重试入口)
配置不可用:打开设置,不发起创建。
核心调用很直接,下面是源码中的实际语句:
const result = await createFeishuDoc(feishuConfig!, title, content);
setDocUrl(result.url);
setState("done");
这里没有让模型重新生成一遍文章,也没有让它控制鼠标去飞书页面粘贴。内容已经存在,前端把用户的点击转换成一次确定的业务调用。创建中显示加载状态,完成后给出可点击的结果。
这条路径也不经过 Agent 工具的风险审批器。用户主动点击、配置检查、飞书鉴权和接口结果共同决定能否完成,不能把它与“模型自主发起写操作”混为一谈。
还有一个容易忽略的边界:当前按钮把状态和链接放在组件的 useState 中。它能反馈这次操作,不能充当跨刷新、跨设备的文档创建台账。真正持久保存的是飞书里的文档;组件重新挂载后,也不能据此保证再次点击不会生成另一篇文档。
Agent 路径:先形成工具调用,再执行外部操作
模型不能凭一段自然语言直接写入飞书。ChatPage 先把当前可用的工具定义交给模型。feishu_doc 定义了 operation、title、content、folder_token 等参数,例如下面这个调用示意:
{
"operation": "create",
"title": "项目评审纪要",
"content": "## 已确认结论\n\n采用方案 B。\n\n## 行动项\n\n- 补充验收条件,负责人待确认。"
}
模型负责组织意图与内容;工具负责验证参数并执行。src/core/tools/feishuDoc.ts 的 create 分支会检查标题是否为空,取出正文与文件夹参数,再调用同一个 createFeishuDoc。
App Secret 和访问 Token 不在这个工具参数契约里。工具运行时从应用配置取得凭证,模型不需要也不应通过正文拼接鉴权信息。
工具是否对模型可见,也受配置约束。ToolRegistry.available() 会检查总工具开关、分类开关、只读限制和工具自己的可用条件。飞书应用未配置,或飞书写工具被限制时,Agent 不能假设这个创建工具仍然可用;执行时还有可用性复查。
执行结果会带着调用 ID 回到 Agent 循环,成为 role: "tool" 的消息。下一轮模型据此继续回答,或调用 read 核对文档。模型说“已创建”应该建立在这份结果之上,不能拿预先编好的链接替代。
用户:整理讨论并创建飞书文档
↓
模型:生成 feishu_doc(create, title, content) 调用
↓
应用:检查工具可用性、风险与执行权限
↓
工具:调用 createFeishuDoc,等待真实接口结果
↓
Agent 循环:将结果按 toolCallId 回灌给模型
↓
模型:返回链接;若任务要求,继续 read 核对
这也是 Agent 与单纯生成文案的区别:它能把内容组织、远端写入和后续核验接成任务,但每一步是否完成,都要由实际工具结果支持。
共用创建函数:先有文档,再写内容
两种入口都进入 src/platform/feishuDoc.ts 的 createFeishuDoc。当前顺序是先在本地转换正文,再创建远端文档并写入:
title + Markdown + folder_token
→ markdownToBlocks(content)
→ POST /docx/v1/documents(创建空文档)
→ 取得 document_id
→ GET /docx/v1/documents/{id}/blocks(查询根块)
→ 按块结构分批写入
→ 返回 documentId 与文档 URL
转换放在创建之前,有实际作用。例如正文里的表格超过当前转换器支持的列数,会在发送创建请求之前报错,避免为一份已经无法转换的内容先留下一篇空文档。
创建请求体主要是标题和可选的目标文件夹。文件夹优先采用本次传入的 folder_token,其次采用设置里的默认值;两者都没有时不传这个字段,由飞书按当前身份和接口规则处理,不能一概说“必然创建在我的某个目录”。飞书提供的创建接口也将文档元信息创建与内容块操作分开。飞书创建文档接口
拿到 document_id 后,当前实现读取块列表,并取第一项的 block_id 作为文档根块,随后调用 insertChildrenAt。根块的取得依赖这份新文档响应的实际结构;代码并没有对任意返回顺序重新查找 Page 类型。
Markdown 怎样变成真正的飞书排版
聊天里看到的标题、列表和表格,原始内容通常是 Markdown。飞书文档内容则由带类型和属性的块组成。src/core/markdownToBlocks.ts 承担这层转换。
例如 ## 已确认结论 会成为二级标题块,普通段落成为文本块;段落内的粗体、行内代码和链接则拆成带样式的文本片段。一个简化后的二级标题结构如下,字段与当前实现一致:
{
"block_type": 4,
"heading2": {
"elements": [{
"text_run": {
"content": "已确认结论",
"text_element_style": {}
}
}],
"style": {}
}
}
text_element_style 即使没有样式也保留空对象;标题层级与文本片段各有职责。代码块还会保留语言类型与换行,不按普通段落重新拼接。
当前转换器支持标题、段落、基础有序与无序列表、引用、代码块、行内样式、GFM 表格与分割线。它不是完整的网页排版引擎:复杂嵌套结构不能据此保证原样还原,Markdown 图片语法也不等于完成图片上传。正文含截图时,要另核对图片写入能力,不能只把图片地址混在文本里就宣称已交付图文文档。

图 2 · 普通块与表格子树的结构不同。图中的内容与 ID 为示意,不是某次真实写入结果。
普通段落和表格,走两种写入方式
普通段落等没有子树的内容,通过 children 接口写入。当前实现把连续的平层块分成每批最多 50 个;这是本项目的批次常量,不应推广成所有飞书接口统一的限制。
表格则包含表格块、单元格块和单元格里的文本块。只发送一个平层文本数组,会丢掉行列关系。当前实现因此为子树分配临时 ID,展开成 descendants,并用 children_id 指出本次应挂到父块下的顶层节点,再调用 descendant 接口。
表格
├─ 单元格 → 文本
├─ 单元格 → 文本
└─ ……
前文 → children
表格 → descendant
后文 → children
前文、表格、后文按顺序写入;追加时使用末尾位置,指定位置插入则按已插入的顶层块数推进索引。飞书创建块接口
长表格还要拆成完整行,分段重复表头,控制单次子树规模。当前转换器按每格“单元格+文本”两块计算切分,并限制最多 100 列。列宽也会根据可见文字分配:短编号少占一些,较长说明多分一些;宽表保持最小列宽,可能超出默认正文宽度,而不是强行压成难以阅读的窄列。
这层处理决定了“会话中有表格”和“飞书里有原生表格”能否对应。它属于应用转换与写入的责任,不能只要求模型“请注意排版”就算解决。
以谁的身份创建,决定了能访问什么
两种入口共享飞书请求封装。当前普通文档创建默认采用双轨鉴权:有可用的用户授权时,优先使用 user_access_token;没有可用用户身份时走应用身份;用户身份请求得到失败响应时,默认尝试 tenant_access_token。
这里的回退有明确边界。fetch 本身抛出的网络异常并没有被这段逻辑统一捕获后切换身份;userOnly 请求则禁止回退。工作台里某些“我的数据”使用 userOnly,不能把那条规则套到本文的普通文档创建上。
应用 Token 带缓存,失效时会清缓存并有限重试;用户 Token 根据返回的过期时间刷新,同一个刷新凭据的并发请求共用一个 Promise,避免重复消费旧刷新凭据。
对用户来说,关键是:授权了个人账号,不代表这条默认创建链路永远只用个人身份。用户和应用的访问范围不同,默认文件夹也必须对实际身份可用。排查权限失败,应依次核对应用能力授权、当前请求身份、目标文件夹访问权限,不能统称为“机器人没权限”。
这个鉴权过程与模型的任务规划分开:模型给出要创建什么,应用负责凭证和 HTTP 请求,飞书决定该身份能否完成操作。
创建权限、编辑确认和飞书授权是三层事
按钮是用户主动点击,走直接创建路径。Agent 调用则先进入应用的执行策略:创建、追加或局部编辑默认属于低风险规则,但带有目标明确性检查;目标不明、用户覆盖规则或其他条件都可能改变判断。发送群消息另有规则,不能从“创建了文档”推导出“也可以发群”。
工具定义里还能看到 confirm 参数。不要只读到“缺少 confirm 返回预览”,就断言所有编辑必然多问一次。当前执行器在低风险或已经获准的 feishu_doc 调用中,会注入 confirm: true;高风险操作仍受标准模式审批,用户的禁止规则也不会因为完全访问而失效。
因此要分别看:用户有没有交代这项任务,应用执行策略是否放行,飞书是否允许当前身份写入。三个层面不能互相替代。
写入失败,文档可能已经存在
创建函数只有在后续插入都完成后才返回成功链接。插入失败会抛错,按钮进入错误态,工具返回错误结果,不会把“空文档创建成功”伪装成“正文已写好”。
但这些请求没有组成一个远端原子事务。可能已经创建空文档,也可能第一批内容写完、第二批失败。当前函数没有自动删除空文档、撤销已写入的内容,也没有把每个批次作为可恢复进度持久保存。
按钮提供了重试入口,可重试并不等于幂等。网络中断后再次执行创建,可能多出一篇同名文档。较稳妥的处理是先查看目标文件夹或已知文档,确定哪一步生效;能定位已有文档时读取实际内容,再决定补写还是重新创建。当前错误字符串并不保证携带刚创建出的文档 ID,不能假定总能自动定位残留文档。

图 3 · 部分请求成功不等于任务全部成功。图中的“读取核对”是用户可要求 Agent 继续执行的步骤,不是创建函数内置的事务回读。
成功返回之后,也还有验收。当前创建函数不会自动重新读取整篇文档逐字比较;如果任务要求核对,需要 Agent 再调用 read 或 blocks,用户也可以打开文档检查。重点看标题、正文完整性、表格结构、资料来源和访问范围,尤其不要把拿到链接当成事实内容已经正确。
从这项功能能看到的 Agent 设计
会话创建飞书文档,把一份生成内容接到了真实的外部产物上。两个入口保留了各自适合的交互:按钮负责将确定内容直接保存,Agent 负责组织材料、发起操作并根据结果继续。
实现中值得保留的分工也很具体:模型输出意图和内容,工具校验契约,共用服务转换块结构并写入,请求层管理身份,执行结果回到会话。格式支持与失败处理都应由代码兑现,不能只靠模型的口头承诺。
对使用者,最直接的起步方式是让 ChatPage 整理一条短回复,确认内容后点击创建;需要完整任务时,再明确要求“新建文档、返回链接、读取核对”。这样才能把聊天里的结果,交付成一份真正能打开和检查的文档。















































































