使用即梦(seedream)来图生图:读取与写入飞书多维表格

最近业务需要从飞书多维表格中提取提示词和参考图片,来完成生图的功能,然后图片链接写入飞书。

这里测试的是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日周四于上海。

相关推荐
Coder_Boy_2 小时前
基于SpringAI的在线考试系统-整体架构优化设计方案(续)
java·数据库·人工智能·spring boot·架构·领域驱动
云飞云共享云桌面2 小时前
推荐一些适合10个SolidWorks设计共享算力的服务器硬件配置
运维·服务器·前端·数据库·人工智能
Elastic 中国社区官方博客2 小时前
Elasticsearch:使用 Base64 编码字符串加速向量摄取
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai·全文检索
大模型玩家七七2 小时前
安全对齐不是消灭风险,而是重新分配风险
android·java·数据库·人工智能·深度学习·安全
李少兄2 小时前
MySQL 中为时间字段设置默认当前时间
android·数据库·mysql
码海踏浪2 小时前
从简单到专业在OceanBase中查看SQL是否走索引
数据库·sql·oceanbase
qinyia2 小时前
**使用AI助手在智慧运维中快速定位并修复服务异常:以Nginx配置错误导致502错误为例**
linux·运维·服务器·数据库·mysql·nginx·自动化
熊文豪3 小时前
关系数据库替换用金仓——Oracle兼容性深度解析
数据库·oracle·金仓数据库·电科金仓·kes
eWidget3 小时前
面向Oracle生态的国产高兼容数据库解决方案
数据库·oracle·kingbase·数据库平替用金仓·金仓数据库