飞书开放平台 API 实战:技术支持岗笔试全记录

11 min

背景

这是一次飞书技术支持岗位的面试前置笔试,考察的核心是候选人能否通过阅读飞书开放平台文档,独立完成一系列 API 调用。考察不要求候选人自行创建应用,而是提供了现成的 app_idapp_secret,直接上手调接口。

笔试一共四个场景,串联了飞书开放平台的多个模块:多维表格、即时通讯(群组)、日历、机器人。以下是完整的技术复盘。

前置:获取 Tenant Access Token

所有飞书开放平台的 API 调用都需要鉴权,第一步就是用应用凭证换取 tenant_access_token

curl -X POST 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal' \
  -H 'Content-Type: application/json' \
  -d '{
    "app_id": "cli_xxxxxxxxxxxx",
    "app_secret": "<your_app_secret>"
  }'

响应:

{
  "code": 0,
  "expire": 5275,
  "msg": "ok",
  "tenant_access_token": "t-g104xxxxx..."
}

这个 token 有效期约 2 小时(expire 单位为秒),后续所有请求都在 Authorization 头中以 Bearer <token> 的形式携带。

文档参考:获取 tenant_access_token

前置:解析多维表格信息

笔试提供了一个多维表格链接,格式如下:

https://<tenant>.feishu.cn/base/<app_token>?table=<table_id>&view=<view_id>

从 URL 中可以直接提取:

参数说明
app_tokenIwPzbs****c13s****0Lqn6c多维表格的唯一标识
table_idtbljZ7****vYwP数据表标识(个人信息表)
view_idvewvtr****Qc视图标识

这些参数会在后续所有多维表格操作中反复使用。

场景 1:写入个人信息

目标:向多维表格的「个人信息表」新增一条记录,包含姓名、年龄、毕业院校、提交时间。

考察材料截图(敏感信息已打码)
考察材料截图(敏感信息已打码)

API 调用

curl -X POST "https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "姓名": "候选人姓名",
      "年龄": 22,
      "毕业院校": "XX大学",
      "提交时间": 1776917790070
    }
  }'

关键细节

  • 字段名必须与多维表格中的列名完全匹配,包括中文和大小写
  • 日期字段需要传入毫秒级 Unix 时间戳,不是 ISO 8601 字符串。可以用 python3 -c "import time; print(int(time.time()*1000))" 快速生成
  • 数字字段直接传数字类型,不要加引号

响应

{
  "code": 0,
  "msg": "success",
  "data": {
    "record": {
      "record_id": "recXXXXXXXX",
      "fields": {
        "姓名": "候选人姓名",
        "年龄": 22,
        "毕业院校": "XX大学",
        "提交时间": 1776917790070
      }
    }
  }
}

务必保存 record_id,后续场景 2 和场景 3 都需要用它来更新这条记录。

文档参考:新增记录

场景 2:创建群组并回写

目标:以机器人身份创建一个群组,群名称为自己的姓名,然后将群名称以群组类型写回多维表格。

2a. 创建群组

curl -X POST "https://open.feishu.cn/open-apis/im/v1/chats" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "候选人姓名"}'

响应中的关键字段:

{
  "code": 0,
  "data": {
    "chat_id": "oc_xxxxxxxxxxxxxxxxxxxx",
    "name": "候选人姓名",
    "chat_type": "private",
    "owner_id": "ou_xxxxxxxxxxxxxxxxxxxx"
  }
}

机器人创建群组时自动成为群主(owner_id 就是机器人的 open_id)。

2b. 回写群名称(群组类型)

这里有一个关键坑点:题目要求写入的是群组类型的值,不是纯文本。群组类型字段需要传入对象数组,包含 id 字段(即 chat_id):

curl -X PUT "https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records/${RECORD_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "群名称": [{"id": "oc_xxxxxxxxxxxxxxxxxxxx"}]
    }
  }'

如果直接传字符串 "群名称": "候选人姓名",虽然不会报错但写入的是纯文本,不会显示为可点击跳转的群组链接。

文档参考:创建群 · 更新记录

