# patbond-ai Patbond 的 AI 生成服务(Python)。负责调用 AI 服务商 API 完成图片与视频生成, 是四仓中唯一直接持有 AI 服务商凭证的仓库。 架构与边界的完整决策见 **patbond-doc `docs/architecture/decisions.md` ADR-023**。 以下为要点,改动前请以 ADR 原文为准。 ## 职责边界 - **只负责调用 AI 服务商 API。** 不访问数据库——生成任务状态的唯一事实来源是 Java 侧的 `creation.generation_jobs`。 - **不持有对象存储凭证。** 产物由 Java 下发预签名 PUT 地址,本服务直传后回报, 再由 Java 核实并建 `media.assets` 行(即客户端两步上传,本服务站在客户端位置)。 - **对上游同步或异步一律对外呈现为异步**:`submit` 立即返回任务 id,同步型 服务商(长连接等待返回)在本服务内部以后台任务承载。这样 Java 侧无长连接占线程, 且 `provider_request_id` 立即可得,超时不再产生「是否已扣费」的歧义。 ## 与 Java 侧的接口 - 传输为 **REST(JSON)**,非 gRPC(理由见 ADR-023 §4:避免引入第二套无门禁契约体系)。 - 契约为本仓内独立的 **`openapi-internal.yaml`**,独立版本号, **不进** patbond-api 的四模块快照锁,**也不写入**面向客户端的 `openapi.yaml`。 - 鉴权为 **服务身份 JWT**(独立密钥对,非用户 token 转发):本服务强制校验 `aud=patbond-ai` 与 `typ=service`。 - Java 主动轮询本服务查询进度;回调是将来的可选优化,当前不启用。 - 请求体**不含 `userId`**(数据最小化):完成工作只需 `jobId`、生成参数与预签名上传地址。 ## 代码结构 分四层,依赖**单向**向内(`config` → `providers` → `domain`),由 `tests/test_layering.py` 自动守着——靠纪律守不住的东西,靠测试才守得住。 ``` app/ ├── domain/generation.py 与服务商无关的领域模型 ├── providers/ │ ├── base.py 生成能力抽象(新增供应商 = 实现它) │ ├── registry.py provider_code → 适配器 │ └── volcengine/ │ ├── client.py HTTP 传输与鉴权(不认识「视频」) │ ├── errors.py 火山错误码 → 领域 Failure │ ├── models.py 厂商请求/响应 DTO │ └── video.py Seedance 视频业务 ├── config.py 集中配置(全部旋钮从环境变量读) └── assembly.py 装配根(唯一同时知道配置与具体供应商的地方) ``` **为什么这么切**: - **`domain/` 对厂商与 httpx 零依赖。** 这是「换供应商不动上层」的支点。判断一个 概念该放哪:问「换一家供应商,它还在吗」——`Retry`/`UserFacing` 描述的是 Worker 与客户端该怎么做,与谁家无关,故在领域层;`expired` 状态、 `QuotaExceeded` 这类码是火山特有的,故在适配层。 - **`client.py` 与 `video.py` 分开。** client 只管鉴权、超时、错误解包,不认识 「视频」。以后接火山的**图片或语音**接口时新建兄弟模块复用同一个 client, 而不是再抄一遍鉴权与错误解包——这是拆它出来的全部理由。 - **适配层不读配置**,全部通过构造参数注入。否则单测得摆弄环境变量,且同一进程 内没法跑两套不同配置的实例。 ### 新增一家供应商 / 一种能力 1. 实现 `app/providers/base.py` 的 `GenerationProvider`; 2. 在 `app/assembly.py` 的 `build_registry()` 里登记一行; 3. 目录表的 `provider_code` 与适配器的 `provider_code` 必须同值——Java 侧按 任务行冻结的 `provider_code_snapshot` 查适配器(**不按当前目录**,因为目录 可能被改指向,而在途任务必须仍由当初提交它的适配器轮询)。 上层与领域层**一行都不用改**。 ## 已接入的服务商 | 服务商 | 能力 | provider_code | 状态 | | --- | --- | --- | --- | | 火山方舟 Seedance | 视频生成 | `volcengine-seedance` | 提交与查询已实现,产物转存待存储决策 | 接口事实与错误码映射的完整取证见 patbond-doc `docs/development/providers/volcengine-seedance.md`。其中两条尤其要记住: - **火山不支持幂等键**,「提交超时后不知道有没有扣费」只能由 `max_attempts` 兜住; - **产物 URL 只有 24 小时有效期**,因此厂商 URL 不能当存储用,必须在时限内转存。 ## 配置 全部旋钮见 `app/config.py`,常用项: | 环境变量 | 默认值 | 说明 | | --- | --- | --- | | `ARK_API_KEY` | _(无)_ | 火山方舟密钥。**缺失时该适配器不装配**,服务仍可启动 | | `ARK_BASE_URL` | 北京区官方地址 | | | `ARK_TIMEOUT_SEC` | `10` | **单次 HTTP 往返**超时,不是生成耗时 | | `ARK_VIDEO_MODEL` | _(无)_ | 仅作兜底;正常由 Java 侧传入任务行的模型快照 | | `ARK_TASK_EXPIRY_SEC` | `3600` | 厂商 `execution_expires_after`,区间 `[3600, 259200]`。默认取下限——厂商默认 48 小时对交互式应用过长 | | `PATBOND_SERVICE_JWT_PUBLIC_KEY` | _(无)_ | 校验 Java 侧服务票的 RS256 公钥 | | `PATBOND_SERVICE_JWT_AUDIENCE` | `patbond-ai` | **必须强制校验**,否则独立密钥对失去意义 | 非法值**响亮失败而不静默回落**:一次配置笔误变成难查的行为偏差是最糟的结果。 ## 本地门禁 与 CI 同一套命令(`.gitea/workflows/ci.yml`): ```bash sh scripts/check-secrets.sh --all # 凭证防泄漏全量自查 ruff format --check . ruff check . pytest -q ``` **每人每仓启用一次** pre-commit 防泄漏钩子: ```bash git config core.hooksPath scripts/hooks ``` `scripts/check-secrets.sh` 是四仓同构副本,**调整规则时四仓同步提交**。 它覆盖 AI 服务商密钥形态(`sk-ant-`、`sk-`/`sk-proj-`/`sk-svcacct-`)与 `api_key`/`auth_token` 实值赋值——本仓正是这些规则的主要适用对象。 ## 凭证纪律 真实密钥**只允许存在于被 gitignore 的文件中**(`.env`),占位样例进 `.env.sample`。 拦下真实凭证时的第一动作是**去服务商控制台轮换/禁用该密钥**,然后才是清理提交。 ## 分支与发布 `dev` 直推 + CI 绿,`main` 经 PR 合并。本仓纳入四仓 tag 同步与发布回归 (ADR-023 §12)。发布流程见 patbond-doc `docs/development/releases.md`。