Compare commits
10 Commits
8f97e34900
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cc538e827f | |||
| de4e02f805 | |||
| 8cf8ba41c9 | |||
| 3f3a5cc50b | |||
| 0b0317415b | |||
| 296d454d30 | |||
| 39cf422f89 | |||
| cd87437abd | |||
| 126b238087 | |||
| c830799fa0 |
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# mkdocs 构建产物。此前仅靠机器级 ~/.gitignore_global 屏蔽,
|
||||||
|
# 换机器或在 CI 中一次 `git add -A` 会提交上百个构建产物(ADR-023 §12)。
|
||||||
|
/site/
|
||||||
|
|
||||||
|
# 本地与编辑器
|
||||||
|
*.log
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# 凭证载体一律不入库(ADR-021)。真实值只存在于被忽略的文件中,
|
||||||
|
# 占位样例走 *.sample / *.example(check-secrets.sh 的 DENY_NAME_OK)。
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.sample
|
||||||
|
!.env.example
|
||||||
|
credentials*
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Patbond Doc
|
||||||
|
|
||||||
|
Patbond 的契约、架构决策与开发过程文档。用 [MkDocs](https://www.mkdocs.org/) 构建成站点。
|
||||||
|
|
||||||
|
**本仓是四仓中唯一的「正典」所在**:契约、数据模型、架构决策以此为准。其余仓库中的同类内容都是副本或实现,冲突时以本仓为准。
|
||||||
|
|
||||||
|
## 权威文档在哪
|
||||||
|
|
||||||
|
| 文档 | 位置 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 对外契约 | `docs/api/openapi.yaml` | 客户端可见的全部端点。升版须与 `patbond-api` 内的四份快照同步 |
|
||||||
|
| 数据模型正典 | `docs/database/patbond_postgresql.sql` | 已评审的目标模型。Flyway 迁移从中逐列摘取,任何偏离都必须在迁移文件头写明原因 |
|
||||||
|
| 架构决策 | `docs/architecture/decisions.md` | ADR-001 起累积。**改动实现前先读相关 ADR** |
|
||||||
|
| 后端模块结构 | `docs/architecture/backend-modules.md` | 各模块职责与边界 |
|
||||||
|
| 发布记录 | `docs/development/releases.md` | 每次发布追加一条,含门禁结果与生效的流程纪律 |
|
||||||
|
| 功能清单 | `docs/development/feature-checklist.md` | 逐条功能的完成状态 |
|
||||||
|
| Git 与门禁 | `docs/development/git-workflow.md` | 分支、提交、CI 门禁与凭证防泄漏规范 |
|
||||||
|
| 真机验证清单 | `docs/development/device-verification.md` | 桌面不可替代的验证项,含逐项步骤与通过标准 |
|
||||||
|
| 服务器暴露面 | `docs/development/server-exposure.md` | 生产主机的端口、常驻服务与处置记录 |
|
||||||
|
| 迭代报告 | `docs/development/iterations/` | 按迭代归档。**存档不回改**,结论失效时追加带日期的修正注记 |
|
||||||
|
|
||||||
|
## 本地构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install mkdocs
|
||||||
|
mkdocs serve # http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地门禁
|
||||||
|
|
||||||
|
与 CI 同一套命令(`.gitea/workflows/ci.yml`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh scripts/check-secrets.sh --all # 凭证防泄漏(ADR-021)
|
||||||
|
mkdocs build --strict # 必须 exit 0 且零 warning
|
||||||
|
```
|
||||||
|
|
||||||
|
`--strict` 会把死链和未挂导航的孤立文档判为失败,所以**新增文档必须同时挂进 `mkdocs.yml` 的 `nav`**。
|
||||||
|
|
||||||
|
**每人每仓启用一次** pre-commit 钩子:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config core.hooksPath scripts/hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/check-secrets.sh` 是四仓同构副本,调整规则时四仓同步提交。
|
||||||
|
|
||||||
|
## 写作纪律
|
||||||
|
|
||||||
|
- **常设文档不写死本机绝对路径**,用 `<你的工作区>/patbond-xxx` 之类的参数化写法;迭代报告存档豁免。
|
||||||
|
- **迭代报告是时点存档,不回改**。结论后来被推翻时,在原文下方追加带日期的修正注记并指向新依据——改掉原文等于销毁历史记录,而追加注记同样能掐断转述链。
|
||||||
|
- **不要引用别的报告的转述结论**。本项目已多次因此误判:某报告写「无 nickname 字段」实指「契约未暴露」,被下游当成「缺列需迁移」,规模预估高估一整档。凡「XX 不存在 / XX 缺失」的判断,请打开原始 SQL、原始契约、原始源码确认,并注明核实的文件与行号。
|
||||||
|
- `site/` 是构建产物,已被 `.gitignore` 排除,不要提交。
|
||||||
|
|
||||||
|
## 相关仓库
|
||||||
|
|
||||||
|
| 仓库 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| `patbond-api` | Spring Boot 多模块后端 |
|
||||||
|
| `patbond-flutter` | Flutter 客户端 |
|
||||||
|
| `patbond-ai` | AI 生成服务(Python),只调服务商 API |
|
||||||
@@ -12,10 +12,13 @@ patbond-api(Maven 多模块,Spring Boot 3.5 + JDK 17)
|
|||||||
├── patbond-auth 认证服务 :8081
|
├── patbond-auth 认证服务 :8081
|
||||||
├── patbond-user 用户服务 + 埋点 :8082 ← Flyway 迁移链唯一持有者
|
├── patbond-user 用户服务 + 埋点 :8082 ← Flyway 迁移链唯一持有者
|
||||||
├── patbond-pet 宠物健康档案服务 :8083 ← M2 新增(ADR-009)
|
├── patbond-pet 宠物健康档案服务 :8083 ← M2 新增(ADR-009)
|
||||||
└── patbond-community 社区服务 :8084 ← M3 新增(ADR-017)
|
├── patbond-community 社区服务 :8084 ← M3 新增(ADR-017)
|
||||||
|
└── patbond-creation AI 生成编排服务 :8085 ← M4 新增(ADR-023)
|
||||||
|
|
||||||
|
patbond-ai(独立仓库,Python) ← M4 新增(ADR-023),只调 AI 服务商 API
|
||||||
```
|
```
|
||||||
|
|
||||||
部署形态:docker compose 六容器(postgres:18 + **MinIO 对象存储**(ADR-016,自托管、私有桶)+ auth + user + pet + community),应用容器无状态(ADR-007)。
|
部署形态:docker compose 六容器(postgres:18 + **MinIO 对象存储**(ADR-016,自托管、私有桶)+ auth + user + pet + community),应用容器无状态(ADR-007)。**`patbond-creation` 尚未入 compose**——其 Worker 依赖 `patbond-ai` 与对象存储方案,两者均未落地。
|
||||||
|
|
||||||
## 模块职责
|
## 模块职责
|
||||||
|
|
||||||
@@ -57,11 +60,28 @@ patbond-api(Maven 多模块,Spring Boot 3.5 + JDK 17)
|
|||||||
- media 上传流程不在本模块(ADR-017:实现在 patbond-user,社区侧只做 asset 只读校验)
|
- media 上传流程不在本模块(ADR-017:实现在 patbond-user,社区侧只做 asset 只读校验)
|
||||||
- **当前状态**:第一波骨架(T3-02)已落地;业务端点随 M3 后续波次按契约实现
|
- **当前状态**:第一波骨架(T3-02)已落地;业务端点随 M3 后续波次按契约实现
|
||||||
|
|
||||||
|
### patbond-creation(AI 生成编排域,:8085,M4 新增)
|
||||||
|
|
||||||
|
- **编排生成任务,不调用 AI 服务商**:上游调用在独立的 `patbond-ai`(Python)仓库,经 REST 到达(ADR-023 §2)
|
||||||
|
- 只读写 `creation` schema(数据表由 V6 建,含两处刻意偏离目标模型:`input_asset_id` 改可空以支持文生图、新增 `first_attempt_at` 使排队时长与执行耗时可分离)
|
||||||
|
- `/api/v1/creation/**` 客户端端点 + **数据库租约队列 Worker**(`FOR UPDATE SKIP LOCKED` + 租约列,**不引入 Redis/MQ**,ADR-023 §3)
|
||||||
|
- **不直连对象存储**:产物上传凭证来自 patbond-user 的内部媒体接口(预签名 PUT),由生成方直传——ADR-017 的边界不动,且 `patbond-ai` 也因此零对象存储凭证
|
||||||
|
- **调用 patbond-ai 用服务身份 JWT,独立密钥对**(不转发用户 token)。选独立密钥对的依据:现有 `JwtVerifier` 只验签名、不校验 `aud`/`iss`,而用户 token 无 `aud` claim,复用密钥对会让四个模块把服务票当合法用户 token 接受(ADR-023 §5)
|
||||||
|
- **当前状态**:骨架已落地(端点、队列仓储、Worker 三通道、租约回收器、provider 适配层接口)。**刻意未实现且会响亮失败**:provider 两个实现位(待服务商文档确认五项)、预签名上传目标获取与产物入 `media.assets`(取决于**挂起中的对象存储决策**,ADR-023 §7)
|
||||||
|
|
||||||
|
### patbond-ai(AI 生成服务,独立仓库,Python,M4 新增)
|
||||||
|
|
||||||
|
- 不属于 `patbond-api` 多模块,是**第四个仓库**,已纳入发布语义(ADR-023 §12)
|
||||||
|
- 只负责调用 AI 服务商 API(视频 Seedance / 图片 gpt-image-2);**不访问数据库**,任务状态的唯一事实来源是 Java 侧的 `creation.generation_jobs`
|
||||||
|
- **对上游同步或异步一律对外呈现为异步**:`submit` 立即返回任务 id,同步型服务商在其内部以后台任务承载长连接。这样 Java 侧无长连接占线程,且 `provider_request_id` 立即可得——超时不再产生「是否已扣费」的歧义
|
||||||
|
- 内部契约为该仓内独立的 `openapi-internal.yaml`,**不进四模块快照锁**,也不写入对外 `openapi.yaml`
|
||||||
|
- **是四仓中唯一持有 AI 服务商凭证的仓库**,故 `check-secrets.sh` 已补 `sk-ant-` / `sk-` / `api_key` 等形态
|
||||||
|
|
||||||
## 关键纪律
|
## 关键纪律
|
||||||
|
|
||||||
1. **契约先行**:所有对外端点以 `patbond-doc/docs/api/openapi.yaml` 为唯一事实源,新接口先冻结契约再实现(M2 起)。
|
1. **契约先行**:所有对外端点以 `patbond-doc/docs/api/openapi.yaml` 为唯一事实源,新接口先冻结契约再实现(M2 起)。
|
||||||
2. **单迁移链**:数据库迁移只进 patbond-user,新表按域用 schema 前缀区分(identity / platform / pet_health / …)。
|
2. **单迁移链**:数据库迁移只进 patbond-user,新表按域用 schema 前缀区分(identity / platform / pet_health / community / creation)。注意「谁拥有 schema」与「迁移文件放哪」是分开的——pet/community/creation 的业务代码各在自己模块,迁移文件全在 patbond-user,`flyway_schema_history` 只有一份。
|
||||||
3. **服务间调用**:MVP 阶段 Feign 静态 URL 直连、无注册中心(ADR-002,Nacos 已移除);微服务化阶段再引入。
|
3. **服务间调用**:MVP 阶段 Feign 静态 URL 直连、无注册中心(ADR-002,Nacos 已移除);微服务化阶段再引入。Java↔Java 用 `InternalAuthFilter` 的共享 token,Java↔Python 用服务身份 JWT——两套并存是有意选择,见 ADR-023 §5。
|
||||||
4. **配置**:敏感配置走 `.env` / `application.yml`(gitignore)+ `.sample` 模式,环境变量注入(`PATBOND_DB_URL` 等)。
|
4. **配置**:敏感配置走 `.env` / `application.yml`(gitignore)+ `.sample` 模式,环境变量注入(`PATBOND_DB_URL` 等)。
|
||||||
5. **测试**:集成测试一律 Testcontainers(postgres:18,ADR-006/008),每模块交付 `./mvnw clean test` 必绿。
|
5. **测试**:集成测试一律 Testcontainers(postgres:18,ADR-006/008),每模块交付 `./mvnw clean test` 必绿。
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,8 @@
|
|||||||
|
|
||||||
**决策**(2026-09-08):M3 MVP = 图片媒体上传闭环 + 帖子草稿/发布/删除 + 公共 Feed 游标分页 + 单层评论 + 点赞/收藏幂等 + Flutter 三页(home/create/post_detail)替换 demo 与乐观更新回滚。关注做最小数据接口(follow/unfollow + 数量);**话题首版剪出**;评论仅单层不做楼中楼。视频后置。
|
**决策**(2026-09-08):M3 MVP = 图片媒体上传闭环 + 帖子草稿/发布/删除 + 公共 Feed 游标分页 + 单层评论 + 点赞/收藏幂等 + Flutter 三页(home/create/post_detail)替换 demo 与乐观更新回滚。关注做最小数据接口(follow/unfollow + 数量);**话题首版剪出**;评论仅单层不做楼中楼。视频后置。
|
||||||
|
|
||||||
|
> **⚠️ 「视频后置」一项已被 ADR-023 取代(2026-09-18)**:M4 视频与图片同批交付。本条其余裁剪(话题首版剪出、评论单层、区域系统属 M5)继续有效。
|
||||||
|
|
||||||
## ADR-019 写接口幂等形态按域选择
|
## ADR-019 写接口幂等形态按域选择
|
||||||
|
|
||||||
**决策**(2026-09-08):二元状态互动(点赞/收藏/关注)用 **PUT/DELETE 语义幂等**(重复调用同终态,无键管理);创建型写入(发帖/评论/媒体登记)用**表内幂等列(request_hash)**。M2 的 Idempotency-Key 键派生机制在 pets 域维持不变,不回改。
|
**决策**(2026-09-08):二元状态互动(点赞/收藏/关注)用 **PUT/DELETE 语义幂等**(重复调用同终态,无键管理);创建型写入(发帖/评论/媒体登记)用**表内幂等列(request_hash)**。M2 的 Idempotency-Key 键派生机制在 pets 域维持不变,不回改。
|
||||||
@@ -193,3 +195,117 @@
|
|||||||
- **获赞总数走读侧实时聚合**(`SUM(like_count)` over 本人未删帖),**不引入新冗余列**:写侧维护成本高于读侧聚合收益,且数据量级远未到瓶颈。端点为新增的 `GET /api/v1/me/community-stats`,不并入既有 `follow-stats`(后者主体是「某用户的关注数」,混入「我的获赞」会造成主体歧义)。
|
- **获赞总数走读侧实时聚合**(`SUM(like_count)` over 本人未删帖),**不引入新冗余列**:写侧维护成本高于读侧聚合收益,且数据量级远未到瓶颈。端点为新增的 `GET /api/v1/me/community-stats`,不并入既有 `follow-stats`(后者主体是「某用户的关注数」,混入「我的获赞」会造成主体歧义)。
|
||||||
|
|
||||||
**版本号**:本迭代交付按 **v0.4.0** 发布(新增端点与字段属功能增量,非纯补丁)。
|
**版本号**:本迭代交付按 **v0.4.0** 发布(新增端点与字段属功能增量,非纯补丁)。
|
||||||
|
|
||||||
|
## ADR-023 M4 AI 创作的架构、边界与关键决策
|
||||||
|
|
||||||
|
**决策**(2026-09-18,开工分析八份报告 + 与用户逐项讨论后拍板)。本 ADR 覆盖 M4 全部跨角色决策;单角色内的细节决策留在各自报告中。
|
||||||
|
|
||||||
|
### 1. 范围:视频与图片同批
|
||||||
|
|
||||||
|
- **视频与图片同批交付**,**取代 ADR-018 的「视频后置」**。数据库侧本无障碍:目标模型的 `creation` 三表自始按 `media_kind IN ('image','video')` 建模、目录按 `(id, media_kind)` 复合唯一键约束,`media.assets.kind` 的 CHECK 亦已含 `'video'`、`duration_ms` 已在。
|
||||||
|
- 因此视频的成本集中在**契约与客户端**,不在 schema:契约 `kind` 枚举加 `video`、Flutter 需引入播放器依赖(现为零)、缩略图与时长呈现(`MediaType.video` 已备未启用)。`iteration-4/03` R13 评级 🟠 中高。
|
||||||
|
- **供应商**:AI 视频 = 火山引擎 Seedance;AI 图片 = gpt-image-2。生产服务器为腾讯云轻量、**无 GPU**,自托管模型不可行,故一律走云 API。
|
||||||
|
- **`patbond-ai` 先完成两类 provider 的接口适配**,客户端视频路径随契约扩展跟上。
|
||||||
|
- `iteration-4/05` 的目录呈现为**数据驱动**(目录返回哪些 `media_kind` 就渲染哪些分组),故契约扩到 video 时 UI 自动长出第二段,无需重画。
|
||||||
|
|
||||||
|
### 2. 服务划分:patbond-ai 为独立 Python 仓,Java 持有队列
|
||||||
|
|
||||||
|
- **新建 Java 模块 `patbond-creation`(:8085)**,前缀 `/api/v1/creation/**`,承载客户端端点、Worker、Python 客户端与 `generation_jobs` 读写。沿 ADR-009 / ADR-017 两次先例(新模块 + 共库 + patbond-user 单迁移链)。
|
||||||
|
- **`patbond-ai` 为独立仓、Python 实现**,只负责调用 AI 供应商 API。**不访问数据库**:任务状态的唯一事实来源是 Java 侧的 `creation.generation_jobs`。
|
||||||
|
- **理由(为何不让 Python 直接消费队列)**:目标模型已把租约、退避、优先级、幂等、provider 去重全部设计在 Java 侧表中(`lease_owner` / `lease_expires_at` / `next_attempt_at` / `attempt_count` / `max_attempts` / `priority` 六列 + 两条与出队/回收语句逐列对齐的偏索引)。让 Python 也轮询该表等于用两种语言实现同一套租约逻辑,并使事实来源分裂。
|
||||||
|
- **迁移链归属不变**:V6 仍放 `patbond-user`。这是既有约定而非新决策——V3(`pet_health`)与 V5(`community`)的业务代码在各自模块,迁移文件全部在 `patbond-user`。
|
||||||
|
- **共享物**:`patbond-creation` 需复制 3 件(JWT 资源侧校验、`UuidV7`、`RequestHashes`)。**存储适配层不复制**,见 §4。是否上提 `patbond-common` 作为独立前置工单评估,退路是各抄一份。
|
||||||
|
|
||||||
|
### 3. 队列载体:Postgres `FOR UPDATE SKIP LOCKED` + 租约,不引入 broker
|
||||||
|
|
||||||
|
- **不引入 Redis、不引入 MQ**,偏离 `development-plan.md:205` 的 RabbitMQ 设想。
|
||||||
|
- 三条理由:① 目标模型已把租约队列设计完(含现成领取 SQL),换中间件等于让这批列作废、推翻已评审模型;② 实测 `docker-compose.yml` 仅 postgres/minio/auth/user/pet/community,**无 Redis 无 MQ**,引入即净增容器与运维面——与 ADR-002 移除 Nacos 同一判例;③ 任务状态真值保持单一(Postgres),无 dual-write,「Worker 重启不丢任务」由租约回收器天然满足。
|
||||||
|
- 缺口:目标模型给了索引但没给回收器代码,**租约超时回收器为 M4 新增工单**。
|
||||||
|
|
||||||
|
### 4. Java↔Python 边界
|
||||||
|
|
||||||
|
- **统一异步,无论上游同步还是异步**。Seedance 类为异步(提交 + 查询),gpt-image 类为同步长连接;该差异**不得渗透到客户端契约与 job 表**——`generation_jobs` 状态机本就是异步形状,`iteration-4/05` 的等待态设计亦只有一条路径。Python 收到 submit 立即返回任务 id,同步 provider 在 Python 内部以后台任务承载长连接。
|
||||||
|
- **收益**:Java 侧无长连接占线程(轻量服务器关键);`provider_request_id` 立即可得,`uq_generation_jobs_provider_request` 唯一索引才真正生效;超时不再产生「是否已扣费」的歧义——同 id 重新轮询即可,而非重发。
|
||||||
|
- **双重扣费的残余风险如实登记**:Java 将自身 `idempotency_key` 透传 Python,Python 转发为上游幂等头。**上游不支持幂等头时风险窗口无法消除**,仅由 `max_attempts` 兜住并记录,不假装已解决。各供应商是否支持须逐一核对官方文档。
|
||||||
|
- **传输为 REST(JSON),不用 gRPC**。理由不是性能——瓶颈是上游数秒至数分钟的出图出片,序列化差异无意义。理由是**契约体系**:本项目全部契约纪律建在 OpenAPI 上(快照、计数断言、契约矩阵、逐迭代冻结),引入 `.proto` 等于第二套无门禁契约体系;而本迭代开工审计恰好证明「没有门禁的契约必然漂移」(四模块快照锁实际不存在,白名单 42/41 之误四份文档一路不可见)。需要类型安全时**从 OpenAPI 生成 Python 客户端**,保持单一来源。
|
||||||
|
- **内部契约独立成文**:`patbond-ai` 仓内单独一份 `openapi-internal.yaml`,独立版本号,**不进四模块快照锁**,也**不写入面向客户端的 `openapi.yaml`**——否则内部适配端点会污染公开契约的路径/操作计数。
|
||||||
|
- **产物落盘走 presigned PUT**:Java 经 `patbond-user` 新增的 `/internal/media/**` 取得预签名上传凭证,交给 Python 直传对象存储,再由 Java 核实并建 `media.assets` 行。这正是客户端现用的两步上传,Python 站到客户端位置。
|
||||||
|
- 由此**服务端无需新增 `putObject`/`getObject` 原语**:`ObjectStorage` 已有 `presignPut` 与 `presignGet`。开工报告中「服务端零对象写能力 = M4 缺失地基」的**推论据此更正**——缺 `putObject` 是事实,但服务端需要的是签发上传凭证,该能力早已存在。
|
||||||
|
- 附带收益:**Python 零对象存储凭证**,少一处凭证面。`MediaProperties` 已区分 `endpoint`(容器内)与 `publicEndpoint`(客户端可达),预签名 URL 的 host 分发本就分离。
|
||||||
|
- **轮询为保证,回调为优化**:绝不可只靠回调(网络抖动、任一侧重启、乱序都会使任务永久卡在 running)。**M4 只做轮询**:submit 请求体预留可选 `callbackUrl` 字段但不启用,将来启用无需契约变更。若日后启用,回调鉴权用 **Java 下发的一次性 job 绑定能力票**(Python 原样带回),使 Python 侧无需任何私钥。
|
||||||
|
- **服务端轮询节律**(Java → Python,Docker 网络内):前 60 秒 3s、60s~5min 10s、5min 后 30s。**租约 TTL 必须大于单次轮询间隔**(设 60s,每次轮询顺带续租),否则会出现「任务仍在跑但租约过期被他人抢占」。`ix_generation_jobs_running` 即为「捞出待轮询的 running 任务」而建。
|
||||||
|
|
||||||
|
### 5. 鉴权:服务身份 JWT,独立密钥对
|
||||||
|
|
||||||
|
- Java→Python **签发服务身份 JWT,使用独立密钥对**;**禁止转发用户 JWT**(Worker 异步执行时用户 token 可能已过期,且不应让 Python 持有用户凭证)。
|
||||||
|
- Claim 约定:`iss=patbond-api`、`aud=patbond-ai`、`sub=service:creation-worker`、`typ=service`、短 `exp`(5 分钟)、带 `jti`。Python 侧强制校验 `aud` 与 `typ`。
|
||||||
|
- **选独立密钥对的事实依据**:现有 `JwtVerifier` **仅验签名,不校验 `aud` 也不校验 `iss`**,而 `JwtSigner` 签发的用户 token **不含 `aud` claim**(仅 `sub`/`iss`/`sessionId`/`exp`)。若服务票复用同一密钥对,user/pet/community(很快加 creation)四个模块都会把它当合法用户 token 接受——`sub` 非 UUID 大概会在后续解析时失败,但那是**意外挡住而非设计挡住**。独立密钥对无需改动任何已发布的验证器。
|
||||||
|
- **两套内部鉴权并存的说明**:Java↔Java 的既有内部通道继续用 `InternalAuthFilter` 的共享 token(header 携带,未配置则 `/internal/**` 全拒;该 token 在 2026-09-11 安全事件中轮换过)。Python 那一跳改用 JWT 是有意选择更强机制(有效期、`aud`、`jti`,无长期共享秘密)。**本迭代不改造既有 Java↔Java 通道**,避免范围蔓延。
|
||||||
|
- **数据最小化**:请求体不传 `userId`,除非 Python 确有需要。Python 完成工作只需 `jobId` + 生成参数 + 预签名上传 URL。
|
||||||
|
|
||||||
|
### 6. 数据模型:V6 两处偏离,V7 待供应商文档
|
||||||
|
|
||||||
|
- V6(`creation` schema 三表 + 14 索引 + 3 触发器 + 补回 `posts.generation_job_id` 外键)已落地,**零跨 schema 外键需裁剪**——首个不必砍外键的 baseline 迁移。
|
||||||
|
- **偏离一:`input_asset_id` 改可空**(目标模型 NOT NULL)。参考图为**选填**,主流生图与生视频均同时支持文生与参考生,由用户是否上传参考图选择路径;原 NOT NULL 会使文生图无法表达。**未发明 `requires_input` 标记**(不超出已评审模型),「某模型是否必须有输入」由应用层查目录判断。
|
||||||
|
- **偏离二:新增 `first_attempt_at`**(目标模型无此列)。`iteration-4/06` §4.2 的指标口径要求分离排队时长与纯执行耗时,而目标模型无法表达——其 `ck_generation_jobs_state` 强制 `queued` 分支 `started_at IS NULL`,故失败重排队必然擦除首次尝试时间。该列仅首次进入 `running` 时写入、永不覆盖,并**刻意排除在 `queued` 分支的置空要求之外**;`ck_generation_jobs_first_attempt` 钉住其与 `attempt_count` 的双向不变式。
|
||||||
|
- **V7 目录种子暂缓**:种子需填 `provider_code` / `provider_model_name` / 版本快照等真实取值,须待 Seedance 与 gpt-image-2 官方文档到位后再写,否则即为编造数据。沿 V3/V4「结构与种子分离」先例。
|
||||||
|
- **`posts.region_id` 仍为裸列**,区域系统属 M5(ADR-018 该项仍有效)。
|
||||||
|
|
||||||
|
### 7. 对象存储:决策挂起,现有实现不动
|
||||||
|
|
||||||
|
- **本地 MinIO 与云对象存储之间的选择本迭代不作决定**,理由是分布式、磁盘容量、备份恢复等考量尚未收敛。**现有本地存储代码一行不动、不删除。**
|
||||||
|
- 已核实的两点供决策参考:
|
||||||
|
- **`media.assets` 已同时支持两种模型**:`storage_type` 的 CHECK 为 `IN ('object','external')`,object 用 `bucket`+`object_key`,external 用 `external_url`,`ck_media_location` 强制二选一。**两种存储可共存于同一张表**,允许存量资产留 `object`、新资产走 `external` 的渐进迁移,**任一选择均无需 DDL 迁移**。
|
||||||
|
- **现状是宿主机本地磁盘**:MinIO 数据落在 Docker 命名卷 `minio-data`,无任何云对象存储。且 `server-exposure.md` §2 显示生产服务器**只运行 gitea/nginx/文档站/act_runner/dockerd/sshd**,六容器栈(含 MinIO)当前**仅在开发机运行**。
|
||||||
|
- **由此挂起两项**:① M4 是否将六容器栈部署到服务器(不部署则交付仍为「本机可验、真机不可达」);② MinIO 的 9000 端口既不在防火墙放行范围(仅 22/80/443)也无 nginx 路由,**预签名 URL 目前对真机不可达**——这构成客户端媒体链路的上线前置,也是「媒体上传弱网」真机验证长期挂起的深层原因之一。**注意 M4 的 AI 产物上传不受此影响**:Python 在容器网络内走内部 endpoint。
|
||||||
|
|
||||||
|
### 8. 埋点与实验
|
||||||
|
|
||||||
|
- **`eventVersion` 定型为「每个事件自身的 props schema 版本」**,否决「字典世代」读法。决定性依据是**可修复性不对称**:此读法下客户端现行硬编码 `1` 本就正确(零改动零回填);「字典世代」读法下全部历史行皆错且不可修复(`server_ts` 无法反推事件当初属于哪代字典)。配套 bump 规则:新增可选属性不 bump;删属性、改语义单位、改既有枚举含义、改触发时机必须 bump,新旧版本在字典中并列为两条。
|
||||||
|
- 落地时白名单 key 改 `(name, version)` 二元组,并新增逐条拒绝原因 `unknown_event_version`——**必须置于逐条校验路径,不得放在 DTO 校验**,否则重犯整批连坐错误(见下条)。
|
||||||
|
- **服务端生成侧不建第二条埋点管道**:以 `creation.generation_jobs` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接。`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表,放宽这两个 NOT NULL 是不可逆的反向操作。
|
||||||
|
- **客户端埋点整批丢弃缺陷须修**:`TrackEventsRequest` 的 `@Valid` 级联至列表元素,任一元素校验失败即整请求 400,客户端进而判为永久拒绝并**整批删段**。这是**与平台无关的毒丸批次放大器**(任一条事件的任一字段可毒死整批 50 条),桌面 `platform=linux` 100% 丢事件只是其最显眼的表现。`analytics_service.dart` 注释所称「逐条 rejected、不影响客户端」与实现相反,且该错误表述已转述进 5 处文档。**枚举排除 linux 是设计意图,不动枚举;修客户端本地短路 + 调试覆盖开关。**
|
||||||
|
- **A/B 前置状态重置为「5 绿 3 半」**(原记「6 绿 1 半」):分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**,须作为前置工单而非既有资产。按旧口径排期会在实验启动日才发现要先写三个组件。
|
||||||
|
- **M4 首个「实验」为 A/A 基建验证空跑**(零产品风险,≥200 曝光即可暴露分流失衡与曝光漏报);真实 A/B 设计冻结存档,**放行条件挂流量而非挂日期**。
|
||||||
|
- **北极星首次出数改为事件驱动触发条件**(原 2026-09-21 日期承诺作废,其 W37 入队窗口已于 09-13 关闭,无补救可能):`T0` = 首个满足「≥1 名真实移动端用户产生生命周期首条 `health_record_create_succeeded`」的 ISO 周,首次出数 = `T0 + 8 天`后的首个周一。**明确否决**用桌面 override 或 curl 造数凑读数。另须把北极星 SQL 落成仓库内可执行载体——它目前只存在于报告正文。
|
||||||
|
- **补一个断言埋点白名单条数的测试**,列为 M4 前置:白名单实际 41 条而四份文档记 42(`experiment_exposed` 被重复计数),漏了整整一个迭代未被发现,根因正是 `EventDictionaryTest` 零条数断言。
|
||||||
|
|
||||||
|
### 9. 契约
|
||||||
|
|
||||||
|
- `openapi.yaml` **v1.4.0 → v1.5.0,纯增量**:零删除、零重命名、零必填收紧。「一键建草稿」复用既有 `POST /api/v1/posts`,**不新增路径**。
|
||||||
|
- **429 与配额拆为两个错误码**,不共用一码:配额耗尽等的是次日(呈现为 sheet 并给出出口),频率过快等的是数秒(呈现为 SnackBar);共用一码必然使一半场景的文案与出口错误。契约当前**完全没有 429、`Retry-After` 或任何响应头机制**(`components.headers` 为空),M4 为首次引入,同时兑付客户端 `Retry-After` 死分支的历史挂账。
|
||||||
|
- **两处配置扩展,零迁移**:`MediaProperties.allowedMimeTypes` 加视频类型(现仅 jpeg/png/webp)、`allowedPurposes` 加 AI 产物 purpose。`media.assets.purpose` 为裸 varchar 无 CHECK。
|
||||||
|
- **须一并修正契约内部既有矛盾**:`openapi.yaml:1228` 称「purpose 仅 `post_image`」与 `:3460` 的三值枚举冲突(M3.5 加值时漏改端点描述)。M4 再加值若不同时修,即为第三次漂移。
|
||||||
|
- **「AI 创作」标签不得由客户端声明或由 purpose 推断**:社区侧 `PostService.validateAssets` 只校验归属与 ready、不校验 purpose,故仅凭 purpose 或客户端标记可被任意图片伪造。AI 来源**须由服务端从 `generation_job_id` 派生**。
|
||||||
|
|
||||||
|
### 10. 客户端
|
||||||
|
|
||||||
|
- **进度反馈用自适应轮询**(1s×3 → 2s×5 → 3s,上限 5s),**不做 SSE/WebSocket**。决定性理由:现有网络层全部围绕「信封解包 + 401/40101 单飞刷新重放一次」构建,SSE 两者皆用不上,等于旁路整条既建鉴权链、形成第二套鉴权路径——而 M3.5 端口事故的根因正是「跨模块调用绕开既有接线纪律」。且任何推送方案都必须有「查状态」GET 兜底,有了它轮询已免费到手。
|
||||||
|
- **AI 对话类若进 M5 需重新评估流式**,本条不构成对流式的一般性否决。
|
||||||
|
- **硬超时按 `media_kind` 分档**,且**由服务端在任务响应中下发**(如 `estimatedTimeoutSec`)而非客户端硬编码:图片 5 分钟对视频不足,硬编码两个数会使换 provider 就要发版。
|
||||||
|
- **绝不承诺推送**:客户端无任何推送能力(无 WS/SSE/FCM)。等待页写「完成后通知你」会使用户锁屏等待而永不到达;正确表述为「回到 App 就能看到结果」。
|
||||||
|
- **不押注百分比进度**:`ck_generation_jobs_progress` 与状态机使 `queued` 时 progress 恒 0、`running` 时无下限约束,押注百分比的后果是最常见观感变成「0% 卡住」。主形态为阶段文字 + 不确定进度环,`progress>0` 才叠确定型数字;**展示已用时而非 ETA**(已用时是事实,ETA 是承诺,M4 给不出可信 ETA)。
|
||||||
|
- 须定义服务端五个 status 之外的**第六个 UI 态:僵死/超时**(`running` 超阈值),且它不是错误态。
|
||||||
|
|
||||||
|
### 11. 遗留搭车与优先级调整
|
||||||
|
|
||||||
|
- **`widthPx`/`heightPx` 列 M4 必修**,且**必须两侧同批**——只修一侧用户零感知。该债的三段事实:详情页客户端**已正确消费**(有 clamp,服务端一给即生效);Feed 卡片是**客户端硬编码 4:3 缺陷**(维度可用却被忽略);客户端**根本无法提供维度**(`CreateMediaUploadRequest` 契约无该字段)。M4 严重度上升的实质不是「更严重」而是**「从看不见变成看得见」**:结果页按真实构图渲染、发到 Feed 被裁成 4:3,是同一次会话内的直接前后对照(9:16 竖图裁掉约 58%)。而 AI 输出路径**不需要图片解码**,Worker 写产物时维度就在手里。
|
||||||
|
- **`SegmentedButton` 粉底降为 P2 品牌一致性债**(原记 P1 无障碍债):实测 `secondaryContainer #FFDAD2` + `onSecondaryContainer #5D4038` 对比度 **7.19:1 达标**。仍搭车修(根因与 M3.5-01 修 DatePicker 完全同构,主题层无 `segmentedButtonTheme` 亦无 `chipTheme`,一处补全修好 6 个使用点),但**不为它拖 M4 范围**。
|
||||||
|
- **「我的收藏与草稿」两页搭车**:后端与客户端仓库方法均已就绪且在测试覆盖内,**只缺 2 个页面**,为本次盘点投入产出比最高项。
|
||||||
|
- **必须与放开写侧同一提交修掉的数据损坏路径**:`post_compose_page.dart` 将恢复的 AI 草稿类目静默降级为 `general`。现无害(写侧被封),**M4 放开写侧后会把类目改错并写回服务端**。
|
||||||
|
- **`ai_creation` 与话题无关**:它只是 category 枚举值,与 topics 表零字段关联,M3 所提「话题随 `ai_creation` 一起做」的搭车理由不成立,话题继续挂起。
|
||||||
|
|
||||||
|
### 12. 门禁、安全与发布
|
||||||
|
|
||||||
|
- **必需状态检查上下文改为 `(pull_request)`**(已执行并复核生效):原为 `CI / backend-test (push)`,而 `ci.yml` 的 push 触发器限定 `branches: [dev]`,造成双重后果——门禁实际由「推 dev」满足使 **PR 侧检查红也能合并**,且**任何非 dev 分支 → main 的 PR 永久无法合并**(其 head 永不产生 `(push)` 状态),使 hotfix 短命分支纪律实际失效。**改动为替换而非追加**(追加不解除死锁)。建议以一次空 PR 验证,勿以正式发布为试验场。
|
||||||
|
- **M4 继续 dev 直推**:`push` 触发器只覆盖 dev,推 `feat/**` 不跑任何 CI,开分支等于自断反馈。仅 `spike/*`(探完不合并)与 `refactor/*`(rebase 后 ff-only 合回)开短命分支,且**均不得直接开 PR 到 main**。
|
||||||
|
- **`check-secrets.sh` 已补 AI 服务商密钥形态**(三仓同构):新增 `AK-ANTHROPIC`(`sk-ant-`)与 `AK-OPENAI`(`sk-`/`sk-proj-`/`sk-svcacct-`),`KEY-ASSIGN` 扩至 `api[-_]?(key|secret|token)` 与 `auth[-_]?token`。修复前 5 种真实形态全部漏网。**须先于任何 AI 凭证落地**——现已完成。
|
||||||
|
- **`patbond-ai` 纳入发布语义**:参与四仓 tag 同步、CI 门禁、防泄漏脚本与 E2E 回归。**它是唯一将存放 AI 凭证的仓,不得成为唯一没有防泄漏门禁的仓。**
|
||||||
|
- **契约跨仓漂移无 CI 兜底**:api 的 CI 只读 api 仓内文件、只验「快照↔守卫↔矩阵」内部自洽,**完全不校验与 doc 正典的字节一致性**。升版须按「doc 升正典单独提交 → 记录 md5 → api 侧一次性改完全部快照/常量/计数断言/矩阵 → 四道校验后单个原子提交」执行;最危险的反模式是手改快照而非从正典 `cp`——CI 不会发现。
|
||||||
|
- **发布流程须固化进 `git-workflow.md`**:现正典是一份标题仍写「草案」的迭代报告,8 步中 4 步已过期,全部修正只以散文形式存在于 `releases.md`。**E2E 份数只在一处写死**,其余位置表述为「全份」并链接过去。M4 引入对外端点,故须新写第五份 E2E 脚本,发布回归变为五份。
|
||||||
|
- **`patbond-doc` 须补 `.gitignore`**:`site/` 目前仅靠机器级全局忽略屏蔽,换机器或进 CI 时一次 `git add -A` 会提交上百个构建产物。
|
||||||
|
- **三仓 `core.hooksPath` 均为 unset**,防泄漏第一层从未生效,须逐仓启用。
|
||||||
|
|
||||||
|
### 13. 版本号
|
||||||
|
|
||||||
|
本迭代交付按 **v0.5.0** 发布(新增模块、新增端点与新增 schema,属功能增量)。
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ flutter run -d <设备ID> \
|
|||||||
# M2 挂起项(2026-09-08 登记,待执行)
|
# M2 挂起项(2026-09-08 登记,待执行)
|
||||||
|
|
||||||
> 来源:报告 iteration-2/10 §2.1(验收 6)与 iteration-2/12 §3.2;方案 A 挂起决议见 iteration-2/29 §4。
|
> 来源:报告 iteration-2/10 §2.1(验收 6)与 iteration-2/12 §3.2;方案 A 挂起决议见 iteration-2/29 §4。
|
||||||
> **时限提醒**(iteration-3/06):建议在 **2026-09-21(北极星首次出数日)前完成**,否则首批读数只能标「未验收」。
|
> **⚠️ 原「2026-09-21 前完成」时限已失效**(2026-09-14 判定,依据 iteration-4/06 §0.2):09-21 是 W37 队列(首记落在 09-07~09-13)+8 天的成熟日,而该队列**入队窗口已于 09-13 结束**。今日起产生的首记一律归入 W38,**无任何补救能为 W37 增加分母**,故该日期不再构成时限。
|
||||||
|
> **改用触发条件(iteration-4/06 §0.4 档 A)**:定义 `T0` = 首个满足「≥1 名真实移动端用户产生生命周期首条 `health_record_create_succeeded`」的 ISO 周;北极星首次出数 = `T0 + 8 天`后的首个周一,此后每周一滚动。**本清单的完成时点决定 `T0` 何时到来,因此仍是北极星出数的前置**,但不再挂固定日期——真机与真实流量均不在项目控制范围内,挂日期只会持续生产假红线。
|
||||||
|
> 若用模拟器自播种(`device-verification.md` 允许模拟器,见下文前置条件),最早可得读数为 **2026-09-28**(W38 最晚首记 09-20 +8 天),但**分子不含首记当日**,需本周内跨两天各操作一次;此类读数须标注「测试账号自播种,非产品读数,仅管道自证」并与真实读数分账登记。
|
||||||
|
|
||||||
## 验证一:Android 事件真实落库(~10 分钟)
|
## 验证一:Android 事件真实落库(~10 分钟)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> 目的:直观呈现哪些功能**已完成且有自动化测试**、哪些**部分完成**、哪些**尚未开始**,方便针对性验证与回归。
|
> 目的:直观呈现哪些功能**已完成且有自动化测试**、哪些**部分完成**、哪些**尚未开始**,方便针对性验证与回归。
|
||||||
> 维护约定:每波工单合入后由执行人更新本清单;状态以 `dev` 分支 + 门禁全绿为准。
|
> 维护约定:每波工单合入后由执行人更新本清单;状态以 `dev` 分支 + 门禁全绿为准。
|
||||||
> 最后更新:2026-09-14(**v0.4.0 发布**:patbond-api `3cd8005` 379 测试、patbond-flutter `fbcd734` 597 测试,均门禁全绿;E2E 四份 42/42 场景零失败、契约偏差 0;M2 条目见第 7~9 节,M3 见第 10~12 节,M3.5 见第 13 节)
|
> 最后更新:2026-09-14(**v0.4.0 发布**:patbond-api `3cd8005` 381 测试、patbond-flutter `fbcd734` 597 测试(+2 skipped),均门禁全绿;E2E 四份 42/42 场景零失败、契约偏差 0;M2 条目见第 7~9 节,M3 见第 10~12 节,M3.5 见第 13 节)
|
||||||
|
|
||||||
图例:✅ 已完成且已测试 | 🟡 部分完成/有已知限制 | ⬜ 未开始
|
图例:✅ 已完成且已测试 | 🟡 部分完成/有已知限制 | ⬜ 未开始
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ curl -s -i http://127.0.0.1:8082/internal/users/by-username/demo_user | head -1
|
|||||||
| 功能 | 状态 | 说明 |
|
| 功能 | 状态 | 说明 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 队列三项加固(30s 定时冲刷 / 指数退避 / anonymousId 持久化) | ✅ | 13 号规范四触发点补齐;退避 30s→5min 只挡定时冲刷;anonymousId 跨冷启动稳定(A/B 前置 #4) |
|
| 队列三项加固(30s 定时冲刷 / 指数退避 / anonymousId 持久化) | ✅ | 13 号规范四触发点补齐;退避 30s→5min 只挡定时冲刷;anonymousId 跨冷启动稳定(A/B 前置 #4) |
|
||||||
| 事件字典 v3 白名单(community 域 19 + experiment_exposed) | ✅ | EventDictionary 22→42;**7 个被否决事件锁死 unknown**(含逐卡曝光 post_impression) |
|
| 事件字典 v3 白名单(community 域 18 + experiment_exposed) | ✅ | EventDictionary 22→**41**(`Map.entry(` 实测 41;原记「community 域 19 + experiment_exposed = 42」把 `experiment_exposed` 重复计了一次,该类 Javadoc 的 8+2+8 拆分同为 18);**7 个被否决事件锁死 unknown**(含逐卡曝光 post_impression)|⚠️ `EventDictionaryTest` 无条数断言,故此类漂移无门禁可拦——补断言列为 M4 前置 |
|
||||||
| 客户端挂接 21 事件 | ✅ | feed 2(聚合 feed_viewed:≥50% 可见 ≥500ms、段内去重、离开结算)+ 互动 8 + 媒体三段 3 + 发布漏斗 5 + page_viewed 页名增量 |
|
| 客户端挂接 21 事件 | ✅ | feed 2(聚合 feed_viewed:≥50% 可见 ≥500ms、段内去重、离开结算)+ 互动 8 + 媒体三段 3 + 发布漏斗 5 + page_viewed 页名增量 |
|
||||||
| 逐卡 Feed 曝光 | ⬜ | ADR-020 否决(量级测算 7~14 个月击穿分区阈值 + 无背压);留 backlog 待 M4+ 服务端下发日志 |
|
| 逐卡 Feed 曝光 | ⬜ | ADR-020 否决(量级测算 7~14 个月击穿分区阈值 + 无背压);留 backlog 待 M4+ 服务端下发日志 |
|
||||||
| Android 真机验证(M2 两项 + M3 四项) | 🟡 | 步骤全部备齐在[真机验证清单](device-verification.md);桌面 `platform=linux` 整批 400 属契约内,落库只能真机验 |
|
| Android 真机验证(M2 两项 + M3 四项) | 🟡 | 步骤全部备齐在[真机验证清单](device-verification.md);桌面 `platform=linux` 整批 400 属契约内,落库只能真机验 |
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ feat: 迁移珊瑚橙主题体系并新增认证基础组件(ADR-005)
|
|||||||
|
|
||||||
## 凭证防泄漏检查(ADR-021)
|
## 凭证防泄漏检查(ADR-021)
|
||||||
|
|
||||||
两层检查共用同一规则表,单一来源为各仓入库的 `scripts/check-secrets.sh`(纯 shell,零外部依赖;三仓副本内容同构,调整规则时三仓同步提交):
|
两层检查共用同一规则表,单一来源为各仓入库的 `scripts/check-secrets.sh`(纯 shell,零外部依赖;**四仓**副本内容同构,调整规则时四仓同步提交——`patbond-ai` 是唯一持有 AI 服务商凭证的仓,尤其不可漏):
|
||||||
|
|
||||||
1. **本地 pre-commit(推荐,每人每仓启用一次)**:
|
1. **本地 pre-commit(推荐,每人每仓启用一次)**:
|
||||||
|
|
||||||
@@ -45,9 +45,9 @@ feat: 迁移珊瑚橙主题体系并新增认证基础组件(ADR-005)
|
|||||||
```
|
```
|
||||||
|
|
||||||
之后每次 `git commit` 自动扫描暂存区内容与文件名。注意 `core.hooksPath` 会整体接管 hooks 目录(当前三仓无其他自定义 hook)。`git commit --no-verify` 可绕过,但仅限确认误报时使用——CI 兜底仍会拦。
|
之后每次 `git commit` 自动扫描暂存区内容与文件名。注意 `core.hooksPath` 会整体接管 hooks 目录(当前三仓无其他自定义 hook)。`git commit --no-verify` 可绕过,但仅限确认误报时使用——CI 兜底仍会拦。
|
||||||
2. **CI 兜底(强制)**:三仓 `ci.yml` 在 checkout 后的首个 step 运行同一脚本的 `--all` 模式,对全部已跟踪文件扫描(本次 push 变更文件的超集),命中即红,禁止合入。
|
2. **CI 兜底(强制)**:四仓 `ci.yml` 在 checkout 后的首个 step 运行同一脚本的 `--all` 模式,对全部已跟踪文件扫描(本次 push 变更文件的超集),命中即红,禁止合入。
|
||||||
|
|
||||||
规则覆盖(细节以脚本内规则表为准,不在文档重复维护,避免两处漂移):云厂商 AccessKey 形态(AWS/腾讯云/阿里云前缀)、**AI 服务商密钥形态(Anthropic `sk-ant-`、OpenAI `sk-` / `sk-proj-` / `sk-svcacct-`)**、MinIO 默认凭证、独立成行的私钥 PEM 头、access/secret key **与 api key/secret/token、auth token** 与 JWT/签名密钥的实值赋值、配置类文件中非 `${}` 注入形态的数据库口令、`.env`/credentials/密钥导出 CSV 文件本体误入版本库。允许清单:`${}` 注入形态、占位值(changeme、your-xxx、`<占位>`、`xxx` 等)与明显示例值——配置真实值仍只允许存在于被 gitignore 的文件中,占位只进 `.sample`。
|
规则覆盖(细节以脚本内规则表为准,不在文档重复维护,避免两处漂移):云厂商 AccessKey 形态(AWS/腾讯云/阿里云前缀)、**AI 服务商密钥形态(Anthropic `sk-ant-`、OpenAI `sk-` / `sk-proj-` / `sk-svcacct-`)**、MinIO 默认凭证、独立成行的私钥 PEM 头、access/secret key **与 api key/secret/token、auth token** 与 JWT/签名密钥的实值赋值、配置类文件中非 `${}` 注入形态的数据库口令、`.env`/credentials/密钥导出 CSV 文件本体误入版本库。允许清单:`${}` 注入形态、占位值(changeme、your-xxx、`<占位>`、`xxx` 等)与明显示例值,以及**依赖注入的传参引用**(如 `api_key=settings.api_key`——行里没有任何秘密,而做 DI 的代码到处都是这种写法)。后者收得很紧:值必须是未加引号的小写起首标识符、只允许恰好一个点且随即行尾,故三段式 JWT 与 `sk-ant-…` 这类前缀仍会被拦。配置真实值仍只允许存在于被 gitignore 的文件中,占位只进 `.sample`。
|
||||||
|
|
||||||
手动全量自查:`sh scripts/check-secrets.sh --all`(在仓库根目录执行)。
|
手动全量自查:`sh scripts/check-secrets.sh --all`(在仓库根目录执行)。
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
> 范围:M3 社区纵切(Feed、帖子、草稿/发布、媒体、评论、点赞、收藏、关注、话题);AI 创作、本地服务不在本轮定义
|
> 范围:M3 社区纵切(Feed、帖子、草稿/发布、媒体、评论、点赞、收藏、关注、话题);AI 创作、本地服务不在本轮定义
|
||||||
> 性质:纯规划文档,供 M3 开发工单直接引用;不含任何代码改动
|
> 性质:纯规划文档,供 M3 开发工单直接引用;不含任何代码改动
|
||||||
|
|
||||||
|
> **⚠️ 后续修正(2026-09-14,依据 iteration-4/06 §0 与 §1.4)**——本报告按迭代报告存档惯例全文保留,但以下结论已不成立,勿再引用:
|
||||||
|
> ① **「2026-09-21 北极星首次出数」已失效**(本文 `:312`、`:320`、`:328`、`:343`、`:454` 共 5 处):09-21 是 W37 队列(首记落在 09-07~09-13)+8 天的成熟日,该队列入队窗口已于 09-13 结束,无任何补救能为其增加分母。现改为触发条件表述(`T0` = 首个有真实移动端首记的 ISO 周,`T0+8` 天后的首个周一出数),见 [真机验证清单](../../device-verification.md) 顶部与 iteration-4/06 §0.4 档 A。
|
||||||
|
> ② **`:343` 的 A/B 前置条件 2 判「绿」依据已失效**:该绿是前瞻式的,挂在「09-21 起自然达成」上。A/B 前置状态经 iteration-4/06 §1.4 逐项取证后**由「6 绿 1 半」重置为「5 绿 3 半」**(条件 4 分流哈希、条件 7 feature flag、Flutter 曝光封装三者代码零实现);`iteration-4/06` 另按放行条件口径记为 0/6 满足,故 M4 首个实验定为 A/A 基建验证空跑。
|
||||||
|
> ③ **本文 §1 的「字典 v3 增量 19 个新事件」中 community 域计数偏大 1**:`EventDictionary.java` 实际白名单为 **41 条**(非各处文档所载 42),根因是 `experiment_exposed` 被重复计入 community 域增量(该类自身 Javadoc 的 8+2+8 拆分等于 18,而非 19)。取证见 iteration-4/07 §基线对账。
|
||||||
|
|
||||||
**速览(五个核心结论)**:
|
**速览(五个核心结论)**:
|
||||||
|
|
||||||
1. 事件字典 v3 增量 **19 个新事件**(post 域 8 + feed 域 2 + 互动 8 + 实验基建 `experiment_exposed` 1),命名沿 v1/v2 惯例,结果编码进事件名,见 §1。
|
1. 事件字典 v3 增量 **19 个新事件**(post 域 8 + feed 域 2 + 互动 8 + 实验基建 `experiment_exposed` 1),命名沿 v1/v2 惯例,结果编码进事件名,见 §1。
|
||||||
|
|||||||
@@ -51,6 +51,10 @@
|
|||||||
|
|
||||||
**真机挂起四项**(步骤已在 [真机验证清单](../../device-verification.md) 备齐):媒体上传弱网、乐观更新手感、Feed 图片加载、社区事件落库。**时限提醒**:埋点角色建议 2026-09-21(北极星首次出数日)前完成 M2 两项,否则首批读数只能标未验收。
|
**真机挂起四项**(步骤已在 [真机验证清单](../../device-verification.md) 备齐):媒体上传弱网、乐观更新手感、Feed 图片加载、社区事件落库。**时限提醒**:埋点角色建议 2026-09-21(北极星首次出数日)前完成 M2 两项,否则首批读数只能标未验收。
|
||||||
|
|
||||||
|
> **⚠️ 后续修正(2026-09-14,依据 iteration-4/06 §0 与 iteration-4/07 §专项 A)**——本段原文按迭代报告存档惯例保留,但两处已不成立:
|
||||||
|
> ① **「2026-09-21 前完成」时限已失效**:09-21 是 W37 队列(首记落在 09-07~09-13)+8 天的成熟日,该队列入队窗口已于 09-13 结束,无任何补救能为其增加分母。现改为触发条件表述,见 [真机验证清单](../../device-verification.md) 顶部与 iteration-4/06 §0.4 档 A。
|
||||||
|
> ② **「挂起四项」是当时口径**:截至 2026-09-14 实际挂起 **10 项**(M2 两项 + M3 四项 + M3.5 四项)。本段的「四项」仅指 M3 引入的那四项。
|
||||||
|
|
||||||
**M3 范围内遗留**:
|
**M3 范围内遗留**:
|
||||||
1. 完整草稿列表与自动保存(26 号 §7);大图「下滑关闭」手势待 photo_view 复评
|
1. 完整草稿列表与自动保存(26 号 §7);大图「下滑关闭」手势待 photo_view 复评
|
||||||
2. `widthPx/heightPx` 恒 null(28 号观察项 1):契约描述称 confirm 后回填,实现无尺寸探测——单图帖一律回落 4:3,不呈现真实宽高比;补实现或改契约描述二选一
|
2. `widthPx/heightPx` 恒 null(28 号观察项 1):契约描述称 confirm 后回填,实现无尺寸探测——单图帖一律回落 4:3,不呈现真实宽高比;补实现或改契约描述二选一
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
3. **白名单实际 41 条,不是 42**,根因是 `experiment_exposed` 被重复计数(community 域实为 18 条,加 platform 域 1 条共 19 条新增;文档记作「19 + experiment_exposed」= 20)。**无任何测试断言字典条数**,故漂移四份文档一路不可见。M4 前置项:补一个条数断言,见 §1.1、§7。
|
3. **白名单实际 41 条,不是 42**,根因是 `experiment_exposed` 被重复计数(community 域实为 18 条,加 platform 域 1 条共 19 条新增;文档记作「19 + experiment_exposed」= 20)。**无任何测试断言字典条数**,故漂移四份文档一路不可见。M4 前置项:补一个条数断言,见 §1.1、§7。
|
||||||
4. **eventVersion 定型为「每个事件自身的 props schema 版本」**(否决「字典世代」读法):客户端现行硬编码 1 在此读法下**本就正确**,零客户端改动、零历史回填;反之则全部历史行皆错且不可修复。配套给出 bump 规则与三层校验对齐方案,见 §2。
|
4. **eventVersion 定型为「每个事件自身的 props schema 版本」**(否决「字典世代」读法):客户端现行硬编码 1 在此读法下**本就正确**,零客户端改动、零历史回填;反之则全部历史行皆错且不可修复。配套给出 bump 规则与三层校验对齐方案,见 §2。
|
||||||
5. **`platform=linux` 整批 400 是契约明文行为,枚举排除 linux 是三层锁死的设计意图;缺陷在客户端**——`analytics_service.dart:99-100` 注释称「逐条 rejected(不影响客户端)」与实现相反:实际整批 400 且被 `uploadBatch` 判为永久拒绝**整批丢弃**,桌面端 100% 静默丢事件。处置为客户端本地短路 + 调试覆盖开关,不动枚举,见 §3。
|
5. **`platform=linux` 整批 400 是契约明文行为,枚举排除 linux 是三层锁死的设计意图;缺陷在客户端**——`analytics_service.dart:99-100` 注释称「逐条 rejected(不影响客户端)」与实现相反:实际整批 400 且被 `uploadBatch` 判为永久拒绝**整批丢弃**,桌面端 100% 静默丢事件。处置为客户端本地短路 + 调试覆盖开关,不动枚举,见 §3。
|
||||||
6. **服务端生成侧不建第二条埋点管道**:`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表;裁定以 `creation.generation_tasks` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接,见 §4.2。
|
6. **服务端生成侧不建第二条埋点管道**:`platform.product_events` 的 `anonymous_id`/`session_id` NOT NULL 与 `platform IN ('android','ios')` CHECK 使 Worker 事件无法入表;裁定以 `creation.generation_jobs` 事实表为服务端指标权威,经 `creationTaskId` 与客户端事件拼接,见 §4.2。
|
||||||
7. **M4 首个「实验」应是 A/A 基建验证空跑,真实 A/B 冻结设计待流量**。A/B 前置为 **5 绿 3 半**——分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**(本角色逐项 grep 取证),必须作为前置工单而非既有资产,见 §6。
|
7. **M4 首个「实验」应是 A/A 基建验证空跑,真实 A/B 冻结设计待流量**。A/B 前置为 **5 绿 3 半**——分流哈希、Flutter 曝光封装、feature flag **三者代码零实现**(本角色逐项 grep 取证),必须作为前置工单而非既有资产,见 §6。
|
||||||
|
|
||||||
**事件增量**:新增 **8** 条(全部客户端侧,`creation` 域),既有事件补属性 **2** 处,废弃 **0** 条;字典 **41 → 49**。
|
**事件增量**:新增 **8** 条(全部客户端侧,`creation` 域),既有事件补属性 **2** 处,废弃 **0** 条;字典 **41 → 49**。
|
||||||
@@ -338,7 +338,7 @@ M4 的生成执行发生在 Worker(队列消费、租约、重试、超时)
|
|||||||
| A | Worker 走 `POST /api/v1/events`,`platform`/`anonymousId`/`sessionId` 填伪值 | **否决**。`platform` 只能填 android/ios(`V2:23`),会把服务端流量混进平台维度,**污染所有既有按平台切分的指标**;`session_id NOT NULL` 也只能造假 |
|
| A | Worker 走 `POST /api/v1/events`,`platform`/`anonymousId`/`sessionId` 填伪值 | **否决**。`platform` 只能填 android/ios(`V2:23`),会把服务端流量混进平台维度,**污染所有既有按平台切分的指标**;`session_id NOT NULL` 也只能造假 |
|
||||||
| B | 扩 `product_events`:platform 加 `server` 值、`anonymous_id`/`session_id` 改可空 | **否决**。需 Flyway 迁移改 CHECK + 放宽两个 NOT NULL + 5 份契约;放宽 NOT NULL 是**收紧不可逆**的反向操作,且此后每个下游查询都要处理 NULL 会话 |
|
| B | 扩 `product_events`:platform 加 `server` 值、`anonymous_id`/`session_id` 改可空 | **否决**。需 Flyway 迁移改 CHECK + 放宽两个 NOT NULL + 5 份契约;放宽 NOT NULL 是**收紧不可逆**的反向操作,且此后每个下游查询都要处理 NULL 会话 |
|
||||||
| C | 新建 `platform.server_events` 表,Worker 进程内直写(不走 HTTP) | 可行但**多余**,见下 |
|
| C | 新建 `platform.server_events` 表,Worker 进程内直写(不走 HTTP) | 可行但**多余**,见下 |
|
||||||
| **D** | **以 `creation.generation_tasks` 事实表为服务端指标权威**,经 `creationTaskId` 与客户端事件拼接 | **采纳** |
|
| **D** | **以 `creation.generation_jobs` 事实表为服务端指标权威**,经 `creationTaskId` 与客户端事件拼接 | **采纳** |
|
||||||
|
|
||||||
**采纳 D 的理由**:
|
**采纳 D 的理由**:
|
||||||
|
|
||||||
@@ -347,9 +347,29 @@ M4 的生成执行发生在 Worker(队列消费、租约、重试、超时)
|
|||||||
3. **零迁移增量**(相对方案 B/C):M4 本就要为 `creation` schema 建 V6 迁移,指标所需列并入即可,不额外建表、不改埋点管道。
|
3. **零迁移增量**(相对方案 B/C):M4 本就要为 `creation` schema 建 V6 迁移,指标所需列并入即可,不额外建表、不改埋点管道。
|
||||||
4. 客户端事件继续只承载**用户可见行为**(看到了什么、点了什么、等了多久),这是它的比较优势且不可被服务端替代(用户是否真的看到结果,服务端不知道)。
|
4. 客户端事件继续只承载**用户可见行为**(看到了什么、点了什么、等了多久),这是它的比较优势且不可被服务端替代(用户是否真的看到结果,服务端不知道)。
|
||||||
|
|
||||||
**因此这是对后端工单的硬要求**(本报告的指标口径依赖它,请在契约冻结时一并确认):`creation.generation_tasks` 须持久化以下列,且**终态行保留 ≥90 天不得物理删除**(软终态):
|
**因此这是对后端工单的硬要求**(本报告的指标口径依赖它,请在契约冻结时一并确认):`creation.generation_jobs` 须持久化以下列,且**终态行保留 ≥90 天不得物理删除**(软终态):
|
||||||
|
|
||||||
`id`(= `creationTaskId`)、`user_id`、`model_key`、`style_key`、`status`、`created_at`、`queued_at`、`first_attempt_at`、`last_attempt_at`、`finished_at`、`attempt_count`、`failure_reason`、`input_media_count`、`output_asset_count`、`idempotency_key`/`request_hash`。
|
> **列名已按正典对齐(2026-09-14 修订)**:本节初稿使用的是本角色自拟名(`generation_tasks` / `queued_at` / `first_attempt_at` / `finished_at` / `failure_reason` / `model_key`),与正典 `patbond_postgresql.sql:605` 的 `creation.generation_jobs` 不一致。下表为逐列映射,**其中一项正典确实缺失,已升级为 V6 的必加列**。
|
||||||
|
|
||||||
|
| 本报告所需事实 | 正典对应列 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id`(= `creationTaskId`) | `id` | ✅ 有 |
|
||||||
|
| `user_id` | `user_id` | ✅ 有 |
|
||||||
|
| `model_key` | `model_id` + 目录表 `generation_models.code`(或 `provider_model_snapshot`) | ✅ 有(分组需 join 或用快照列) |
|
||||||
|
| `style_key` | `style_code_snapshot` | ✅ 有(正典已反范式化,无需 join) |
|
||||||
|
| `status` | `status` | ✅ 有 |
|
||||||
|
| `created_at` | `created_at` | ✅ 有 |
|
||||||
|
| `queued_at` | **`created_at`**(行创建即 `status='queued'`,两者恒等) | ✅ 等价,不需新列 |
|
||||||
|
| **`first_attempt_at`** | **无对应列** | ❌ **缺失,V6 必须新增** —— 见下方说明 |
|
||||||
|
| `last_attempt_at` | `started_at`(当前尝试) | ✅ 有 |
|
||||||
|
| `finished_at` | `completed_at` | ✅ 有(改名) |
|
||||||
|
| `attempt_count` | `attempt_count` | ✅ 有 |
|
||||||
|
| `failure_reason` | `error_code` + `error_message` | ✅ 有(拆两列) |
|
||||||
|
| `input_media_count` | 由 `input_asset_id` 是否为 NULL 派生(0/1) | ✅ 可派生 |
|
||||||
|
| `output_asset_count` | 由 `output_asset_id` 是否为 NULL 派生(0/1) | ✅ 可派生 |
|
||||||
|
| `idempotency_key` / `request_hash` | 同名两列 | ✅ 有 |
|
||||||
|
|
||||||
|
**❌ `first_attempt_at` 缺口(本次修订新发现,非改名问题)**:正典没有这一列,而且 **`ck_generation_jobs_state` 会在重试时主动销毁该信息**——该 CHECK 规定 `status='queued'` 分支必须 `started_at IS NULL`,因此失败重排队时 `started_at` 必须被清空,首次尝试时间不可恢复。后果正是下一段所述:排队时长与纯执行耗时无法分离。**处置:V6 增加 `first_attempt_at timestamptz`(仅首次进入 running 时写入,重试不覆盖),并在状态机 CHECK 中把它排除在 queued 分支的置空要求之外。** 这是 §4.2 对后端工单的硬要求中唯一需要改动正典结构的一项(另一项是 A2 拍板导致的 `input_asset_id` 改可空)。
|
||||||
|
|
||||||
若上述任一列缺失,对应指标不可计算——`first_attempt_at` 缺失 ⇒ 排队时长与纯执行耗时无法分离(只能得到端到端耗时);`attempt_count` 缺失 ⇒ 重试率不可得。
|
若上述任一列缺失,对应指标不可计算——`first_attempt_at` 缺失 ⇒ 排队时长与纯执行耗时无法分离(只能得到端到端耗时);`attempt_count` 缺失 ⇒ 重试率不可得。
|
||||||
|
|
||||||
@@ -464,21 +484,21 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
| 1 进入创作 | `creation_started` | 客户端 | userId |
|
| 1 进入创作 | `creation_started` | 客户端 | userId |
|
||||||
| 2 选定模型 | `creation_model_selected`(末次) | 客户端 | userId |
|
| 2 选定模型 | `creation_model_selected`(末次) | 客户端 | userId |
|
||||||
| 3 提交生成 | `creation_submit_succeeded` / `creation_submit_failed` | 客户端 | userId(任务量用 creationTaskId) |
|
| 3 提交生成 | `creation_submit_succeeded` / `creation_submit_failed` | 客户端 | userId(任务量用 creationTaskId) |
|
||||||
| 4 排队与执行 | `generation_tasks`:`queued_at` → `first_attempt_at` → `finished_at`、`status`、`attempt_count` | **服务端事实表** | creationTaskId |
|
| 4 排队与执行 | `generation_jobs`:`created_at` → `first_attempt_at` → `completed_at`、`status`、`attempt_count` | **服务端事实表** | creationTaskId |
|
||||||
| 5 结果触达 | `creation_result_viewed` / `creation_failure_viewed` / `creation_cancelled` | 客户端 | creationTaskId |
|
| 5 结果触达 | `creation_result_viewed` / `creation_failure_viewed` / `creation_cancelled` | 客户端 | creationTaskId |
|
||||||
| 6 建草稿 → 发帖 | `creation_draft_created` → `post_publish_succeeded`(`creationTaskId` 非空) | 客户端 | userId |
|
| 6 建草稿 → 发帖 | `creation_draft_created` → `post_publish_succeeded`(`creationTaskId` 非空) | 客户端 | userId |
|
||||||
|
|
||||||
**join key 定型**:`creationTaskId`(UUIDv7,由服务端在创建任务时生成并在响应中返回,客户端原样上报)。
|
**join key 定型**:`creationTaskId`(UUIDv7,由服务端在创建任务时生成并在响应中返回,客户端原样上报)。
|
||||||
|
|
||||||
- 客户端事件侧位置:`props->>'creationTaskId'`。
|
- 客户端事件侧位置:`props->>'creationTaskId'`。
|
||||||
- 服务端侧:`creation.generation_tasks.id`。
|
- 服务端侧:`creation.generation_jobs.id`。
|
||||||
- 拼接 SQL 惯例(漏斗第 3~5 段):
|
- 拼接 SQL 惯例(漏斗第 3~5 段):
|
||||||
```sql
|
```sql
|
||||||
SELECT t.id, t.status, t.attempt_count,
|
SELECT t.id, t.status, t.attempt_count,
|
||||||
t.first_attempt_at - t.queued_at AS queue_wait,
|
t.first_attempt_at - t.created_at AS queue_wait,
|
||||||
t.finished_at - t.first_attempt_at AS exec_time,
|
t.completed_at - t.first_attempt_at AS exec_time,
|
||||||
v.props->>'waitedMs' AS perceived_wait_ms
|
v.props->>'waitedMs' AS perceived_wait_ms
|
||||||
FROM creation.generation_tasks t
|
FROM creation.generation_jobs t
|
||||||
LEFT JOIN platform.product_events v
|
LEFT JOIN platform.product_events v
|
||||||
ON v.event_name = 'creation_result_viewed'
|
ON v.event_name = 'creation_result_viewed'
|
||||||
AND (v.props->>'creationTaskId')::uuid = t.id
|
AND (v.props->>'creationTaskId')::uuid = t.id
|
||||||
@@ -488,7 +508,7 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
|
|
||||||
**闭环成立**:六段无断点,第 4 段的服务端事实与前后客户端事件均可经 `creationTaskId` 连上。
|
**闭环成立**:六段无断点,第 4 段的服务端事实与前后客户端事件均可经 `creationTaskId` 连上。
|
||||||
|
|
||||||
**缺口 1(接受不埋):客户端 `creation_queued`。** 排队进入/结束的时点以服务端为准(`queued_at`/`first_attempt_at`),客户端只能靠轮询观测,粒度取决于轮询周期,会系统性高估排队时长。客户端侧的「等待」由 `waitedMs` 一个用户感知量表达即可,不再补一个精度更差的重复事件。
|
**缺口 1(接受不埋):客户端 `creation_queued`。** 排队进入/结束的时点以服务端为准(`created_at`/`first_attempt_at`),客户端只能靠轮询观测,粒度取决于轮询周期,会系统性高估排队时长。客户端侧的「等待」由 `waitedMs` 一个用户感知量表达即可,不再补一个精度更差的重复事件。
|
||||||
|
|
||||||
**缺口 2(接受不埋):`creation_retried` 独立事件。** 重试即一次新提交,已由 `creation_submit_succeeded.regenerateFrom ∈ {failure, dissatisfaction}` 表达。独立事件会与提交事件双计,且让「提交总数」这个分母出现两种口径。
|
**缺口 2(接受不埋):`creation_retried` 独立事件。** 重试即一次新提交,已由 `creation_submit_succeeded.regenerateFrom ∈ {failure, dissatisfaction}` 表达。独立事件会与提交事件双计,且让「提交总数」这个分母出现两种口径。
|
||||||
|
|
||||||
@@ -517,8 +537,8 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
| 指标 | 分子 | 分母 | 数据源 | 口径要点 |
|
| 指标 | 分子 | 分母 | 数据源 | 口径要点 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| **生成成功率** | `status='succeeded'` 的任务数 | `status IN ('succeeded','failed')` 的任务数 | 服务端事实表 | **排除 `cancelled`**(用户主动取消不是系统失败,计入会让「用户不耐烦」伪装成「系统不可靠」);**排除未终态**(`queued`/`running`);按 task 去重非按 user |
|
| **生成成功率** | `status='succeeded'` 的任务数 | `status IN ('succeeded','failed')` 的任务数 | 服务端事实表 | **排除 `cancelled`**(用户主动取消不是系统失败,计入会让「用户不耐烦」伪装成「系统不可靠」);**排除未终态**(`queued`/`running`);按 task 去重非按 user |
|
||||||
| **P50 / P95 生成耗时** | —— | —— | 服务端事实表 | `finished_at - first_attempt_at`(**纯执行耗时,不含排队**);只取 `status='succeeded'`(失败任务的耗时是超时值,混入会污染尾部);P95 用 `percentile_cont(0.95)`;按模型(`model_key`)分组发布,**不发布跨模型合并的 P95**(不同模型耗时分布差一个量级,合并值无意义) |
|
| **P50 / P95 生成耗时** | —— | —— | 服务端事实表 | `completed_at - first_attempt_at`(**纯执行耗时,不含排队**);只取 `status='succeeded'`(失败任务的耗时是超时值,混入会污染尾部);P95 用 `percentile_cont(0.95)`;按模型(`model_id`,展示名 join `generation_models.code`;或直接用 `provider_model_snapshot`)分组发布,**不发布跨模型合并的 P95**(不同模型耗时分布差一个量级,合并值无意义) |
|
||||||
| **排队时长** | —— | —— | 服务端事实表 | `first_attempt_at - queued_at`;P50/P95 同上;**重试任务只取首次尝试**,否则重试等待会被计成排队 |
|
| **排队时长** | —— | —— | 服务端事实表 | `first_attempt_at - created_at`;P50/P95 同上;**重试任务只取首次尝试**,否则重试等待会被计成排队 |
|
||||||
| **端到端感知等待**(辅助) | —— | —— | 客户端 | `creation_result_viewed.waitedMs` 的 P50/P95。与「排队 + 执行」之差即客户端轮询/渲染开销——**这个差值是客户端性能债的直接读数** |
|
| **端到端感知等待**(辅助) | —— | —— | 客户端 | `creation_result_viewed.waitedMs` 的 P50/P95。与「排队 + 执行」之差即客户端轮询/渲染开销——**这个差值是客户端性能债的直接读数** |
|
||||||
| **结果触达率** | `creation_result_viewed` 的去重 `creationTaskId` | `status='succeeded'` 的任务数 | 双源 | 衡量「生成完了但用户没看到」。**这是 AI 创作特有的浪费指标**:算力已花但价值未交付。触达率低 ⇒ 需要完成通知 |
|
| **结果触达率** | `creation_result_viewed` 的去重 `creationTaskId` | `status='succeeded'` 的任务数 | 双源 | 衡量「生成完了但用户没看到」。**这是 AI 创作特有的浪费指标**:算力已花但价值未交付。触达率低 ⇒ 需要完成通知 |
|
||||||
| **生成 → 发帖转化率**(M4 核心) | 24h 内 `post_publish_succeeded` 且 `props->>'creationTaskId'` 非空 的去重 `userId` | `creation_result_viewed` 的去重 `userId` | 客户端 | 分母用**结果触达**而非提交或成功——没看到结果的人不可能发帖,用提交做分母会把系统故障算进产品转化的账 |
|
| **生成 → 发帖转化率**(M4 核心) | 24h 内 `post_publish_succeeded` 且 `props->>'creationTaskId'` 非空 的去重 `userId` | `creation_result_viewed` 的去重 `userId` | 客户端 | 分母用**结果触达**而非提交或成功——没看到结果的人不可能发帖,用提交做分母会把系统故障算进产品转化的账 |
|
||||||
@@ -545,18 +565,18 @@ Map.entry("post_draft_saved", Set.of("trigger", "mediaCount", "creationTaskId"))
|
|||||||
SELECT
|
SELECT
|
||||||
(SELECT count(DISTINCT props->>'creationTaskId') FROM platform.product_events
|
(SELECT count(DISTINCT props->>'creationTaskId') FROM platform.product_events
|
||||||
WHERE event_name = 'creation_submit_succeeded' AND server_ts >= :from) AS client_submits,
|
WHERE event_name = 'creation_submit_succeeded' AND server_ts >= :from) AS client_submits,
|
||||||
(SELECT count(*) FROM creation.generation_tasks WHERE created_at >= :from) AS server_tasks;
|
(SELECT count(*) FROM creation.generation_jobs WHERE created_at >= :from) AS server_tasks;
|
||||||
|
|
||||||
-- 对账 2:孤儿 taskId——客户端上报了但服务端无此任务(期望恒为空集,命中即 P1)
|
-- 对账 2:孤儿 taskId——客户端上报了但服务端无此任务(期望恒为空集,命中即 P1)
|
||||||
SELECT DISTINCT e.props->>'creationTaskId' AS orphan_task
|
SELECT DISTINCT e.props->>'creationTaskId' AS orphan_task
|
||||||
FROM platform.product_events e
|
FROM platform.product_events e
|
||||||
LEFT JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
|
LEFT JOIN creation.generation_jobs t ON (e.props->>'creationTaskId')::uuid = t.id
|
||||||
WHERE e.event_name LIKE 'creation_%' AND e.props ? 'creationTaskId' AND t.id IS NULL;
|
WHERE e.event_name LIKE 'creation_%' AND e.props ? 'creationTaskId' AND t.id IS NULL;
|
||||||
|
|
||||||
-- 对账 3:AI 来源帖的 creationTaskId 必须指向本人任务(期望恒为空集,命中即越权或口径错)
|
-- 对账 3:AI 来源帖的 creationTaskId 必须指向本人任务(期望恒为空集,命中即越权或口径错)
|
||||||
SELECT e.event_id
|
SELECT e.event_id
|
||||||
FROM platform.product_events e
|
FROM platform.product_events e
|
||||||
JOIN creation.generation_tasks t ON (e.props->>'creationTaskId')::uuid = t.id
|
JOIN creation.generation_jobs t ON (e.props->>'creationTaskId')::uuid = t.id
|
||||||
WHERE e.event_name = 'post_publish_succeeded' AND e.user_id IS NOT NULL
|
WHERE e.event_name = 'post_publish_succeeded' AND e.user_id IS NOT NULL
|
||||||
AND t.user_id <> e.user_id;
|
AND t.user_id <> e.user_id;
|
||||||
|
|
||||||
@@ -575,7 +595,7 @@ WHERE NOT EXISTS (SELECT 1 FROM platform.product_events p
|
|||||||
WHERE p.event_name = e.name AND p.server_ts >= :from);
|
WHERE p.event_name = e.name AND p.server_ts >= :from);
|
||||||
```
|
```
|
||||||
|
|
||||||
**未取证**:以上 SQL 未在真实数据上执行过(无数据,§0.2),且 `creation.generation_tasks` 的列名以 §4.2 的要求为准、尚未由后端契约确认;后端定稿后需回改列名并试跑一次。
|
**未取证**:以上 SQL 未在真实数据上执行过(无数据,§0.2)。**列名已于 2026-09-14 按正典 `patbond_postgresql.sql:605` 回改完毕**(映射表见 §4.2),唯一例外是 `first_attempt_at`——正典无此列且状态机会在重试时清空 `started_at`,已升级为 V6 必加列;该列落地前上述「排队时长 / 纯执行耗时」两项 SQL 不可运行。后端 V6 定稿后仍需试跑一次。
|
||||||
|
|
||||||
## 6. A/B 实验设计
|
## 6. A/B 实验设计
|
||||||
|
|
||||||
@@ -737,7 +757,7 @@ creationTaskId 非空的去重 userId / creation_result_viewed 去重 userId)
|
|||||||
| **2** | **eventVersion 口径定型** | ① 每个事件自身的 props schema 版本 ② 事件字典世代号 | **建议 ①**(§2.2 四条理由)。决定性理由是可修复性不对称:读法 ① 下客户端现行硬编码 1 **本就正确**,零改动零回填;读法 ② 下全部历史行皆错且**不可修复**(`server_ts` 无法反推事件当初属于哪代字典)。必须在 M4 新增 8 条事件**之前**定型,否则债务从 41 条规模翻到 49 条 |
|
| **2** | **eventVersion 口径定型** | ① 每个事件自身的 props schema 版本 ② 事件字典世代号 | **建议 ①**(§2.2 四条理由)。决定性理由是可修复性不对称:读法 ① 下客户端现行硬编码 1 **本就正确**,零改动零回填;读法 ② 下全部历史行皆错且**不可修复**(`server_ts` 无法反推事件当初属于哪代字典)。必须在 M4 新增 8 条事件**之前**定型,否则债务从 41 条规模翻到 49 条 |
|
||||||
| **3** | **eventVersion 是否上服务端校验** | ① 推荐档:字典 key 改 `(name, version)`,未知版本逐条 `unknown_event_version` ② 最小档:仅补契约 `minimum: 1` + DTO `@Min(1)` | **建议 ①**。§1.1 刚证明「无门禁的口径必然漂移四份文档而不可见」,纯文档纪律不足。注意实现须放**逐条拒绝路径**,不可放 DTO 校验——否则重犯 `platform` 的整批连坐错误 |
|
| **3** | **eventVersion 是否上服务端校验** | ① 推荐档:字典 key 改 `(name, version)`,未知版本逐条 `unknown_event_version` ② 最小档:仅补契约 `minimum: 1` + DTO `@Min(1)` | **建议 ①**。§1.1 刚证明「无门禁的口径必然漂移四份文档而不可见」,纯文档纪律不足。注意实现须放**逐条拒绝路径**,不可放 DTO 校验——否则重犯 `platform` 的整批连坐错误 |
|
||||||
| **4** | **platform=linux 处置** | ① 把 linux/server 加进枚举 ② 客户端本地短路 + 默认关闭的调试覆盖开关 | **建议 ②**。枚举是三层锁死(契约 × 5 份 + DTO 正则 + DB CHECK `V2:23`),加值需 Flyway 迁移且会让非产品流量污染 platform 维度;收益仅「桌面调试方便」,不成比例。**采纳 ② 的前提条件**:覆盖开关必须配 `appVersion` 可识别后缀护栏(§3.2),否则它就是档 C 造数据的后门 |
|
| **4** | **platform=linux 处置** | ① 把 linux/server 加进枚举 ② 客户端本地短路 + 默认关闭的调试覆盖开关 | **建议 ②**。枚举是三层锁死(契约 × 5 份 + DTO 正则 + DB CHECK `V2:23`),加值需 Flyway 迁移且会让非产品流量污染 platform 维度;收益仅「桌面调试方便」,不成比例。**采纳 ② 的前提条件**:覆盖开关必须配 `appVersion` 可识别后缀护栏(§3.2),否则它就是档 C 造数据的后门 |
|
||||||
| **5** | **服务端生成侧指标来源** | ① 新建 `server_events` 埋点管道 ② 扩 `product_events` 容纳服务端事件 ③ 以 `creation.generation_tasks` 事实表为权威 | **建议 ③**(§4.2)。所需事实 100% 已在任务表(M4 验收标准本就逼出这些列),事件层加不了信息还会引入双写不一致;② 需放宽两个 NOT NULL(不可逆的反向操作)+ 改 DB CHECK。**代价**:后端须承诺 §4.2 列出的列齐备且终态行保留 ≥90 天,否则对应指标不可算 |
|
| **5** | **服务端生成侧指标来源** | ① 新建 `server_events` 埋点管道 ② 扩 `product_events` 容纳服务端事件 ③ 以 `creation.generation_jobs` 事实表为权威 | **建议 ③**(§4.2)。所需事实 100% 已在任务表(M4 验收标准本就逼出这些列),事件层加不了信息还会引入双写不一致;② 需放宽两个 NOT NULL(不可逆的反向操作)+ 改 DB CHECK。**代价**:后端须承诺 §4.2 列出的列齐备且终态行保留 ≥90 天,否则对应指标不可算 |
|
||||||
| **6** | **隐私红线是否为 `creationTaskId` 开例外** | ① 维持「无内容或对方主体 ID」的字面禁止,漏斗改用 (userId, 时间窗) 拼接 ② 收敛措辞为「禁他人/内容主体 ID」,允许上报者自有资源 ID 作拼接键 | **建议 ②**。原红线意图是防止重建「谁对谁的内容做了什么」的社交图谱,`creationTaskId` 不涉第二主体、不泄露 prompt 与产物;① 的时间窗拼接在并发提交下会把 A 任务耗时配给 B 任务结果且不可察觉,对 P95 这类尾部指标是致命的。**否决**「做成第 11 个公共属性」的替代方案——为单域需求撬动跨迭代冻结的十项公共属性面不成比例 |
|
| **6** | **隐私红线是否为 `creationTaskId` 开例外** | ① 维持「无内容或对方主体 ID」的字面禁止,漏斗改用 (userId, 时间窗) 拼接 ② 收敛措辞为「禁他人/内容主体 ID」,允许上报者自有资源 ID 作拼接键 | **建议 ②**。原红线意图是防止重建「谁对谁的内容做了什么」的社交图谱,`creationTaskId` 不涉第二主体、不泄露 prompt 与产物;① 的时间窗拼接在并发提交下会把 A 任务耗时配给 B 任务结果且不可察觉,对 P95 这类尾部指标是致命的。**否决**「做成第 11 个公共属性」的替代方案——为单域需求撬动跨迭代冻结的十项公共属性面不成比例 |
|
||||||
| **7** | **M4 首个「实验」的形态** | ① 直接跑真实 A/B ② A/A 基建验证先行,真实 A/B 冻结设计待流量 | **建议 ②**(§6.1)。真实 A/B 需 ~1,560 名到达结果页用户,当前真实用户 0,按 M2 §4.3 止损纪律现在就判不可行;且三项前置为零代码。A/A 零产品风险、极小样本即可暴露分流失衡与曝光漏报——**跳过它等于把三个从未运行过的组件与一个产品结论绑在一起** |
|
| **7** | **M4 首个「实验」的形态** | ① 直接跑真实 A/B ② A/A 基建验证先行,真实 A/B 冻结设计待流量 | **建议 ②**(§6.1)。真实 A/B 需 ~1,560 名到达结果页用户,当前真实用户 0,按 M2 §4.3 止损纪律现在就判不可行;且三项前置为零代码。A/A 零产品风险、极小样本即可暴露分流失衡与曝光漏报——**跳过它等于把三个从未运行过的组件与一个产品结论绑在一起** |
|
||||||
| **8** | **A/B 前置状态是否按取证结果重置** | ① 维持 `iteration-3/29` 的「6 绿 1 半」 ② 重置为 **5 绿 3 半**并写入 ADR | **建议 ②**。#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag 三者代码零实现(逐项 grep 取证,§1.4),且 #1/#2 的绿是**前瞻式**的(挂在「09-21 起自然达成」上,该前提已随 §0 失效)。这直接决定 M4 排期——**若按「6 绿 1 半」排,会在实验启动日才发现要先写三个组件** |
|
| **8** | **A/B 前置状态是否按取证结果重置** | ① 维持 `iteration-3/29` 的「6 绿 1 半」 ② 重置为 **5 绿 3 半**并写入 ADR | **建议 ②**。#4 分流哈希、#5 Flutter 曝光封装、#7 feature flag 三者代码零实现(逐项 grep 取证,§1.4),且 #1/#2 的绿是**前瞻式**的(挂在「09-21 起自然达成」上,该前提已随 §0 失效)。这直接决定 M4 排期——**若按「6 绿 1 半」排,会在实验启动日才发现要先写三个组件** |
|
||||||
@@ -751,7 +771,7 @@ creationTaskId 非空的去重 userId / creation_result_viewed 去重 userId)
|
|||||||
1. **真机执行人(首日,1 小时,无依赖,最高优先)**:`export ANDROID_HOME=~/Android/Sdk` + `flutter emulators --launch Pixel_7`,执行 M2 两项真机验证(步骤 `device-verification.md:42-92`,模拟器场景宿主机地址用 `10.0.2.2`),填 L106 执行记录。产出 `platform='android'` 事件实证,解除 A/B 前置 #1 的拦路项。**这是 §0.3 的 decisive finding 的直接落地,且不依赖任何其他工单**。
|
1. **真机执行人(首日,1 小时,无依赖,最高优先)**:`export ANDROID_HOME=~/Android/Sdk` + `flutter emulators --launch Pixel_7`,执行 M2 两项真机验证(步骤 `device-verification.md:42-92`,模拟器场景宿主机地址用 `10.0.2.2`),填 L106 执行记录。产出 `platform='android'` 事件实证,解除 A/B 前置 #1 的拦路项。**这是 §0.3 的 decisive finding 的直接落地,且不依赖任何其他工单**。
|
||||||
2. **用户 / PM(首日)**:拍板 §8 的 #1、#2、#5、#8(前两项决定后续工单形态,#5 决定后端表结构,#8 决定排期)。
|
2. **用户 / PM(首日)**:拍板 §8 的 #1、#2、#5、#8(前两项决定后续工单形态,#5 决定后端表结构,#8 决定排期)。
|
||||||
3. **数据侧(0.5 天,依赖 1)**:把 `iteration-2/06` §2.1 北极星 SQL 落成仓库内可执行载体,跑出 W37 实测分母,按 §0.4 档 B 出「基建就绪读数」。
|
3. **数据侧(0.5 天,依赖 1)**:把 `iteration-2/06` §2.1 北极星 SQL 落成仓库内可执行载体,跑出 W37 实测分母,按 §0.4 档 B 出「基建就绪读数」。
|
||||||
4. **后端(依赖 2 的 #5)**:`creation` schema 的 V6 迁移中确保 §4.2 要求的列齐备(`queued_at`/`first_attempt_at`/`attempt_count`/`failure_reason` 等)+ 终态行保留 ≥90 天纪律;与生成任务契约同批冻结 `modelKey`/`styleKey` 枚举。
|
4. **后端(依赖 2 的 #5)**:`creation` schema 的 V6 迁移中确保 §4.2 要求的列齐备(`first_attempt_at`(新增)/`attempt_count`/`error_code` 等)+ 终态行保留 ≥90 天纪律;与生成任务契约同批冻结目录 `code` 口径(正典为 `generation_models.code` / `generation_styles.code`,任务行反范式化列为 `provider_model_snapshot` / `style_code_snapshot`)。
|
||||||
5. **后端(依赖 2 的 #2#3)**:`EventDictionary` v4 增量 8 条 + 2 条改写(§4.5 代码块可直抄);eventVersion 校验按推荐档;**同批补字典条数与全名快照断言**(§7.2 同步点 4)。可与 4 并行。
|
5. **后端(依赖 2 的 #2#3)**:`EventDictionary` v4 增量 8 条 + 2 条改写(§4.5 代码块可直抄);eventVersion 校验按推荐档;**同批补字典条数与全名快照断言**(§7.2 同步点 4)。可与 4 并行。
|
||||||
6. **后端(依赖 5)**:契约 5 份副本同步 `eventVersion` 描述 + `minimum: 1` + `reason` 枚举增量;跑契约一致性矩阵。
|
6. **后端(依赖 5)**:契约 5 份副本同步 `eventVersion` 描述 + `minimum: 1` + `reason` 枚举增量;跑契约一致性矩阵。
|
||||||
7. **Flutter(依赖 2 的 #4,可与 4/5 并行)**:`analytics_service.dart` 四项改动(eventVersion 查表、注释据实、桌面短路、调试覆盖 + appVersion 护栏);两份 E2E 脚本 `eventVersion` 改 1。
|
7. **Flutter(依赖 2 的 #4,可与 4/5 并行)**:`analytics_service.dart` 四项改动(eventVersion 查表、注释据实、桌面短路、调试覆盖 + appVersion 护栏);两份 E2E 脚本 `eventVersion` 改 1。
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
# OpenAI gpt-image-2 图片生成:接口事实与映射
|
||||||
|
|
||||||
|
> 取证日期:2026-09-21 来源:用户提供的三份官方文档(`doc_temp/gpt生图.md`、
|
||||||
|
> `doc_temp/gpt图像编辑.md`、`doc_temp/gpt错误码.md`)
|
||||||
|
> 实现位置:`patbond-ai` 的 `app/providers/openai/`(`client.py` 传输 / `errors.py` 错误映射 /
|
||||||
|
> `models.py` 厂商 DTO / `image.py` 图片业务 / `capabilities.py` 参数限制 /
|
||||||
|
> `tasks.py` 同步→异步桥)
|
||||||
|
|
||||||
|
本页记录**从官方文档实际读到的事实**,供实现与排查引用。凡本页与代码不一致,
|
||||||
|
以代码为准并回改本页;凡本页与厂商文档不一致,以厂商文档为准并回改两处。
|
||||||
|
|
||||||
|
## 1. 两个端点
|
||||||
|
|
||||||
|
| 用途 | 方法与路径 | 载荷形式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 文生图 | `POST https://api.openai.com/v1/images/generations` | JSON |
|
||||||
|
| 图生图(图像编辑) | `POST https://api.openai.com/v1/images/edits` | **multipart/form-data** |
|
||||||
|
| 鉴权 | `Authorization: Bearer $OPENAI_API_KEY` | — |
|
||||||
|
|
||||||
|
**没有第三个端点**:没有任务提交接口,也**没有任务查询接口**。两个端点都是同步
|
||||||
|
的——一次请求里等到图片生成完才返回。这一条决定了第 2、7 两节的全部内容。
|
||||||
|
|
||||||
|
## 2. 与火山视频的三处根本差异
|
||||||
|
|
||||||
|
| | 火山 Seedance(视频) | OpenAI gpt-image-2(图片) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 调用模型 | 异步:提交返回 `{"id"}`,另有查询端点 | **同步**:一次请求等到底,无任务 ID |
|
||||||
|
| 产物形式 | URL,**有效期 24 小时** | **base64 字节**,只在内存里 |
|
||||||
|
| 图生图载荷 | JSON 里放图片 URL | **multipart**,`image` 是文件 |
|
||||||
|
|
||||||
|
三处差异的处置,逐条对应到代码:
|
||||||
|
|
||||||
|
1. **同步 → 异步**由 `tasks.py` 的 `SyncTaskRegistry` 在本服务内部吸收:`submit`
|
||||||
|
把同步调用丢进线程池、立刻返回一个本地生成的 `local-<uuid>` 任务 ID,`fetch`
|
||||||
|
查这个内存注册表。对外契约仍是 ADR-023 §4 的两段式提交/轮询,**Java 侧看不出
|
||||||
|
两家供应商的同步性差异**。代价见第 7 节。
|
||||||
|
2. **base64 产物**没有「URL 过期」问题(这比火山的 24 小时硬约束宽松),但字节
|
||||||
|
只在内存里,进程重启即丢。领域结果里放的是 `data:image/png;base64,...` 这个
|
||||||
|
data URI,**它只为满足「SUCCEEDED 必须有 asset_url」的不变式而存在,不是可长期
|
||||||
|
保存的地址**——一张 1024×1024 的 png 约 1~2MB,base64 后更大,绝不能原样写进
|
||||||
|
`media.assets.external_url`。转存落地后这里应改为返回上传后的 object key。
|
||||||
|
3. **multipart 图生图**:我们的输入是一个 URL,故需先下载再作为 multipart 上传。
|
||||||
|
下载失败按参数错误**同步拒绝**(见第 3 节末),而不是变成一个注定失败的后台
|
||||||
|
任务。字段名统一用 `image[]`(该端点支持多图且共用此名),避免单图一种写法、
|
||||||
|
多图另一种的分叉。
|
||||||
|
|
||||||
|
**另一处差异:尺寸不必查对照表。** 响应直接带 `size` 字段(`WIDTHxHEIGHT`),
|
||||||
|
故像素宽高是确定值——不像火山那边只给 `ratio` + `resolution`、需要一张随模型族
|
||||||
|
变化的对照表。
|
||||||
|
|
||||||
|
## 3. 请求参数:接了哪些、没接哪些
|
||||||
|
|
||||||
|
**接入的**(`ImageSubmission`):
|
||||||
|
|
||||||
|
| 厂商字段 | 来源 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `model` | 任务行冻结的快照,缺失时回落 `OPENAI_IMAGE_MODEL` | 适配层**不自行选模型**——任务记录与实际调用必须一致,否则事后无法对账 |
|
||||||
|
| `prompt` | 领域请求 | — |
|
||||||
|
| `n` | **固定 1,不开放** | 一个任务一个产物(`generation_jobs.output_asset_id` 是单值),放开 n>1 要先改数据模型 |
|
||||||
|
| `size` | `width_px`×`height_px` 拼成 `WIDTHxHEIGHT` | 两个都有才拼;只给一个时**不给 size**、交模型自选(半个尺寸不如不给) |
|
||||||
|
| `quality` | 可选 | 不传 = 服务商默认 `auto` |
|
||||||
|
| `output_format` | 可选 | `png`/`jpeg`/`webp`,默认 png |
|
||||||
|
| `background` | 可选 | `transparent` 时 `output_format` 只能 png/webp |
|
||||||
|
| `user` | 领域的 `safety_identifier` | ⚠️ **厂商字段名是 `user`**,不是 `safety_identifier`。必须传哈希,不可传明文用户 ID |
|
||||||
|
|
||||||
|
**刻意没接的**:`stream`、`partial_images`、`mask`、`input_fidelity`、
|
||||||
|
`output_compression`、`moderation`、`response_format`。理由分两类:
|
||||||
|
|
||||||
|
- `response_format` **对 GPT image 系列根本不支持**(文档原文:这些模型 always
|
||||||
|
return base64-encoded images),传了无意义。
|
||||||
|
- 其余是能力而非缺陷——多暴露一个参数,就多一处要在契约、Java 客户端、文档三处
|
||||||
|
同步维护的地方。其中 `input_fidelity: high` 对「参考图要保真」的场景有价值,
|
||||||
|
是最可能第一个被补上的,记在这里以备后用。
|
||||||
|
|
||||||
|
**参数校验发生在受理之前。** `capabilities.py` 的三个 `validate_*` 与参考图下载
|
||||||
|
都在 `registry.submit()` **之前**执行,故违规请求以 400 同步返回,而不是变成一个
|
||||||
|
注定失败的后台任务——后者要等 Java 侧一轮轮询才被发现,还白占一个线程池槽位。
|
||||||
|
|
||||||
|
## 4. 尺寸约束:四条算术规则
|
||||||
|
|
||||||
|
文档对 `size` 施加的是算术约束而非枚举(这是 gpt-image-2 与 2.5 系列共有的能力,
|
||||||
|
dall-e 系列才是固定枚举)。逐条:
|
||||||
|
|
||||||
|
| 规则 | 文档原文依据 | 代码 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 宽高**都能被 16 整除** | width and height must both be divisible by 16 | `EDGE_MULTIPLE = 16` |
|
||||||
|
| 宽高比在 **1:3 ~ 3:1** | the requested aspect ratio must be between 1:3 and 3:1 | `MIN_ASPECT` / `MAX_ASPECT` |
|
||||||
|
| 上限 **3840x2160** | the maximum supported resolution is `3840x2160` | `MAX_EDGE` / `MAX_PIXELS` |
|
||||||
|
| 超过 **2560x1440** 属实验性 | Resolutions above `2560x1440` are experimental | 放行 + 记一条 INFO 日志 |
|
||||||
|
|
||||||
|
**标准尺寸** `1024x1024` / `1536x1024` / `1024x1536` 与 `auto` 一律直接放行,
|
||||||
|
不过算术校验:它们是文档明示支持的取值。
|
||||||
|
|
||||||
|
### ⚠️ 一处由我们推断的语义:上限是逐边还是按像素
|
||||||
|
|
||||||
|
文档那句「the maximum supported resolution is 3840x2160」**没说清**限制是逐边
|
||||||
|
(宽≤3840 **且** 高≤2160)还是按总像素。
|
||||||
|
|
||||||
|
按逐边理解会**禁掉所有高于 2160 的竖图**——而 `1024x1536` 是文档明示的标准竖图
|
||||||
|
尺寸、竖版显然受支持,故逐边理解必然过严。
|
||||||
|
|
||||||
|
采用的读法:**最长边 ≤ 3840,且总像素 ≤ 3840×2160**。它允许 `2160x3840` 这类
|
||||||
|
与横图同像素量的竖图,又拦住明显越界的尺寸。同理,「实验性」阈值也按面积理解。
|
||||||
|
拿到更明确的文档或实测结果后,回来校正 `capabilities.py` 与本节。
|
||||||
|
|
||||||
|
文档还有一句「The requested size must also satisfy the model's current pixel and
|
||||||
|
edge limits」——**没给具体数值**,故这一条无法在本地校验,只能由服务商的 400
|
||||||
|
兜住。
|
||||||
|
|
||||||
|
### 画质档按模型族分叉
|
||||||
|
|
||||||
|
`low`/`medium`/`high`/`auto` 是 GPT image 系列共有;`xhigh` 与 `max` **仅**
|
||||||
|
`gpt-image-2.5-sunburst` / `gpt-image-2.5-flare`(含其 `2026-09-08` 快照)支持。
|
||||||
|
`capabilities.py` 目前只覆盖 gpt-image-2 一张表——接入 2.5 系列时再按模型名分表,
|
||||||
|
现在分是给一个不存在的需求写代码。
|
||||||
|
|
||||||
|
## 5. 响应体的关键字段
|
||||||
|
|
||||||
|
两个端点响应同形(`ImagesResponse`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"created": 1713833628,
|
||||||
|
"data": [{ "b64_json": "..." }],
|
||||||
|
"size": "1024x1024",
|
||||||
|
"output_format": "png",
|
||||||
|
"quality": "high",
|
||||||
|
"usage": {
|
||||||
|
"total_tokens": 100, "input_tokens": 50, "output_tokens": 50,
|
||||||
|
"input_tokens_details": { "text_tokens": 10, "image_tokens": 40 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 映射到领域 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `data[0].b64_json` | `asset_url`(data URI) | **GPT image 系列始终返回它**;`data[].url` 只用于 dall-e,对本系列不支持 |
|
||||||
|
| `size` | `width_px` / `height_px` | 直接解析,无需对照表 |
|
||||||
|
| `output_format` | `output_format` | — |
|
||||||
|
| `usage.total_tokens` | `billing_tokens` | 计费对账依据 |
|
||||||
|
| — | `asset_url_expires_in_sec` = **`None`** | 字节在手,无时效。用 `None` 而不是编一个假的秒数 |
|
||||||
|
| — | `progress` | 同步接口不提供进度:在途一律 0、完成 100。契约本不承诺平滑进度(ADR-023 §10) |
|
||||||
|
|
||||||
|
**`2xx` 但 `data` 为空当作失败处理**(`provider_code = MissingImageData`,
|
||||||
|
可退避重试)。服务商说成功却没给图,那不是成功——把它当成功会让上层拿到一个没有
|
||||||
|
产物的 SUCCEEDED 去空指针。
|
||||||
|
|
||||||
|
## 6. 错误映射
|
||||||
|
|
||||||
|
> 取证补充(2026-09-21):收到官方 Error codes 页(`doc_temp/gpt错误码.md`)。
|
||||||
|
> 本节此前是按 HTTP 状态码推断的版本,现已按文档实际码名重写。
|
||||||
|
|
||||||
|
### 先记一笔教训:此前猜的五个码名,一个都不存在
|
||||||
|
|
||||||
|
| 我猜的 | 官方文档里 |
|
||||||
|
| --- | --- |
|
||||||
|
| `rate_limit_exceeded` | ❌ 不存在。真实的是 type `rate_limit_error` + code `slow_down` |
|
||||||
|
| `moderation_blocked` | ❌ 不存在 |
|
||||||
|
| `content_policy_violation` | ❌ 不存在 |
|
||||||
|
| `invalid_api_key` | ❌ 不存在(401 只给人类可读消息,无 code) |
|
||||||
|
| `image_generation_user_error` | ❌ 不存在 |
|
||||||
|
| `insufficient_quota` | ✅ 存在,但它是**宽 `type`** 而非 `code` |
|
||||||
|
|
||||||
|
六个里错五个。这就是「凭记忆列错误码表」的实际命中率——那张表当时看起来是权威的。
|
||||||
|
故当时的 ⚠️ 标注不是客套:**没有文档就要明写没有文档**。
|
||||||
|
|
||||||
|
### ⚠️ 官方错误码表里没有任何业务层失败
|
||||||
|
|
||||||
|
13 条全是**基础设施与计费**:
|
||||||
|
|
||||||
|
| 类别 | 条目 |
|
||||||
|
| --- | --- |
|
||||||
|
| 鉴权(401) | 无效认证 / 密钥不正确 / 不属于任何组织 / IP 不在白名单 |
|
||||||
|
| 地域(403) | 国家、地区或领地不受支持 |
|
||||||
|
| 参数(400) | `service_tier` 不被项目允许 |
|
||||||
|
| 限流(429) | 请求过快 / `slow_down`(涨得太快) |
|
||||||
|
| 钱(429) | 余额耗尽 / 组织消费限额 / 项目消费限额 / 组织用量限额 |
|
||||||
|
| 服务端(500 / 503) | 内部错误 / 模型过载 |
|
||||||
|
|
||||||
|
**没有内容审核、没有版权/侵权、没有「这个提示词不能生成」。** 但图片接口明确有
|
||||||
|
`moderation` 参数(`low`/`auto`),说明审核确实存在——只是它的错误形态未被文档化。
|
||||||
|
|
||||||
|
### 映射表(键全部抄自文档)
|
||||||
|
|
||||||
|
| 依据 | `retry` | 用户可见 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `credit_balance_exhausted` | `QUOTA` | 配额耗尽 |
|
||||||
|
| `organization_spend_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||||
|
| `project_spend_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||||
|
| `organization_usage_limit_exceeded` | `QUOTA` | 配额耗尽 |
|
||||||
|
| `insufficient_quota`(宽 type) | `QUOTA` | 配额耗尽 |
|
||||||
|
| `slow_down` / `rate_limit_error` | `BACKOFF` | 限流 |
|
||||||
|
| `server_is_overloaded` / `service_unavailable_error` | `BACKOFF` | 上游不可用 |
|
||||||
|
| HTTP 429(无 code 且 type 不认识) | `BACKOFF` | 限流 |
|
||||||
|
| HTTP 400 **带** `error.param` | `NEVER` | 服务端错误 |
|
||||||
|
| HTTP 400 **不带** `error.param` | `NEVER` | **内容被拒**(推断,见下) |
|
||||||
|
| HTTP 401 / 403 | `NEVER` | 服务端错误 |
|
||||||
|
| 其余 4xx | `NEVER` | 服务端错误 |
|
||||||
|
| 网络层失败(连不上、超时) | `BACKOFF` | 上游不可用 |
|
||||||
|
| 5xx 及一切未知 | `BACKOFF` | 上游不可用 |
|
||||||
|
|
||||||
|
`Retry.QUOTA` 这一档的存在理由正是文档那句原话:**「Retrying billing, spend, or
|
||||||
|
quota errors won't restore API access」**——重试不会恢复访问,要人去充值或改限额。
|
||||||
|
Worker 不该把重试预算花在这上面,应直接终态失败并让用户看到出口。
|
||||||
|
|
||||||
|
**两个字段查同一张表。** 文档把某个标识列在哪个字段下,不保证实际响应里它一定出现
|
||||||
|
在那个字段(`insufficient_quota` 被描述为「broader `error.type`」,但很容易出现在
|
||||||
|
`code` 里)。查错字段的代价是把配额耗尽降级成限流、白烧一轮重试预算;而两边都查
|
||||||
|
不引入任何**新**词汇。
|
||||||
|
|
||||||
|
### 唯一的推断:400 的两义如何区分
|
||||||
|
|
||||||
|
因为没有内容审核码,**一次侵权或违规拒绝很可能以一个普通 400 到达**,只带一句
|
||||||
|
人类可读的消息。若把这种 400 一律归为「服务端错误」,用户会看到「服务出错了」
|
||||||
|
然后反复重试同一个注定被拒的提示词。
|
||||||
|
|
||||||
|
用 `error.param` 的有无做结构化区分,三条依据:
|
||||||
|
|
||||||
|
1. 文档在 `service_tier` 那条明确写了「as an `invalid_request_error` with
|
||||||
|
`error.param` set to `service_tier`」——**参数类错误会带 `param`**。
|
||||||
|
2. 官方表没有任何内容审核码,故内容拒绝只能以通用 400 到达,且它不指向任何具体
|
||||||
|
参数,`param` 应为空。
|
||||||
|
3. 我方在调用前已用 `capabilities.py` 校验过尺寸、画质、格式,`model`/`n` 由程序
|
||||||
|
生成——**能活着到达服务商并被 400 拒掉的,参数嫌疑本来就不大**。
|
||||||
|
|
||||||
|
取错的代价不对称,这也是选这个方向的理由:把内容拒绝报成「服务出错」会让用户反复
|
||||||
|
重试一个注定被拒的提示词;反过来把一个参数 bug 报成「内容可能不被支持」只是让用户
|
||||||
|
多试一次,而排查者看到的 `provider_code` 与日志仍然准确。走这条分支时会记一条
|
||||||
|
**WARNING**,那是日后校正它的唯一素材来源。
|
||||||
|
|
||||||
|
**待补**:拿到真实的审核错误形态(或官方补出业务码)后,把这里换成确定的判定。
|
||||||
|
|
||||||
|
### 一处厂商改判,对我们无影响
|
||||||
|
|
||||||
|
文档提醒:**视频过载已从 429 改判为 503**,且 Python SDK 分别抛 `RateLimitError`
|
||||||
|
与 `InternalServerError`,只捕一类会漏。我们两者都归可重试,故这次改判不影响本
|
||||||
|
服务——`tests/providers/openai/test_errors.py` 里有一条测试专门钉住这个「无影响」,
|
||||||
|
哪天有人把 5xx 改成不可重试,它会红。
|
||||||
|
|
||||||
|
### 未接的一项:`Retry-After`
|
||||||
|
|
||||||
|
文档在 429 与 503 处反复要求遵守 `Retry-After` 响应头。本服务目前**不读它**。
|
||||||
|
|
||||||
|
不读的理由是可算的:Java 侧 Worker 的退避阶梯基线 30 秒、指数增长、上限 10 分钟
|
||||||
|
(`CreationProperties.retryBackoffFor`),**在绝大多数情形下已长于服务商给的
|
||||||
|
`Retry-After`**——也就是我们已经比要求更保守,忽略它只会让重试偏慢,不会加剧限流。
|
||||||
|
|
||||||
|
要接起来是一次跨语言契约变更:领域 `Failure` 加字段 → `openapi-internal.yaml` 小
|
||||||
|
版本 → Java 侧 DTO 与 Worker 调度都要改。收益只是延迟优化,故排在存储决策之后。
|
||||||
|
|
||||||
|
## 7. 同步接口的两笔代价
|
||||||
|
|
||||||
|
把同步接口包成异步是有代价的,且这两笔代价**无法靠代码消除**,只能明写出来。
|
||||||
|
|
||||||
|
### ① `patbond-ai` 从此持有状态
|
||||||
|
|
||||||
|
`SyncTaskRegistry` 是本服务**唯一**的有状态组件。后果:
|
||||||
|
|
||||||
|
- **进程重启会丢掉所有在途任务。** 重启后 Java 侧轮询该任务 ID 得到
|
||||||
|
`task_not_found`,按失败处理。这与「任务 ID 从未存在过」无法区分,故代码不假装
|
||||||
|
区分——两种情形下调用方要做的是同一件事:停止轮询、任务判失败。
|
||||||
|
- **`patbond-ai` 因此不能随意多副本部署。** 任务 ID 只在签发它的那个进程里有效,
|
||||||
|
一旦多副本 + 无粘性路由,轮询会落到不认识该 ID 的副本上。当前单机部署无此问题,
|
||||||
|
**但横向扩容前必须先解决它**(把注册表外移到共享存储,或在网关做按 ID 的粘性
|
||||||
|
路由)。视频那侧没有这个约束——火山的任务 ID 是全局的,任何副本都能查。
|
||||||
|
- 内存有界:`OPENAI_MAX_INFLIGHT`(默认 256)满了以后**拒绝新提交**(503),
|
||||||
|
而不是挤掉在途任务——挤掉一个在途任务会让一次可能正在计费的调用变成无人认领的
|
||||||
|
开销。已完成条目按 `OPENAI_COMPLETED_TTL_SEC` 过期,**在途条目永不过期**。
|
||||||
|
|
||||||
|
### ② 超时窗口无法靠「再查一次」弥补
|
||||||
|
|
||||||
|
`OPENAI_TIMEOUT_SEC` 的语义与 `ARK_TIMEOUT_SEC` **不同**,这一点最容易配错:
|
||||||
|
|
||||||
|
| | `ARK_TIMEOUT_SEC` | `OPENAI_TIMEOUT_SEC` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 含义 | 单次 HTTP 短往返(提交只入队即返回) | **生成耗时的上限** |
|
||||||
|
| 默认 | 10 秒 | 180 秒 |
|
||||||
|
|
||||||
|
请求超时时,**那次调用可能已经在服务商侧完成并计费**,而我们拿不到结果。图片接口
|
||||||
|
没有任务 ID 可供事后查询,故这个窗口**无法**像火山那样靠重查一次弥补——只能由
|
||||||
|
`max_attempts` 兜住上限。这是选用同步型供应商的固有成本,不是实现缺陷。
|
||||||
|
|
||||||
|
### 一处已修的死锁,记下来避免重犯
|
||||||
|
|
||||||
|
`SyncTaskRegistry` 起初用 `add_done_callback` 记录完成时刻。这会**死锁**:快速完成
|
||||||
|
的任务在 `add_done_callback` 时已经 done,回调便**同步地在调用线程执行**,而该线程
|
||||||
|
此刻正持有注册表的锁(非可重入)。现在的做法是——完成时刻在**首次被观察到**时
|
||||||
|
惰性记录(`fetch` 与过期清理里)。代价是 TTL 起算点可能略晚于实际完成时刻,
|
||||||
|
这对「结果被取走前不要清掉」这个目的无害,反而更保守。
|
||||||
|
|
||||||
|
## 8. 实现状态
|
||||||
|
|
||||||
|
`app/providers/openai/` 与火山适配层**刻意不共用传输层**:两家的错误体形状不同
|
||||||
|
(OpenAI 是 `{"error": {message, type, code, param}}`,火山是 `{"error": {code, message}}`),
|
||||||
|
且 OpenAI 的编辑端点走 multipart。强行抽一个共同基类会让两边都被对方的特例污染
|
||||||
|
——供应商适配层之间**不该**互相复用,各自复用自家的传输层才是分层的本意。分层约束
|
||||||
|
由 `tests/test_layering.py` 用 AST 自动守着。
|
||||||
|
|
||||||
|
| 项 | 状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| HTTP 传输与鉴权(`client.py`) | ✅ 离线测试(MockTransport) |
|
||||||
|
| 错误分类(`errors.py`) | ✅ 按官方 Error codes 页逐条落地;仅 400 的两义区分是推断(第 6 节) |
|
||||||
|
| 参数预校验(`capabilities.py`) | ✅ 四条算术规则 + 画质/格式 |
|
||||||
|
| 同步→异步桥(`tasks.py`) | ✅ 含容量上限、TTL、死锁回归 |
|
||||||
|
| 图片业务(`image.py`) | ✅ 文生图 + 图生图(含参考图下载) |
|
||||||
|
| 装配与配置(`assembly.py` / `config.py` / `.env.sample`) | ✅ 缺 `OPENAI_API_KEY` 时跳过装配,服务仍可启动 |
|
||||||
|
| 产物转存 | ⏸ 阻塞于存储决策(ADR-023 §7)。注意 base64 字节只在内存里 |
|
||||||
|
| V7 目录种子(图片模型) | ⏸ 未播种——播一个没有转存能力的模型会让客户端出现点了没反应的入口 |
|
||||||
|
| Java 侧图片 provider 路由 | ⏸ 待上一项 |
|
||||||
|
|
||||||
|
**跨语言约定,两侧都要对齐**:目录表的 `provider_code` 必须与适配器的
|
||||||
|
`PROVIDER_CODE` **逐字一致**(`openai-gpt-image`)。对不上时 Worker 按任务行冻结的
|
||||||
|
快照查不到适配器,任务会永远停在排队——而那看起来像队列坏了,不像两个字符串不
|
||||||
|
一致。播种图片模型时须同时加上这条一致性断言(火山那侧已有)。
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
# 火山方舟 Seedance 视频生成:接口事实与映射
|
||||||
|
|
||||||
|
> 取证日期:2026-09-20 来源:用户提供的三份官方文档(`doc_temp/火山视频请求.md`、
|
||||||
|
> `doc_temp/火山错误码.md`、`doc_temp/火山查询.md`)
|
||||||
|
> 实现位置:`patbond-ai` 的 `app/providers/volcengine/`(`client.py` 传输 / `errors.py` 错误映射 /
|
||||||
|
> `models.py` 厂商 DTO / `video.py` 视频业务 / `capabilities.py` 模型限制)
|
||||||
|
|
||||||
|
本页记录**从官方文档实际读到的事实**,供实现与排查引用。凡本页与代码不一致,
|
||||||
|
以代码为准并回改本页;凡本页与厂商文档不一致,以厂商文档为准并回改两处。
|
||||||
|
|
||||||
|
> **文档文件名与内容对不上**:用户提供的 `火山错误码.md` 里装的是**查询 API**
|
||||||
|
> 文档,`火山查询.md` 里装的是**错误码表**。按文件名取用会读错,已按实际内容归档。
|
||||||
|
|
||||||
|
## 1. 三个端点
|
||||||
|
|
||||||
|
| 用途 | 方法与路径 |
|
||||||
|
| --- | --- |
|
||||||
|
| 提交任务 | `POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks` |
|
||||||
|
| 查询任务 | `GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id}` |
|
||||||
|
| 鉴权 | `Authorization: Bearer $ARK_API_KEY` |
|
||||||
|
|
||||||
|
**提交是异步接口**:响应只有 `{"id": ...}`,状态靠查询接口拿。这与 ADR-023 §4
|
||||||
|
「Java↔Python 边界一律异步」天然吻合,无需在 `patbond-ai` 内部做后台任务承载。
|
||||||
|
|
||||||
|
## 2. 开工前问的五项,逐条答案
|
||||||
|
|
||||||
|
### ① 是否支持幂等键:**不支持**
|
||||||
|
|
||||||
|
请求参数全集为 `model`、`content`、`omni_reference_task_type`、`resolution`、
|
||||||
|
`ratio`、`duration`、`generate_audio`、`watermark`、`output_format`、
|
||||||
|
`return_last_frame`、`callback_url`、`execution_expires_after`、`priority`、
|
||||||
|
`safety_identifier`。**没有任何幂等相关字段。**
|
||||||
|
|
||||||
|
**后果**:ADR-023 §4 设想的「把我方 `idempotency_key` 透传为厂商幂等头」这条
|
||||||
|
缓解手段**对本供应商不成立**。「提交超时后不知道有没有扣费」的窗口只能由
|
||||||
|
`max_attempts` 兜住。**如实登记,不假装已解决。**
|
||||||
|
|
||||||
|
**两点减轻**(非消除):
|
||||||
|
|
||||||
|
1. 提交是异步的,只入队即返回,**耗时短、超时概率低**——与同步长连接型供应商
|
||||||
|
的风险量级不同。
|
||||||
|
2. 厂商支持 `callback_url`,任务状态变化时 POST 推送(请求体与查询接口返回体
|
||||||
|
一致)。**一个「提交超时但实际已创建」的孤儿任务会通过回调把 ID 送回来**,
|
||||||
|
因此回调在此供应商上不只是延迟优化,还是**孤儿任务回收通道**。这是 ADR-023
|
||||||
|
§4「M4 只做轮询、回调仅预留字段」之外的一个新论据,值得复评。
|
||||||
|
|
||||||
|
### ② 查询接口形态与频率限制
|
||||||
|
|
||||||
|
- 形态:`GET .../tasks/{id}`,返回体见 §3。
|
||||||
|
- **任务记录只保留最近 7 天**(区间 `[T-7天, T)`)。更早的任务查询不到。
|
||||||
|
- 频率限制**未给具体数值**,但错误码表里有多个 429 分支(见 §4),说明确实有限。
|
||||||
|
- **官方 SDK 示例用 10 秒轮询 + 30 分钟截止**。我们的 3s/10s/30s 退避阶梯与之
|
||||||
|
兼容,且厂商自己的示例也用 10 秒——说明这个量级是预期用法。
|
||||||
|
|
||||||
|
### ③ 单任务最长时长与超时行为
|
||||||
|
|
||||||
|
- `execution_expires_after`:默认 **172800 秒(48 小时)**,取值范围
|
||||||
|
**`[3600, 259200]`**(1~72 小时),从 `created_at` 起算。
|
||||||
|
- 超时任务被自动终止并标记为 **`expired`**。
|
||||||
|
- **实现取下限 3600**:48 小时对交互式应用过长,被放弃的任务会在厂商侧挂两天。
|
||||||
|
注意**无法设得更短**——1 小时是厂商下限。
|
||||||
|
- 官方 SDK 示例的客户端截止时间是 **30 分钟**。ADR-023 §10 原先给视频客户端
|
||||||
|
硬超时排的是 15~20 分钟,**建议按 30 分钟对齐厂商示例**。
|
||||||
|
|
||||||
|
### ④ 产物返回形式:**有效期 24 小时的 URL**
|
||||||
|
|
||||||
|
- `content.video_url`:生成视频 URL,**有效期 24 小时**;Seedance 2.5 另有
|
||||||
|
**下载次数上限 100 次**。
|
||||||
|
- `content.last_frame_url`:尾帧图(png、尺寸与视频一致、**无水印**),仅在提交
|
||||||
|
时 `return_last_frame: true` 才返回。
|
||||||
|
|
||||||
|
**这一条对挂起中的存储决策是硬约束**:厂商 URL **不能**当存储用。把它存进
|
||||||
|
`media.assets.external_url` 会在 24 小时后变成死链,因此**无论最终选本地存储
|
||||||
|
还是云存储,都必须在 24 小时内把文件复制到我们自己的存储**。「直接存厂商 URL」
|
||||||
|
不是一个可选项。
|
||||||
|
|
||||||
|
**两个顺带的好处**:
|
||||||
|
|
||||||
|
- `last_frame_url` 可**直接用作视频封面图**,省掉我们自己抽帧。`iteration-4/03`
|
||||||
|
R13 把「缩略图」列为视频进 M4 的成本项之一,这一项实际是免费的。
|
||||||
|
- 查询返回 `ratio`、`resolution`、`framespersecond`、`duration`/`frames`,
|
||||||
|
**Worker 写产物时维度与时长都在手里、无需解码**。这正是 ADR-023 §11 所说
|
||||||
|
「AI 输出路径不需要图片解码」的具体依据,`widthPx`/`heightPx` 债在 AI 产物
|
||||||
|
上可零成本修掉。
|
||||||
|
|
||||||
|
**一个精度陷阱**:查询返回的 `duration` 是**向下取整的约数**——官方公式为
|
||||||
|
`duration = 实际总帧数 / 24` 向下取整,例如 133 帧实际 5.54 秒会返回 5。且
|
||||||
|
**`duration` 与 `frames` 只会返回其中一个**(提交时指定了 `frames` 才返回
|
||||||
|
`frames`)。故实现优先按 `frames / framespersecond` 精确换算,仅在只有
|
||||||
|
`duration` 时退回整数秒并**明确标注为约数**。
|
||||||
|
|
||||||
|
### ⑤ 限流与配额响应形态
|
||||||
|
|
||||||
|
见 §4 的完整映射。核心结论:**两族必须分开**,且 `QuotaExceeded` 一码两义。
|
||||||
|
|
||||||
|
## 3. 查询返回体的关键字段
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "cgt-2026****-****",
|
||||||
|
"model": "doubao-seedance-2-0-260128",
|
||||||
|
"status": "succeeded",
|
||||||
|
"content": { "video_url": "https://...", "last_frame_url": "https://...png" },
|
||||||
|
"usage": { "completion_tokens": 108900, "total_tokens": 108900 },
|
||||||
|
"created_at": 1779348818,
|
||||||
|
"updated_at": 1779348874,
|
||||||
|
"resolution": "720p", "ratio": "16:9",
|
||||||
|
"duration": 5, "framespersecond": 24,
|
||||||
|
"execution_expires_after": 172800,
|
||||||
|
"error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`usage.completion_tokens` 可作**计费对账依据**(视频生成不统计输入 token,故
|
||||||
|
`total_tokens == completion_tokens`)。Seedance 2.0 系列有**最低 token 用量**,
|
||||||
|
实际低于下限时按下限计费。
|
||||||
|
|
||||||
|
### ⚠️ 状态枚举在两份文档里不一致,必须按宽的实现
|
||||||
|
|
||||||
|
| 来源 | 列出的状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| 查询接口文档 | `queued` / `running` / `cancelled` / `succeeded` / `failed` |
|
||||||
|
| 创建接口的 `callback_url` 一节 | `queued` / `running` / `succeeded` / `failed` / **`expired`** |
|
||||||
|
|
||||||
|
**只按查询文档实现,线上遇到超时任务会掉进未知分支。** 实现按**六个**状态处理,
|
||||||
|
并把 `expired` 当作**可重试的失败**(是超时而非内容或参数问题);另注意厂商在
|
||||||
|
`expired` 时不一定给 `error.code`,故单独分类而非走通用路径。
|
||||||
|
|
||||||
|
另:`cancelled` **只支持取消 `queued` 状态的任务**,且取消状态 24 小时自动删除。
|
||||||
|
这意味着我方「取消正在 running 的任务」无法向厂商传导——**已 running 的任务取消
|
||||||
|
后仍会计费**。ADR-023 §6 关于取消的说明据此可以定稿:取消只改我方任务记录。
|
||||||
|
|
||||||
|
## 4. 错误码映射
|
||||||
|
|
||||||
|
实现在 `app/providers/volcengine/errors.py`,测试在
|
||||||
|
`tests/providers/volcengine/test_errors.py`(32 项)。
|
||||||
|
|
||||||
|
### 🔴 `QuotaExceeded` 一码两义
|
||||||
|
|
||||||
|
官方错误码表里有**两行 code 完全相同**的 `QuotaExceeded`:
|
||||||
|
|
||||||
|
| message | 含义 | 处置 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `The request has exceeded the quota.` | 当前账号**排队中任务数**超限 | **等几秒**,退避重试 |
|
||||||
|
| `...exhausted its free trial quota...` / `...exceeded the 5-hour/weekly/monthly usage quota...` | 额度耗尽 | **等到明天或充值**,重试无用 |
|
||||||
|
|
||||||
|
**按 code 一刀切必然有一半场景的文案和出口是错的。** 实现只能按 message 判别
|
||||||
|
(没有别的字段可依据),判别式与两条用例都在测试里钉住。
|
||||||
|
|
||||||
|
### 分族
|
||||||
|
|
||||||
|
| 族 | 代表码 | 重试 | 用户侧 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **内容被拒** | `*SensitiveContentDetected*`(输入/输出 × 文本/图片/视频/音频 × PolicyViolation / PrivacyInformation / DeepFake 等子类)、`*RiskDetection` | **NEVER** | 换提示词或换图 |
|
||||||
|
| **限流** | `RateLimitExceeded.Endpoint{RPM,TPM,FlexTPM}Exceeded`、`ModelAccount{Rpm,Tpm,FlexTpm,Ipm}RateLimitExceeded`、`APIAccountRpmRateLimitExceeded`、`AccountRateLimitExceeded`、`ServerOverloaded`、`RequestBurstTooFast` | BACKOFF | 稍后重试(SnackBar) |
|
||||||
|
| **配额/计费** | `SetLimitExceeded`、`InflightBatchsizeExceeded`、`AccountOverdueError`、`OperationDenied.Service{Overdue,NotOpen}`、`ModelNotOpen` | QUOTA | 去充值/开通(sheet) |
|
||||||
|
| **我方故障** | `AuthenticationError`、`InvalidEndpointOrModel.NotFound`、`InvalidParameter*`、`MissingParameter*`、`InvalidParameter.TaskType{Constraint,Mismatch}` | **NEVER** | 通用失败 |
|
||||||
|
| **上游内部** | `InternalServiceError` | BACKOFF | 稍后重试 |
|
||||||
|
|
||||||
|
两处容易搞错的地方,实现里各有测试钉住:
|
||||||
|
|
||||||
|
- **`ContentSecurityDetectionError` 不属于内容被拒**——它是风险识别产品自身请求
|
||||||
|
失败,不是内容真的违规,**可重试**。
|
||||||
|
- **内容安全族用前缀匹配而非穷举**:该族的码在持续增加(Input/Output × 媒体类型
|
||||||
|
× 子原因),穷举必然漏,而整族处置方式完全一致。
|
||||||
|
- **未知码一律按可退避重试处理**,不判死:厂商在持续新增码,把未知码当终态失败
|
||||||
|
会让一次可恢复的抖动变成用户看到的失败;重试预算本身有上限,最坏只多试两次。
|
||||||
|
|
||||||
|
**403 的欠费/未开通归入配额族**:HTTP 状态码不同(403 而非 429),但用户要做的
|
||||||
|
事一样——去充值。按状态码分族会把它们错分。
|
||||||
|
|
||||||
|
## 5. 模型与能力矩阵
|
||||||
|
|
||||||
|
| 模型 | 文生视频 | 图生视频-首帧 | 首尾帧 | 全模态参考 | 有声 |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| Seedance 2.5 | ✓ | ✓ | ✓ | ✓(图 0-30 / 视频 0-10 / 音频 0-10,可仅音频) | ✓ |
|
||||||
|
| Seedance 2.0 系列 | ✓ | ✓ | ✓ | ✓(图 0-9 / 视频 0-3 / 音频 0-3,不可仅音频) | ✓ |
|
||||||
|
| Seedance 1.5 pro | ✓ | ✓ | ✓ | — | ✓ |
|
||||||
|
| Seedance 1.0 pro | ✓ | ✓ | ✓ | — | — |
|
||||||
|
| Seedance 1.0 pro fast | ✓ | ✓ | — | — | — |
|
||||||
|
|
||||||
|
分辨率与宽高比的可选值**随模型不同**(2.5 无 4k,2.0 fast/mini 只到 720p;
|
||||||
|
2.5 在首帧/首尾帧、视频编辑、视频延长任务中 **`ratio` 只支持 `adaptive`**)。
|
||||||
|
时长范围:2.5 为 `[4, 30]`,2.0 系列为 `[4, 15]`,均支持 `-1` 智能选择。
|
||||||
|
|
||||||
|
文档出现过的 model ID 示例:`doubao-seedance-2-5-260628`、
|
||||||
|
`doubao-seedance-2-0-260128`、`doubao-seedance-1-0-pro-250528`、
|
||||||
|
`doubao-seedance-1-0-lite-i2v-250428`。
|
||||||
|
|
||||||
|
### 账号实际开通的模型(2026-09-20 确认)
|
||||||
|
|
||||||
|
| model ID | 目录 code | 关键限制 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `doubao-seedance-2-5-260628` | `seedance-2-5` | 时长 `[4,30]`;分辨率 480p/720p/1080p(**无 4k**);**图生视频时 `ratio` 只支持 `adaptive`**;支持 `output_format` mp4/mov;1080p 为 10bit + H.265/HEVC |
|
||||||
|
| `doubao-seedance-2-0-260128` | `seedance-2-0` | 时长 `[4,15]`;分辨率 480p/720p/1080p/**4k**;`ratio` 可指定;不支持 `output_format`;4k 为 10bit + H.265/HEVC |
|
||||||
|
|
||||||
|
已由 **V7 目录种子**播种(`provider_code = 'volcengine-seedance'`)。注意与目标
|
||||||
|
模型 `patbond_postgresql.sql:1495` 的种子**有意不同**——正典那份是服务商未定时
|
||||||
|
写下的 `provider_code='fixture'` 占位。
|
||||||
|
|
||||||
|
**两个模型的限制差异不进目录表**(正典的 `generation_models` 没有能力列,本迭代
|
||||||
|
不发明超出已评审模型的列),而是由 `patbond-ai` 的
|
||||||
|
`app/providers/volcengine/capabilities.py` 承载并在提交前**本地预校验**。这样
|
||||||
|
超范围的请求不必花一次往返去换一个厂商 400——而那一次往返还会计入 RPM 并表现为
|
||||||
|
一个失败的任务。按**模型族前缀**匹配而非完整 ID,故一次小版本升级(如
|
||||||
|
`2-5-261231`)不需要改表;**未知模型一律放行**,使「账号开通即可用」不退化为
|
||||||
|
「开通还得改代码发版」。
|
||||||
|
|
||||||
|
同一组参数在两个模型上一个合法一个不合法(如 20 秒时长、4k 分辨率),这正是
|
||||||
|
需要这张表的理由,测试里两个方向都钉住了。
|
||||||
|
|
||||||
|
**风格表(`generation_styles`)刻意未播种**:风格是产品内容决策(提供哪些艺术
|
||||||
|
风格、叫什么、配图是什么),无法从服务商文档推导;正典那 4 条风格均为 `image`
|
||||||
|
类且其 `preview_asset_id` 指向 demo 资产,而 V1..V7 链路不含该 demo 种子,照抄
|
||||||
|
会违反外键。`generation_jobs.style_id` 可空,故空风格表不阻塞任何功能。
|
||||||
|
|
||||||
|
**图片模型同样未播种**:gpt-image-2 的适配层已落地(见
|
||||||
|
[OpenAI gpt-image-2](openai-gpt-image.md)),但**产物转存仍阻塞于存储决策**,
|
||||||
|
故仍不播种——播种一个拿不到产物的模型会让客户端渲染出一个点了没反应的入口
|
||||||
|
(目录端点按 `media_kind` 聚合可用模型,故当前只返回 `video`)。
|
||||||
|
|
||||||
|
## 6. 实现状态
|
||||||
|
|
||||||
|
`patbond-ai` 已按高内聚低耦合分层(`config → assembly → providers → domain`),
|
||||||
|
分层约束由 `tests/test_layering.py` 用 AST 自动守着。以后接火山的图片/语音接口
|
||||||
|
应新建兄弟模块复用同一个 `ArkClient`,而不是再抄一遍鉴权与错误解包。
|
||||||
|
|
||||||
|
| 项 | 状态 |
|
||||||
|
| --- | --- |
|
||||||
|
| 错误分类(`providers/volcengine/errors.py`) | ✅ 32 测试 |
|
||||||
|
| HTTP 传输与鉴权(`providers/volcengine/client.py`) | ✅ 15 测试,全部离线(MockTransport) |
|
||||||
|
| 视频业务(`providers/volcengine/video.py`) | ✅ 提交与查询 + 状态映射 |
|
||||||
|
| 模型限制预校验(`providers/volcengine/capabilities.py`) | ✅ 23 测试 |
|
||||||
|
| V7 目录种子(两个已开通模型) | ✅ 6 测试(含 `provider_code` 跨语言一致性) |
|
||||||
|
| 产物转存 | ⏸ 阻塞于存储决策(ADR-023 §7)。注意 §2-④ 的 24 小时硬约束 |
|
||||||
|
| HTTP 端点与 `openapi-internal.yaml` | ✅ 提交/查询 + 服务身份 JWT 校验 |
|
||||||
|
| Java 侧 provider 适配 | ✅ `patbond-creation` 的 `provider/ai/`(Feign + snake_case) |
|
||||||
|
|
||||||
|
**一处跨语言约定,两侧都有测试钉住**:目录表的 `provider_code` 必须与
|
||||||
|
`patbond-ai` 适配器的 `PROVIDER_CODE` **逐字一致**(`volcengine-seedance`)。
|
||||||
|
对不上时 Worker 按任务行冻结的快照查不到适配器,任务会永远停在排队——而那看起来
|
||||||
|
像队列坏了,不像两个字符串不一致。
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
| 仓库 | tag | 提交 | 内容 |
|
| 仓库 | tag | 提交 | 内容 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| patbond-api | `v0.4.0` | `3cd8005` | 用户资料读写 + 宠物头像 + 社区统计;379 测试;契约 v1.4.0 快照 |
|
| patbond-api | `v0.4.0` | `3cd8005` | 用户资料读写 + 宠物头像 + 社区统计;381 测试;契约 v1.4.0 快照 |
|
||||||
| patbond-flutter | `v0.4.0` | `fbcd734` | 中文本地化 + 日期录入收口 + 资料页/编辑页 + 头像接线;597 测试;四份 E2E 脚本 |
|
| patbond-flutter | `v0.4.0` | `fbcd734` | 中文本地化 + 日期录入收口 + 资料页/编辑页 + 头像接线;597 测试;四份 E2E 脚本 |
|
||||||
| patbond-doc | `v0.4.0` | 本记录所在提交 | 契约 v1.4.0 + M3.5 报告 01~08 + 安全事件复盘 + 服务器暴露面清单 |
|
| patbond-doc | `v0.4.0` | 本记录所在提交 | 契约 v1.4.0 + M3.5 报告 01~08 + 安全事件复盘 + 服务器暴露面清单 |
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
|
|
||||||
| 门禁项 | 结果 | 证据 |
|
| 门禁项 | 结果 | 证据 |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 全量测试 | ✅ | api **379** / flutter **597**,双绿 |
|
| 全量测试 | ✅ | api **381** / flutter **597**(+2 skipped),双绿 |
|
||||||
| **E2E 回归(四份,同环境串行)** | ✅ | M1 7/7 + M2 11/11 + M3 14/14 + **M3.5 10/10** = **42/42 场景、234 断言零失败** — [08 号报告](iterations/iteration-3.5/08-release-e2e-regression.md) |
|
| **E2E 回归(四份,同环境串行)** | ✅ | M1 7/7 + M2 11/11 + M3 14/14 + **M3.5 10/10** = **42/42 场景零失败**;断言数机械可数 **226**(210 `check(` + M1 16 `✗`),原记「234」无可复核来源、已停止引用(脚本无计数器,循环内断言使运行时数可能更高——补计数器见 iteration-4/04) — [08 号报告](iterations/iteration-3.5/08-release-e2e-regression.md) |
|
||||||
| 契约偏差 | ✅ **0** | 逐场景对照 v1.4.0;契约计数机械复核 32/45/75 |
|
| 契约偏差 | ✅ **0** | 逐场景对照 v1.4.0;契约计数机械复核 32/45/75 |
|
||||||
| 契约向后兼容 | ✅ | v1.3.0→v1.4.0 纯增量,v1.3.0 客户端无需改动 |
|
| 契约向后兼容 | ✅ | v1.3.0→v1.4.0 纯增量,v1.3.0 客户端无需改动 |
|
||||||
| 契约一致性矩阵 | ✅ | 173→**181 格**,11 格「未声明字段即漂移」的红全部转绿,mutation 三处注毒自证 |
|
| 契约一致性矩阵 | ✅ | 173→**181 格**,11 格「未声明字段即漂移」的红全部转绿,mutation 三处注毒自证 |
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ nav:
|
|||||||
- 发布记录: development/releases.md
|
- 发布记录: development/releases.md
|
||||||
- 服务器暴露面清单: development/server-exposure.md
|
- 服务器暴露面清单: development/server-exposure.md
|
||||||
- CI Runner 部署手册: development/ci-runner-setup.md
|
- CI Runner 部署手册: development/ci-runner-setup.md
|
||||||
|
- 服务商接口:
|
||||||
|
- 火山方舟 Seedance(视频): development/providers/volcengine-seedance.md
|
||||||
|
- OpenAI gpt-image-2(图片): development/providers/openai-gpt-image.md
|
||||||
- 第一迭代:
|
- 第一迭代:
|
||||||
- 进展看板: development/iterations/iteration-1/index.md
|
- 进展看板: development/iterations/iteration-1/index.md
|
||||||
- 01 任务分解: development/iterations/iteration-1/01-pm-task-breakdown.md
|
- 01 任务分解: development/iterations/iteration-1/01-pm-task-breakdown.md
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
# check-secrets.sh —— 凭证防泄漏检查(ADR-021,规范见 patbond-doc docs/development/git-workflow.md)
|
# check-secrets.sh —— 凭证防泄漏检查(ADR-021,规范见 patbond-doc docs/development/git-workflow.md)
|
||||||
#
|
#
|
||||||
# 规则单一来源:本地 pre-commit 与 CI 兜底跑的是同一个脚本、同一张规则表。
|
# 规则单一来源:本地 pre-commit 与 CI 兜底跑的是同一个脚本、同一张规则表。
|
||||||
# 三仓(patbond-api / patbond-flutter / patbond-doc)各存一份同构副本,改规则时三仓同步。
|
# 四仓(patbond-api / patbond-flutter / patbond-doc / patbond-ai)各存一份同构副本,
|
||||||
|
# 改规则时四仓同步提交。patbond-ai 是唯一持有 AI 服务商凭证的仓,尤其不可漏。
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# sh scripts/check-secrets.sh --staged # pre-commit:扫暂存区内容(经 scripts/hooks/pre-commit 调用)
|
# sh scripts/check-secrets.sh --staged # pre-commit:扫暂存区内容(经 scripts/hooks/pre-commit 调用)
|
||||||
@@ -19,6 +20,15 @@ mode="${1:---all}"
|
|||||||
# 放行它使「讲解规则的文档」不会被规则自身拦下(如 iteration-4/08 §漏检清单)。
|
# 放行它使「讲解规则的文档」不会被规则自身拦下(如 iteration-4/08 §漏检清单)。
|
||||||
ALLOW='\$\{[^}]*\}|\{\{[^}]*\}\}|changeme|change[-_]me|your[-_][a-zA-Z0-9_-]+|<[a-zA-Z0-9 ,_.-]+>|placeholder|example|sample|dummy|fake|redacted|\*\*\*|x{3,}'
|
ALLOW='\$\{[^}]*\}|\{\{[^}]*\}\}|changeme|change[-_]me|your[-_][a-zA-Z0-9_-]+|<[a-zA-Z0-9 ,_.-]+>|placeholder|example|sample|dummy|fake|redacted|\*\*\*|x{3,}'
|
||||||
|
|
||||||
|
# 依赖注入的传参不是字面量密钥:`api_key=settings.api_key`、
|
||||||
|
# `.param("secretKey", props.getSecretKey())` 这类行里没有任何秘密,而做 DI 的
|
||||||
|
# 代码到处都是这种写法——不放行就得在每个调用点绕过一次。
|
||||||
|
# 收得很紧,避免把真凭证放进来:
|
||||||
|
# * 值必须是**未加引号**的小写起首标识符引用(真密钥要么带引号,要么不是标识符形状)
|
||||||
|
# * 只允许**恰好一个点**且随即行尾(JWT 是 a.b.c 三段,仍会被拦)
|
||||||
|
# * 连字符不在字符集内(sk-ant-… 这类前缀仍会被拦)
|
||||||
|
ALLOW_REFERENCE='[:=][[:space:]]*[a-z][a-zA-Z0-9_]*\.[a-z][a-zA-Z0-9_]*[[:space:]]*[,);]?$'
|
||||||
|
|
||||||
# 内容扫描跳过:本脚本与 hook 自身(含规则文本,非凭证)
|
# 内容扫描跳过:本脚本与 hook 自身(含规则文本,非凭证)
|
||||||
SKIP_PATHS='(^|/)scripts/(check-secrets\.sh|hooks/pre-commit)$'
|
SKIP_PATHS='(^|/)scripts/(check-secrets\.sh|hooks/pre-commit)$'
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ for f in $files; do
|
|||||||
ci=""
|
ci=""
|
||||||
[ "$flag" = "i" ] && ci="-i"
|
[ "$flag" = "i" ] && ci="-i"
|
||||||
grep -InE $ci -e "$pat" "$tmp" 2>/dev/null | grep -viE "$ALLOW" |
|
grep -InE $ci -e "$pat" "$tmp" 2>/dev/null | grep -viE "$ALLOW" |
|
||||||
|
grep -vE "$ALLOW_REFERENCE" |
|
||||||
sed "s|^|[$id] $f:|" >>"$viol" || true
|
sed "s|^|[$id] $f:|" >>"$viol" || true
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -100,7 +111,7 @@ if [ -s "$viol" ]; then
|
|||||||
处置:
|
处置:
|
||||||
1. 若是真实凭证:先去云控制台轮换/禁用该密钥,再从提交中移除;
|
1. 若是真实凭证:先去云控制台轮换/禁用该密钥,再从提交中移除;
|
||||||
2. 若是误报:改用 ${} 注入或占位值(changeme / your-xxx / <占位>),
|
2. 若是误报:改用 ${} 注入或占位值(changeme / your-xxx / <占位>),
|
||||||
或与团队确认后调整三仓同构的 scripts/check-secrets.sh 规则表。
|
或与团队确认后调整四仓同构的 scripts/check-secrets.sh 规则表。
|
||||||
敏感信息只允许存在于被 gitignore 的文件或 .sample 占位中(git-workflow.md)。
|
敏感信息只允许存在于被 gitignore 的文件或 .sample 占位中(git-workflow.md)。
|
||||||
MSG
|
MSG
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Reference in New Issue
Block a user