Blog/开发者工具/

Lemon8 内容发现 API 教程 | SandBase

搭一套 Lemon8 内容发现工作流:搜关键词、打开帖子、给作者建档——一个 SandBase 密钥,无需 Lemon8 登录、无需 SDK。

深色电影质感画面:Lemon8 关键词搜索化为一张帖子卡和一张作者资料卡,汇入 Agent 内核

如果你在 Lemon8——字节跳动的美妆、美食和生活方式 App——上做消费趋势研究,你多半想要一个可复用的循环:围绕某个主题找到帖子、打开重要的那些、并搞清楚背后是谁。本教程用三个 SandBase 端点把这个循环串起来,让 Agent 能端到端跑完。它建立在 Lemon8 公开数据 API 汇总页之上;建议先读那篇了解全局。

这里的一切都是公开、只读数据。不需要登录 Lemon8、不需要 SDK——但仍需要一个 SandBase API 密钥来鉴权。端点 API 参考是参数和响应信封的权威来源;下面的载荷字段名来自我实际跑的调用(测试于 2026-09-27,UTC),仅作为一次观测到的结构展示——请以真实响应为准核对,因为负载会随时间变化。

先说结论

  • 三个端点构成一个发现循环:search → post-detail → user-profile。
  • 每次调用都是 POST /v1/api/lemon8/<path>,只传该端点的参数,一个 SANDBASE_API_KEY。
  • 以自然标识链式串联:搜索浮现出 item_id 和 author_id;把它们喂给后两个调用。
  • 只按 status 分支判断一次,并在三步中复用同一段读 JSON 的辅助函数。

工作流一览

  1. 用 lemon8/app/search 搜关键词拿到匹配帖子。
  2. 用从搜索里取到的 item_id 调用 lemon8/app/post-detail 打开一条帖子。
  3. 用帖子上携带的 author_id 调用 lemon8/app/user-profile 给作者建档。

每个端点返回相同信封——一个 id、一个 status、model,以及在 completed 运行上一个 outputs 数组、其唯一元素在 data 下承载数据。failed 或 timeout 的运行带 error 而无 outputs。把 status 检查写一次、处处复用。

SandBase Lemon8 API 页面,含本工作流用到的端点列表 SandBase 上的 Lemon8 端点——search、post-detail 和 user-profile 驱动这个循环。

第 0 步:一个辅助函数管所有调用

import os
import requests

API = "https://api.sandbase.ai/v1/api"
HEADERS = {
    "Authorization": f"Bearer {os.environ['SANDBASE_API_KEY']}",
    "Content-Type": "application/json",
}

def call(path: str, payload: dict) -> dict:
    resp = requests.post(f"{API}/{path}", headers=HEADERS, json=payload, timeout=60)
    resp.raise_for_status()
    body = resp.json()
    if body.get("status") != "completed":
        error = body.get("error", {})
        raise RuntimeError(error.get("message", f"{path} 未完成"))
    return body["outputs"][0]["data"]

下面每一步都调用 call(...),并用 .get() 防御式读取有名字的字段。

第 1 步:搜关键词

blocks = call("lemon8/app/search", {"query": "coffee"})

# 搜索返回一个块列表;code == 0 的块承载 items。
items = []
for block in blocks if isinstance(blocks, list) else []:
    data = block.get("data", {})
    if block.get("code") == 0 and isinstance(data.get("items"), list):
        items = data["items"]
        break

print(len(items), "条帖子")

在我抓到的响应里(搜索 run id 4bac9e8a-eeec-49ec-ba84-49feff8e1928,测试于 2026-09-27,UTC),每条帖子带一个 item_id 和一个含 author_id 的 author 对象。该块还暴露了 has_more 加 max_cursor/min_cursor 用于翻页。字段名会随时间变化——把它们当作观测到的,并对照真实响应核对。

first = next((it for it in items if isinstance(it, dict) and it.get("item_id")), None)
item_id = first.get("item_id") if first else None
author_id = first.get("author", {}).get("author_id") if first else None

第 2 步:打开帖子