场景 3:创建日程 + 上传截图

目标:创建一个当天 10:00-10

的日程(标题为自己姓名),截图 API 响应,上传到多维表格的附件字段。

场景 3 & 4 考察要求(敏感信息已打码)
场景 3 & 4 考察要求(敏感信息已打码)

这是四个场景中步骤最多的一个,串联了三个不同的 API 模块。

3a. 获取主日历 ID

curl -X POST "https://open.feishu.cn/open-apis/calendar/v4/calendars/primary" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json"

响应:

{
  "code": 0,
  "data": {
    "calendars": [{
      "calendar": {
        "calendar_id": "feishu.cn_XXXXXXXXXXXX@group.calendar.feishu.cn",
        "summary": "技术支持面试考察",
        "type": "primary"
      }
    }]
  }
}

calendar_id 藏在 data.calendars[0].calendar.calendar_id 里,嵌套较深。

3b. 创建日程

时间戳计算是关键——需要的是秒级 Unix 时间戳(不是毫秒级,和多维表格日期字段不同):

from datetime import datetime, timezone, timedelta

tz = timezone(timedelta(hours=8))
start = datetime(2026, 4, 23, 10, 0, 0, tzinfo=tz)
end = datetime(2026, 4, 23, 10, 30, 0, tzinfo=tz)

print(int(start.timestamp()))  # 1776909600
print(int(end.timestamp()))    # 1776911400
curl -X POST "https://open.feishu.cn/open-apis/calendar/v4/calendars/${CALENDAR_ID}/events" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "候选人姓名",
    "start_time": {
      "timestamp": "1776909600",
      "timezone": "Asia/Shanghai"
    },
    "end_time": {
      "timestamp": "1776911400",
      "timezone": "Asia/Shanghai"
    }
  }'

注意 timestamp 字段的值是字符串类型,不是数字。

3c. 生成响应截图

API 响应需要截图上传。我用 Python + Pillow 将 JSON 响应渲染为 PNG 图片:

import json
from PIL import Image, ImageDraw, ImageFont

with open("calendar_response.json") as f:
    data = json.load(f)

pretty = json.dumps(data, indent=2, ensure_ascii=False)
lines = pretty.split('\n')

font = ImageFont.truetype(
    "/System/Library/Fonts/Hiragino Sans GB.ttc", 14
)

line_height = 20
padding = 20
max_width = max(font.getlength(line) for line in lines)
width = int(max_width + padding * 2 + 20)
height = len(lines) * line_height + padding * 2

img = Image.new('RGB', (width, height), color=(30, 30, 30))
draw = ImageDraw.Draw(img)

y = padding
for line in lines:
    draw.text((padding, y), line, fill=(0, 255, 0), font=font)
    y += line_height

img.save("calendar_screenshot.png")

生成效果如下:

日程 API 响应截图
日程 API 响应截图

有两个渲染细节需要注意:

  • 字体选择:macOS 上 Menlo 等等宽字体不支持中文,会导致 "summary": "候选人姓名" 中的中文显示为方块。需要用 Hiragino Sans GBPingFang 等中文字体
  • 图片宽度:要根据最长行动态计算宽度,否则长 URL(如 app_link)会被截断

3d. 上传素材到多维表格

飞书多维表格的附件字段不能直接传 URL,需要先通过素材上传接口获取 file_token

curl -X POST "https://open.feishu.cn/open-apis/drive/v1/medias/upload_all" \
  -H "Authorization: Bearer ${TOKEN}" \
  -F "file_name=calendar_response.png" \
  -F "parent_type=bitable_image" \
  -F "parent_node=${APP_TOKEN}" \
  -F "size=${FILE_SIZE}" \
  -F "file=@calendar_screenshot.png"

关键参数说明:

参数说明
parent_typebitable_image表示上传到多维表格,如果是普通文件用 bitable_file
parent_node多维表格的 app_token不是 table_id
size文件字节数必须精确匹配,可用 stat -f%z file 获取

3e. 更新记录附件字段

拿到 file_token 后,更新记录的附件字段:

