最近业务需要从飞书多维表格中提取提示词和参考图片,来完成生图的功能,然后图片链接写入飞书。
这里测试的是record_id,这个是每行的独特标识。
需要使用火山 TOS 作为图床
env文件
.env文件(里面的具体数值为假的值)
c
ARK_API_KEY=AKLTYzY1NMTI2MDFhNzAxNDcwYjY
# 火山 TOS(示例名,按你自己的填)
TOS_ACCESS_KEY="AKLTYTFlZmVlNWME2YmFkZjlmODI5ZGMyODdhMWM"
TOS_SECRET_KEY="TURreFkyVxTWpZME5EZGhZamcyTjJSalpXVTNOR1JoT1ROa09XSQ=="
TOS_ENDPOINT="tos-cn-beijing.volces.com" # 例:北京区
TOS_REGION="cn-beijing"
TOS_BUCKET="image-generate"
# 飞书配置(必需)
# ===== 飞书应用 =====
FEISHU_APP_ID=cli_a9fb17d857385bc2
#cli_a73b7=
FEISHU_APP_SECRET=pEb6PUwkY1QU4yPzvKnv8d0PEvCBt8HX
#xeKSIOpQiVW6oHe
# ===== 飞书多维表格(Bitable)=====
# 这是新的生成图片样本的
FEISHU_BASE_TOKEN=Fh12bz2rscOypcG72cnLd
FEISHU_TABLE_TOKEN=tblMlrJL1ch
# (可选)只操作某个视图
# FEISHU_VIEW_ID=vewKPAuI2U
# 日志级别(DEBUG/INFO/WARNING/ERROR)
LOG_LEVEL=DEBUG
# 是否在 DEBUG 日志中输出明文联系方式(true/false)
DEBUG_LOG_CONTACT=false
具体代码
具体使用的模型id在https://console.volcengine.com/ark/region:ark+cn-beijing/model/detail?Id=doubao-seedream-4-5 这里找到。
c
import os
import time
import mimetypes
from typing import Optional, Tuple
import requests
import tos # ve-tos-python-sdk
## 有record id 就可以按照prompt生图
# ===== 飞书配置 =====
FEISHU_APP_ID = os.getenv("FEISHU_APP_ID")
FEISHU_APP_SECRET = os.getenv("FEISHU_APP_SECRET")
APP_TOKEN = os.getenv("FEISHU_BASE_TOKEN") # bitable app_token
TABLE_ID = os.getenv("FEISHU_TABLE_TOKEN") # table_id
FIELD_PROMPT = "提示词"
FIELD_REF_IMAGE = "参考图片"
FIELD_OUTPUT = "生成图片链接" # 你表里需要有这一列(文本/URL)
# 可选字段(你现在表里没有就保持 None)
FIELD_STATUS = None
FIELD_ERROR = None
# ===== Ark/即梦配置 =====
ARK_API_KEY = os.getenv("ARK_API_KEY")
ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
SEEDREAM_MODEL = "doubao-seedream-4-5-251128"
# ===== TOS 配置 =====
TOS_AK = os.getenv("TOS_ACCESS_KEY")
TOS_SK = os.getenv("TOS_SECRET_KEY")
TOS_ENDPOINT = os.getenv("TOS_ENDPOINT") # e.g. tos-cn-beijing.volces.com
TOS_REGION = os.getenv("TOS_REGION") # e.g. cn-beijing
TOS_BUCKET = os.getenv("TOS_BUCKET") # e.g. image-generate
def assert_env():
need = {
"FEISHU_APP_ID": FEISHU_APP_ID,
"FEISHU_APP_SECRET": FEISHU_APP_SECRET,
"FEISHU_BASE_TOKEN": APP_TOKEN,
"FEISHU_TABLE_TOKEN": TABLE_ID,
"ARK_API_KEY": ARK_API_KEY,
"TOS_ACCESS_KEY": TOS_AK,
"TOS_SECRET_KEY": TOS_SK,
"TOS_ENDPOINT": TOS_ENDPOINT,
"TOS_REGION": TOS_REGION,
"TOS_BUCKET": TOS_BUCKET,
}
missing = [k for k, v in need.items() if not v]
if missing:
raise RuntimeError(f"Missing env vars: {missing}")
def feishu_tenant_token() -> str:
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
r = requests.post(url, json={"app_id": FEISHU_APP_ID, "app_secret": FEISHU_APP_SECRET}, timeout=30)
r.raise_for_status()
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"feishu token error: {data}")
return data["tenant_access_token"]
def bitable_get_record(tk: str, record_id: str) -> dict:
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{record_id}"
r = requests.get(url, headers={"Authorization": f"Bearer {tk}"}, timeout=30)
r.raise_for_status()
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"get record error: {data}")
return data["data"]["record"]
def bitable_update_record(tk: str, record_id: str, fields: dict):
url = f"https://open.feishu.cn/open-apis/bitable/v1/apps/{APP_TOKEN}/tables/{TABLE_ID}/records/{record_id}"
r = requests.put(
url,
headers={"Authorization": f"Bearer {tk}", "Content-Type": "application/json"},
json={"fields": fields},
timeout=30,
)
# 打印错误体,方便排错
if r.status_code != 200:
print("bitable_update status =", r.status_code)
print("bitable_update raw =", r.text)
r.raise_for_status()
data = r.json()
if data.get("code") != 0:
raise RuntimeError(f"update record error: {data}")
def feishu_download_media_from_bitable_field(tk: str, ref_field: dict) -> Tuple[bytes, str]:
"""
ref_field: fields["参考图片"][0] 这种结构
关键:直接使用字段里的 url/tmp_url,并带 Authorization 下载
"""
download_url = ref_field.get("url") or ref_field.get("tmp_url")
if not download_url:
raise RuntimeError(f"reference image field has no url/tmp_url: {ref_field}")
headers = {"Authorization": f"Bearer {tk}"}
r = requests.get(download_url, headers=headers, timeout=60)
# 有些情况下返回体为空,但 status!=200,打印一下方便定位
if r.status_code != 200:
print("feishu_download status =", r.status_code)
print("feishu_download raw =", r.text)
r.raise_for_status()
content_type = r.headers.get("Content-Type", "application/octet-stream")
return r.content, content_type
def tos_client():
return tos.TosClientV2(
ak=TOS_AK,
sk=TOS_SK,
endpoint=TOS_ENDPOINT,
region=TOS_REGION,
)
def tos_put_and_presign(data: bytes, content_type: str, key: str, expires: int = 3600) -> str:
client = tos_client()
client.put_object(
bucket=TOS_BUCKET,
key=key,
content=data,
content_type=content_type,
)
# 新版本 SDK generate_presigned_url 直接返回 str
presigned_url = client.generate_presigned_url(
Method="GET",
Bucket=TOS_BUCKET,
Key=key,
ExpiresIn=expires,
)
return presigned_url
def ark_generate(prompt: str, image_url: Optional[str]) -> dict:
url = f"{ARK_BASE_URL}/images/generations"
headers = {"Authorization": f"Bearer {ARK_API_KEY}", "Content-Type": "application/json"}
payload = {
"model": SEEDREAM_MODEL,
"prompt": prompt,
"size": "2K",
"n": 1,
"watermark": False,
"response_format": "url",
"stream": False,
}
if image_url:
payload["image"] = image_url
r = requests.post(url, headers=headers, json=payload, timeout=120)
if r.status_code != 200:
raise RuntimeError(f"Ark error {r.status_code}: {r.text}")
return r.json()
def safe_get_first_image_field(fields: dict) -> Optional[dict]:
arr = fields.get(FIELD_REF_IMAGE) or []
if isinstance(arr, list) and arr:
return arr[0]
return None
def run_one(record_id: str):
tk = feishu_tenant_token()
# 可选:先标记状态(你没用就不写)
if FIELD_STATUS:
try:
bitable_update_record(tk, record_id, {FIELD_STATUS: "生成中"})
except Exception:
pass
try:
rec = bitable_get_record(tk, record_id)
fields = rec.get("fields", {})
prompt = (fields.get(FIELD_PROMPT) or "").strip()
if not prompt:
raise RuntimeError("提示词为空")
ref = safe_get_first_image_field(fields)
image_url = None
# 有参考图 => 先下载飞书附件 => 上传 TOS => 取预签名 URL => 图生图
if ref:
img_bytes, content_type = feishu_download_media_from_bitable_field(tk, ref)
ext = mimetypes.guess_extension(content_type) or ".bin"
key = f"feishu_refs/{record_id}_{int(time.time())}{ext}"
image_url = tos_put_and_presign(img_bytes, content_type, key, expires=3600)
# 调即梦
result = ark_generate(prompt=prompt, image_url=image_url)
gen_url = result["data"][0]["url"]
# 回写生成图链接
update = {FIELD_OUTPUT: gen_url}
if FIELD_STATUS:
update[FIELD_STATUS] = "成功"
bitable_update_record(tk, record_id, update)
print("✅ done:", record_id, gen_url)
except Exception as e:
err = str(e)
print("❌ failed:", err)
update = {}
if FIELD_STATUS:
update[FIELD_STATUS] = "失败"
if FIELD_ERROR:
update[FIELD_ERROR] = err[:2000]
if update:
try:
bitable_update_record(tk, record_id, update)
except Exception:
pass
raise
if __name__ == "__main__":
assert_env()
run_one("recpU2Au47")
输入提示词:把瓶子变成绿色,细节丰富。
参考图片:

运行结果:


后记
2026年2月5日周四于上海。