`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:
@@ -0,0 +1,156 @@
|
||||
"""与服务商无关的生成领域模型。
|
||||
|
||||
**本模块不得 import 任何服务商代码,也不得 import httpx。** 这不是洁癖,是本层
|
||||
存在的唯一理由——Java 侧、未来的 gpt-image-2、以及任何第三家供应商都对着这一层
|
||||
编程。一旦这里漏进一个厂商概念,「换供应商不动上层」就不再成立,而那正是分层
|
||||
想买到的东西。
|
||||
|
||||
判断某个概念该放这里还是放 `providers/<厂商>/`:问「换一家供应商,这个概念还在
|
||||
吗」。`Retry`/`UserFacing` 描述的是 **Worker 与客户端该怎么做**,与谁家供应商
|
||||
无关,故在此;`expired` 状态、`QuotaExceeded` 这类码是火山特有的,故不在此。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MediaKind(Enum):
|
||||
"""生成产物的类型,与 `creation.generation_jobs.media_kind` 同值。"""
|
||||
|
||||
IMAGE = "image"
|
||||
VIDEO = "video"
|
||||
|
||||
|
||||
class Retry(Enum):
|
||||
"""失败后该怎么办。由服务商适配层判定,由 Java 侧的 Worker 执行。"""
|
||||
|
||||
#: 不可重试:同样的输入必然得到同样的结果(参数错、内容被拒、鉴权失败)。
|
||||
NEVER = "never"
|
||||
#: 可重试且应尽快:限流、突发流量保护、服务过载。退避几秒到几十秒。
|
||||
BACKOFF = "backoff"
|
||||
#: 可重试但要等很久:额度/配额耗尽,通常要等到下一个计费周期或充值之后。
|
||||
#: Worker 不该把重试预算浪费在这上面,应直接终态失败并让用户看到出口。
|
||||
QUOTA = "quota"
|
||||
|
||||
|
||||
class UserFacing(Enum):
|
||||
"""给终端用户的呈现类别,对应客户端的两个 429 分支(ADR-023 §9)。
|
||||
|
||||
「配额耗尽」与「频率过快」必须分开:前者等的是明天或充值(弹 sheet 给出口),
|
||||
后者等的是几秒(弹 SnackBar 可自动重试)。合成一类必然有一半场景的文案和
|
||||
出口是错的。
|
||||
"""
|
||||
|
||||
QUOTA_EXHAUSTED = "quota_exhausted"
|
||||
RATE_LIMITED = "rate_limited"
|
||||
CONTENT_REJECTED = "content_rejected"
|
||||
SERVER_ERROR = "server_error"
|
||||
UPSTREAM_UNAVAILABLE = "upstream_unavailable"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Failure:
|
||||
"""一次失败的完整描述。
|
||||
|
||||
:param provider_code: 原始厂商错误码,**原样保留、绝不丢弃**——排查时它往往
|
||||
是唯一线索。领域层不理解它的含义,只负责把它带到日志和响应里。
|
||||
"""
|
||||
|
||||
retry: Retry
|
||||
user_facing: UserFacing
|
||||
provider_code: str
|
||||
message: str = ""
|
||||
|
||||
|
||||
class TaskState(Enum):
|
||||
"""与服务商无关的任务状态。
|
||||
|
||||
刻意比火山的状态集**更小**:火山有 queued / running / cancelled / succeeded /
|
||||
failed / expired 六个,这里把 queued 与 running 合成 PENDING/RUNNING 两态、
|
||||
把 expired 归入 FAILED(它是超时,属于失败的一种,重试策略由 `Failure`
|
||||
单独表达)。映射的取舍放在适配层,领域层只保留上层真正需要区分的状态。
|
||||
"""
|
||||
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
SUCCEEDED = "succeeded"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
@property
|
||||
def is_terminal(self) -> bool:
|
||||
return self in {TaskState.SUCCEEDED, TaskState.FAILED, TaskState.CANCELLED}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerationRequest:
|
||||
"""一次生成请求,服务商无关。
|
||||
|
||||
这是 Java 侧发过来的内容。**刻意不含 `user_id`**:本服务干活只需要
|
||||
`job_id`、生成参数与上传目标,传用户标识属于不必要的扩散(ADR-023 §5
|
||||
数据最小化)。需要给厂商做滥用检测时传的是 `safety_identifier`——一个哈希,
|
||||
不是明文用户 ID。
|
||||
|
||||
:param model: 厂商侧的模型标识,由 Java 侧从目录表的 `provider_model_name`
|
||||
快照取值后传入。**适配层不自行选模型**——选型是目录的职责,且任务行里
|
||||
冻结的快照必须与实际调用的模型一致。
|
||||
:param input_url: 参考图/首帧的公网可达 URL。为空即文生视频(参考图选填,
|
||||
决策 A2)。
|
||||
:param upload_target: 产物的预签名 PUT 地址。当前未启用(转存阻塞于存储
|
||||
决策,ADR-023 §7),保留字段使将来启用无需改契约。
|
||||
"""
|
||||
|
||||
job_id: str
|
||||
media_kind: MediaKind
|
||||
model: str
|
||||
prompt: str | None = None
|
||||
input_url: str | None = None
|
||||
ratio: str | None = None
|
||||
resolution: str | None = None
|
||||
duration_sec: int | None = None
|
||||
generate_audio: bool = True
|
||||
watermark: bool = False
|
||||
safety_identifier: str | None = None
|
||||
callback_url: str | None = None
|
||||
upload_target: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerationResult:
|
||||
"""一次查询的结果,服务商无关。
|
||||
|
||||
:param asset_url: 产物 URL。**可能是有时效的厂商地址**——火山 Seedance 的
|
||||
有效期是 24 小时,因此调用方必须在有效期内转存,不可长期保存此值。
|
||||
适配层通过 `asset_url_expires_in_sec` 告知时效,避免上层把它当永久地址。
|
||||
:param cover_url: 封面图 URL(视频场景)。火山的尾帧图可免费充当此用途。
|
||||
:param width_px: 像素宽高。**火山不直接返回像素值**,只返回 `ratio` 与
|
||||
`resolution`;换算需要一张随模型family变化的对照表。故此处在 V7 目录
|
||||
种子定下模型族之前保持 None,并把 `ratio`/`resolution` 原样透出,
|
||||
由上层或后续实现换算——编一个可能错的尺寸比留空更糟。
|
||||
:param billing_tokens: 计费对账依据(火山为 `usage.completion_tokens`)。
|
||||
"""
|
||||
|
||||
provider_request_id: str
|
||||
state: TaskState
|
||||
progress: int = 0
|
||||
asset_url: str | None = None
|
||||
asset_url_expires_in_sec: int | None = None
|
||||
cover_url: str | None = None
|
||||
ratio: str | None = None
|
||||
resolution: str | None = None
|
||||
width_px: int | None = None
|
||||
height_px: int | None = None
|
||||
duration_ms: int | None = None
|
||||
output_format: str | None = None
|
||||
failure: Failure | None = None
|
||||
billing_tokens: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# 失败态必须带 Failure,否则上层无法决定重试策略,只能盲目重试或盲目
|
||||
# 放弃——两者都错。宁可在这里炸掉也不要把半个失败态放出去。
|
||||
if self.state is TaskState.FAILED and self.failure is None:
|
||||
raise ValueError("FAILED 状态必须携带 Failure")
|
||||
if self.state is TaskState.SUCCEEDED and not self.asset_url:
|
||||
raise ValueError("SUCCEEDED 状态必须携带 asset_url")
|
||||
Reference in New Issue
Block a user