`api_key=volcengine.api_key` 这类**依赖注入传参**被 KEY-ASSIGN 判为疑似凭证。 这是规则的真缺口而非代码问题:行里没有任何秘密,而做 DI 的代码到处都是这种 写法——绕过一次就得在每个调用点绕过,那等于把规则废掉。故改规则。 新增 ALLOW_REFERENCE,刻意收得很紧以免开后门: - 值必须是**未加引号**的小写起首标识符引用(真密钥要么带引号,要么不是标识符形状) - 只允许**恰好一个点且随即行尾** - 连字符不在字符集内 对抗性夹具验证(三真三假): - 拦下 `api_key = "sk-T3Blbk…"`(OpenAI 形态) - 拦下 `secret_key: "AKID…"`(腾讯云形态) - 拦下 `auth_token=eyJ….eyJ….Sfl…`(**三段式 JWT——两个点,故不落进放行**) - 放行 `api_key=lowercase.reference` / `api_key = settings.api_key` / `api_token: config.authToken` 原 5 形态泄漏夹具复验仍 8 条命中,一条不少。 另更正口径:脚本头与 git-workflow.md 仍写「三仓同构」,实际已是四仓—— patbond-ai 是唯一持有 AI 服务商凭证的仓,漏同步它风险最大。四仓 md5 已一致 (4945940093b4b8413b7d3eec8bf6a5ec),四仓 --all 全绿。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,76 @@ Patbond 的 AI 生成服务(Python)。负责调用 AI 服务商 API 完成
|
||||
- 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`):
|
||||
|
||||
Reference in New Issue
Block a user