detail = call("lemon8/app/post-detail", {"item_id": str(item_id)})
post = detail.get("data", {})  # 以真实响应核对确切路径

在我这次运行里(post-detail run id 814a5872-8dae-4fb9-b7be-c3990afd8da6),负载是一个含 data、message、server_time 的对象,帖子主体在 data 下。防御式读取——它的结构和搜索不同,所以先对照一次真实响应确定路径。

Lemon8 post-detail 端点的 SandBase API 参考 post-detail 参考——item_id 参数和响应路径的事实来源。

第 3 步:给作者建档

profile = call("lemon8/app/user-profile", {"user_id": str(author_id)})
data = profile.get("data", {})  # 以真实响应核对路径

signals = {
    "followers": data.get("followers_count"),
    "following": data.get("following_count"),
    "likes": data.get("digg_count"),
    "comments": data.get("comment_count"),
    "bio": data.get("description"),
}
print(signals)

user-profile 端点接受一个 user_id;搜索结果里的 author_id 可作为该标识。在我这次运行里(user-profile run id a3cf09c5-c08c-4ef7-a898-6a22eeb722ef),负载带 data(含 followers_count、following_count、digg_count、comment_count、description 等),外加 error_code 和 message。用 .get() 读每个字段——可用性各异,请对照真实响应核对。

串起来

def discover(query: str):
    blocks = call("lemon8/app/search", {"query": query})
    items = []
    for block in blocks if isinstance(blocks, list) else []:
        d = block.get("data", {})
        if block.get("code") == 0 and isinstance(d.get("items"), list):
            items = d["items"]
            break

    results = []
    for it in items:
        if not isinstance(it, dict) or not it.get("item_id"):
            continue
        item_id = it["item_id"]
        author_id = it.get("author", {}).get("author_id")
        detail = call("lemon8/app/post-detail", {"item_id": str(item_id)})
        profile = (
            call("lemon8/app/user-profile", {"user_id": str(author_id)})
            if author_id else {}
        )
        results.append({
            "item_id": item_id,
            "post": detail.get("data", {}),
            "author": profile.get("data", {}),
        })
    return results

因为三个端点共享同一个信封,循环保持扁平:call(...) 里一次 status 检查、处处同一套 .get() 模式,标识从一步流向下一步。翻页时,在 has_more 为真期间用块里的游标重发 search。

Lemon8 user-profile 端点的 SandBase API 参考 user-profile 参考——传一个 user_id;搜索结果的 author_id 可作为该标识。

实操要点

  • 搜索返回的是块、不是扁平列表。 找到 code == 0 的块、读它的 items。每个元素都用 isinstance(..., dict) 兜底。
  • 标识是字符串。 稳妥起见把 item_id 和 user_id 当字符串传。
  • 结构因端点而异。 search、post-detail 和 user-profile 各自以不同方式嵌套负载——每条路径先对照一次真实响应确定。
  • 仅公开、只读数据。 不发帖、不涉及私有或仅账号可见数据。用 SandBase API 密钥鉴权。
  • 做个好客户端。 遇到 HTTP 429 等瞬时错误按退避重试;用游标翻页而不是猛打。

常见问题

我需要 Lemon8 登录或 SDK 吗? 不需要。你用自己的 SANDBASE_API_KEY 向 SandBase 鉴权。这些读取端点不需要你这边有 Lemon8 账号或 OAuth。

我怎么拿到 item_id 和作者 id? 从 search。每条帖子带一个 item_id,它的 author 对象带一个 author_id,后者可作为 user-profile 的 user_id。

为什么搜索返回一个块列表? Lemon8 的搜索把结果分组成块,每块带一个上游 code;code == 0 的块承载 items。防御式读取,并以真实响应核对路径。

怎么翻更多结果? 块里暴露了 has_more 加游标字段(max_cursor/min_cursor)。在 has_more 为真期间用游标重发 search。确切参数名请对照参考核对。

小结

三个端点、一个信封、标识在步骤间流动——这就是整个发现循环。完整端点目录和响应细节见 Lemon8 公开数据 API 汇总页。准备好后: