Commit Graph

5 Commits

Author SHA1 Message Date
lixi 86d63ab1cb docs: 补正 4762358 的提交记录——该提交实际包含 AI 服务分层重构
CI / ai-gates (push) Failing after 31s
**这是一条记录补正,不含代码改动。**

4762358 的提交信息只写了「凭证扫描放行依赖注入传参 + 四仓口径更正」,但它实际
包含了 patbond-ai 的整个分层重构(22 个文件、约 1900 行增、约 700 行删)。

成因:前一次提交尝试被 pre-commit 钩子拦下(它正确地拦了 assembly.py 里
`api_key=volcengine.api_key` 这个形态),但 `git add -A` 已经把重构文件留在
暂存区;随后修规则的那次提交只 `git add` 了脚本,却把暂存区里既有的内容一并
提交了。提交前未核对 `git status` 的暂存清单——正是 git-workflow.md「禁止事项」
第一条要求逐一核对的那一步。

不用 amend 重写历史:dev 是共享分支,git-workflow.md 明确禁止对其 force push。
故以本条补正记录说明 4762358 的真实内容,其完整说明如下。

---

**refactor(ai): 按高内聚低耦合分层重构 + 集中配置(100 测试)**

原先 seedance.py 一个文件里混了 HTTP 传输、鉴权、厂商 DTO、状态映射与视频业务,
接第二个火山接口(图片/语音)时只能整段再抄一遍。按依赖单向向内重切四层:

    config.py → assembly.py → providers/ → domain/

- **domain/generation.py**:与服务商无关的领域模型,对厂商与 httpx **零依赖**
  ——「换供应商不动上层」的支点。判断归属的准绳是「换一家供应商它还在吗」:
  Retry/UserFacing 描述 Worker 与客户端该怎么做,故在领域层;expired 状态、
  QuotaExceeded 这类码是火山特有的,故在适配层。GenerationResult 拒绝
  「FAILED 但无 Failure」与「SUCCEEDED 但无 asset_url」——半个失败态放给上层
  只会让它盲目重试或盲目放弃。
- **providers/base.py + registry.py**:能力抽象与 provider_code 分发,对具体
  供应商零依赖。registry 拒绝重复注册(静默覆盖会让「哪个实例在处理任务」
  取决于注册顺序)。
- **providers/volcengine/** 拆四块:client.py(HTTP 传输与鉴权,**不认识
  「视频」**,以后接火山图片/语音接口复用它——这是拆它的全部理由;并修正连接
  归属:只关自己创建的连接池)、errors.py(火山码 → 领域 Failure,补 message
  透传)、models.py(厂商 DTO 与常量,base URL 从 config 收拢至此)、
  video.py(仅视频业务)。
- **config.py**:集中配置。非法值响亮失败而不静默回落;厂商区间
  [3600,259200] 启动时校验;密钥绝无默认值。另加 .env.sample。
- **assembly.py**:装配根,唯一同时知道配置与具体供应商的地方。缺凭证的供应商
  被跳过而非让启动失败。
- **tests/test_layering.py**:用 AST 断言架构约束本身。分层的价值全在「换供应商
  不动上层」,而这个性质只要有人无意加一行 import 就会悄悄失效——靠纪律守不住,
  靠测试才守得住。写完它当即抓到一处真问题:厂商 base URL 硬编码在 config.py。

测试 58 → 100,全部离线。另两处新行为:succeeded 但缺 video_url 判为可重试失败;
asset_url 随 URL 给出 24 小时时效,使上层没有理由把厂商地址当永久存储。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 12:19:50 +08:00
lixi 4762358f54 fix(security): 凭证扫描放行依赖注入传参 + 四仓口径更正
CI / ai-gates (push) Failing after 29s
`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>
2026-09-20 12:18:18 +08:00
lixi c6e8dddc8d feat(seedance): 火山方舟视频生成客户端与错误分类(58 测试,全离线)
CI / ai-gates (push) Successful in 31s
按用户提供的三份官方文档适配 Seedance 视频生成:提交
(POST /contents/generations/tasks)与查询(GET .../tasks/{id})。
取证记录见 patbond-doc docs/development/providers/volcengine-seedance.md。

**开工前问的五项,两项与原假设不同:**

1. **不支持幂等键**。请求参数全集里没有任何幂等字段。故 ADR-023 §4 设想的
   「把我方 idempotency_key 透传为厂商幂等头」对本供应商不成立,「提交超时后
   不知道有没有扣费」只能由 max_attempts 兜住——如实登记,不假装解决。
   两点减轻:提交是异步的(只入队即返回,超时概率低);且厂商支持 callback_url,
   孤儿任务会通过回调把 ID 送回来,故回调在此供应商上还是**孤儿回收通道**,
   不只是延迟优化——这是 ADR-023 §4「回调仅预留」之外的新论据,值得复评。
2. **产物是有效期 24 小时的 URL**(2.5 另有 100 次下载上限)。因此厂商 URL
   **不能当存储用**:存进 media.assets.external_url 会在 24h 后变死链。
   **这对挂起中的存储决策是硬约束**——无论选本地还是云,都必须在 24 小时内
   把文件复制到我们自己的存储。

其余三项:查询接口只保留 7 天记录;无明确频率数值,但官方 SDK 示例用 10s 轮询
(我们的 3s/10s/30s 阶梯与之兼容)、客户端截止 30 分钟(建议据此把 ADR-023 §10
的视频客户端超时从 15~20 分钟对齐到 30 分钟);execution_expires_after 范围
[3600, 259200],实现取下限 1 小时(厂商默认 48h 对交互式应用过长,且无法更短)。

**两个文档自身的问题,实现按宽的那份:**

- **状态枚举不一致**:查询文档列 5 个(queued/running/cancelled/succeeded/
  failed),创建文档的回调一节额外列 **expired**。只按查询文档实现会让超时
  任务掉进未知分支。按 6 个实现,expired 视为**可重试**失败(是超时不是内容
  问题),且厂商 expired 时不一定给 error.code 故单独分类。
- **QuotaExceeded 一码两义**:同一 code 既表示「排队任务数超限」(等几秒)
  又表示「额度耗尽」(等到明天或充值)。按 code 一刀切必然有一半场景的文案
  和出口是错的;只能按 message 判别,两条用例都在测试里钉住。

**顺带拿到两个免费收益**:last_frame_url 可直接作视频封面(iteration-4/03 R13
把缩略图列为视频成本项,实际免费);查询返回 ratio/resolution/framespersecond/
duration,**Worker 写产物时维度与时长都在手里、无需解码**——widthPx/heightPx
债在 AI 产物上可零成本修掉。

一处精度陷阱已处理:返回的 duration 是「帧数/24 向下取整」的约数(133 帧实际
5.54s 返回 5),且 duration 与 frames 只返回其一。故优先按 frames/fps 精确换算,
只有 duration 时退回整数秒并标注为约数,缺失时返回 None 而非编 0。

取消语义已查明:厂商**只支持取消 queued 状态**的任务,故我方取消已 running
的任务无法向厂商传导,**仍会计费**。ADR-023 §6 关于取消的说明据此可定稿。

测试 58 项(错误分类 32 + 客户端 26),全部离线用 httpx MockTransport,
夹具响应体逐字取自官方文档示例以免字段名对不上。

过程中防泄漏钩子拦下了测试夹具里的 `api_key="test-key"`——规则是对的(这正是
真实密钥最常见的泄漏形态),故改用 ALLOW 清单认可的占位词,未放宽规则。

未实现:产物转存(阻塞于存储决策)、HTTP 服务端点与 openapi-internal.yaml。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 11:51:37 +08:00
lixi 6990709124 chore: 建仓门禁四件套——CI、防泄漏脚本与钩子、gitignore、职责边界 README
CI / ai-gates (push) Successful in 47s
patbond-ai 纳入发布语义(ADR-023 §12)。本仓是四仓中唯一直接持有 AI
服务商凭证的仓库,因此不得成为唯一没有防泄漏门禁的仓库——建仓即配齐,
不等代码落地。

- .gitea/workflows/ci.yml:ai-gates 作业,push(dev) + pull_request 双触发,
  与另三仓同构。Secret scan 为第一个 step 且先于任何依赖安装。
  pip 走腾讯云镜像(与 patbond-doc 装 mkdocs 同一策略)。
  门禁命令 ruff format --check / ruff check / pytest,对应 flutter 仓的
  dart format + analyze + test 三步。requirements*.txt 落地前只装工具本身,
  使 CI 自建仓第一天即为绿;pytest 无测试时退出码 5 视为通过。
- scripts/check-secrets.sh:四仓同构副本,md5 640994c12bfa78b4512e872d709ed685。
  已覆盖 AI 服务商密钥形态(sk-ant- / sk- / sk-proj- / sk-svcacct-)与
  api_key/auth_token 实值赋值——本仓正是这些规则的主要适用对象。
- scripts/hooks/pre-commit:与另三仓同构。四仓 core.hooksPath 均已启用,
  ADR-023 §12 登记的「防泄漏第一层从未生效」至此闭环。
  实测验证:含 5 种真实凭证形态的 .env 夹具被钩子拦下,提交未产生。
- .gitignore:Python 产物与工具缓存;凭证载体(.env / credentials* / *.pem
  / *.key)一律排除,占位样例走 .env.sample(对应脚本的 DENY_NAME_OK)。
- README.md:职责边界(不访问数据库、不持有对象存储凭证、对上游同步异步
  一律对外呈现为异步)、与 Java 侧的接口约定(REST 非 gRPC、独立
  openapi-internal.yaml、服务身份 JWT 独立密钥对、请求体不含 userId)、
  本地门禁命令与凭证纪律。细节以 ADR-023 为准。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:42:34 +08:00
lixi 172f7d7537 first commit 2026-09-17 15:28:15 +08:00