curl -X PUT "https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records/${RECORD_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "日程接口响应截图": [{"file_token": "XXXXXXXXXXXX"}]
    }
  }'

附件字段的值是一个数组,每个元素包含 file_token。如果要追加附件而不是覆盖,需要先读取现有记录,合并 token 后再写入。

文档参考:获取主日历 · 创建日程 · 上传素材

场景 4:获取机器人信息并写入

目标:获取机器人应用信息,写入多维表格的对应字段,机器人名称后附加自己的姓名。

4a. 获取机器人信息

curl -X GET "https://open.feishu.cn/open-apis/bot/v3/info" \
  -H "Authorization: Bearer ${TOKEN}"

响应:

{
  "code": 0,
  "msg": "ok",
  "bot": {
    "activate_status": 2,
    "app_name": "技术支持面试考察",
    "avatar_url": "https://s3-imfile.feishucdn.com/static-resource/v1/XXXXXXXXXXXX",
    "ip_white_list": [],
    "open_id": "ou_xxxxxxxxxxxxxxxxxxxx"
  }
}

注意这个接口的响应结构与其他接口不同——bot 对象直接在根级别,不在 data 里。

4b. 写入机器人信息

curl -X PUT "https://open.feishu.cn/open-apis/bitable/v1/apps/${APP_TOKEN}/tables/${TABLE_ID}/records/${RECORD_ID}" \
  -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "fields": {
      "机器人名称": "技术支持面试考察-候选人姓名",
      "图像地址": {
        "text": "机器人头像",
        "link": "https://s3-imfile.feishucdn.com/static-resource/v1/XXXXXXXXXXXX"
      },
      "机器人的open_id": "ou_xxxxxxxxxxxxxxxxxxxx"
    }
  }'

「图像地址」字段是超链接类型,需要传 {text, link} 对象而非纯文本。

场景 4 完成效果(敏感信息已打码)
场景 4 完成效果(敏感信息已打码)

文档参考:获取机器人信息

踩坑总结

多维表格字段类型映射

这次笔试涉及的字段类型比较多,总结如下:

多维表格字段类型API 传值格式示例
文本字符串"姓名": "候选人姓名"
数字数字"年龄": 22
日期毫秒级时间戳"提交时间": 1776917790070
群组对象数组 [{id}]"群名称": [{"id": "oc_xxx"}]
附件对象数组 [{file_token}]"截图": [{"file_token": "xxx"}]
超链接对象 {text, link}"链接": {"text": "显示文字", "link": "https://..."}

时间戳精度差异

  • 多维表格日期字段:毫秒级时间戳(13 位)
  • 日历 API 的 start_time/end_time:秒级时间戳(10 位),且为字符串类型

权限问题

调用 API 时可能遇到 99991672 错误(权限不足)。这不影响写操作——有些应用可能只配置了写权限而没有读权限。遇到读接口报权限错误时,可以尝试直接调用写接口,只要从 URL 中提取了 app_tokentable_id 就不需要先调用列表接口。

素材上传

parent_type 的值容易混淆:

  • bitable_image:上传图片到多维表格
  • bitable_file:上传文件到多维表格
  • parent_node 是多维表格的 app_token,不是 table_id

完整调用链路

获取 tenant_access_token

  ├─ 场景1:POST /bitable/.../records  → 拿到 record_id

  ├─ 场景2:POST /im/v1/chats          → 拿到 chat_id
  │         PUT  /bitable/.../records   → 回写群名称

  ├─ 场景3:POST /calendar/.../primary  → 拿到 calendar_id
  │         POST /calendar/.../events   → 创建日程,保存响应
  │         POST /drive/.../upload_all  → 上传截图,拿到 file_token
  │         PUT  /bitable/.../records   → 回写附件

  └─ 场景4:GET  /bot/v3/info           → 拿到 bot 信息
            PUT  /bitable/.../records   → 回写机器人信息

整个流程涉及 8 次 API 调用,横跨 4 个飞书开放平台模块。每一步的输出都是下一步的输入,是一次很好的 API 串联实战练习。