From 91ae0164708d884f19e61fc59a8078c65d74aa03 Mon Sep 17 00:00:00 2001 From: Lixi20 Date: Sun, 20 Sep 2026 15:13:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20V7=20=E7=9B=AE=E5=BD=95=E7=A7=8D?= =?UTF-8?q?=E5=AD=90=E2=80=94=E2=80=94=E4=B8=A4=E4=B8=AA=E5=B7=B2=E5=BC=80?= =?UTF-8?q?=E9=80=9A=E7=9A=84=20Seedance=20=E8=A7=86=E9=A2=91=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=88T4-01b=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按账号实际开通的模型播种:doubao-seedance-2-5-260628 与 doubao-seedance-2-0-260128,provider_code 为 volcengine-seedance。 与目标模型 patbond_postgresql.sql:1495 的种子**有意不同**:正典那份是服务商 未定时写下的 provider_code='fixture' 占位(这也是开工审计据以判定「AI provider BLOCKED」的证据之一),现已接入真实服务商。 三处刻意不做的事,文件头逐条写明原因: - **不播种图片模型**:gpt-image-2 适配层未落地,播种一个没有适配器的模型会让 客户端渲染出一个点了没反应的入口(目录端点按 media_kind 聚合可用模型)。 - **不播种风格**:风格是产品内容决策(提供哪些艺术风格、叫什么、配图是什么), 无法从服务商文档推导;正典那 4 条均为 image 类且 preview_asset_id 指向 demo 资产,而 V1..V7 链路不含该 demo 种子,照抄会违反外键。style_id 可空,不阻塞。 - **不在目录表加能力列**:正典的 generation_models 没有能力列,本迭代不发明 超出已评审模型的列。两个模型的限制差异(2.5 时长到 30 秒但无 4k;2.0 有 4k 但时长上限 15 秒;2.5 图生视频时 ratio 锁 adaptive)由 patbond-ai 的 capabilities.py 承载并在提交前本地预校验。 新增 CreationCatalogSeedTest(6 项),钉住的是几个**跨语言、跨仓库约定**—— 它们对不上时症状都离根因很远: - provider_code 必须与 patbond-ai 适配器的 PROVIDER_CODE 逐字一致。对不上时 Worker 按任务行冻结的快照查不到适配器,任务永远停在排队,看起来像队列坏了。 - provider_model_name 必须是实际开通的 ID,否则撞 InvalidEndpointOrModel.NotFound 且不可重试。 - 目录里不得出现没有适配器的 media_kind。 - 模型 ID 尾部须为日期版本——CatalogRepository 以 provider_model_name 兼作版本 快照,该做法只在 ID 自带版本时成立。 全量 404 → 410,零失败零跳过。另修测试里一处 Java 21 API(List.getFirst()): 项目 release 为 17,本地 JDK 26 不代表能用。 Co-Authored-By: Claude Opus 5 (1M context) --- .../repository/CreationCatalogSeedTest.java | 113 ++++++++++++++++++ .../migration/V7__creation_catalog_seed.sql | 42 +++++++ 2 files changed, 155 insertions(+) create mode 100644 patbond-creation/src/test/java/com/patbond/patbond/creation/repository/CreationCatalogSeedTest.java create mode 100644 patbond-user/src/main/resources/db/migration/V7__creation_catalog_seed.sql diff --git a/patbond-creation/src/test/java/com/patbond/patbond/creation/repository/CreationCatalogSeedTest.java b/patbond-creation/src/test/java/com/patbond/patbond/creation/repository/CreationCatalogSeedTest.java new file mode 100644 index 0000000..8aa807a --- /dev/null +++ b/patbond-creation/src/test/java/com/patbond/patbond/creation/repository/CreationCatalogSeedTest.java @@ -0,0 +1,113 @@ +package com.patbond.patbond.creation.repository; + +import com.patbond.patbond.creation.TestcontainersConfiguration; +import com.patbond.patbond.creation.domain.MediaKind; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * V7 目录种子的内容断言。 + * + *

不只是数行数:这里钉住的是几个**跨语言、跨仓库的约定**,它们一旦对不上, + * 症状都离根因很远: + * + *

+ */ +@SpringBootTest +@Import(TestcontainersConfiguration.class) +@TestPropertySource(properties = "patbond.creation.worker.enabled=false") +class CreationCatalogSeedTest { + + /** + * 与 patbond-ai 的 {@code app/providers/volcengine/video.py} 的 + * {@code PROVIDER_CODE} 同值。**改任一侧必须同时改另一侧。** + */ + private static final String EXPECTED_PROVIDER_CODE = "volcengine-seedance"; + + /** 账号实际开通的两个模型 ID。 */ + private static final List ACTIVATED_MODEL_IDS = List.of( + "doubao-seedance-2-5-260628", + "doubao-seedance-2-0-260128"); + + @Autowired + private JdbcClient jdbcClient; + + @Autowired + private CatalogRepository catalog; + + @Test + void v7SeedsExactlyTheActivatedVideoModels() { + List seeded = jdbcClient.sql(""" + SELECT provider_model_name FROM creation.generation_models + WHERE media_kind = 'video' ORDER BY sort_order + """) + .query(String.class) + .list(); + assertThat(seeded).containsExactlyElementsOf(ACTIVATED_MODEL_IDS); + } + + @Test + void everySeededModelPointsAtTheProviderCodeTheAdapterRegistersUnder() { + List codes = jdbcClient.sql( + "SELECT DISTINCT provider_code FROM creation.generation_models") + .query(String.class) + .list(); + assertThat(codes).containsExactly(EXPECTED_PROVIDER_CODE); + } + + @Test + void seededModelsAreEnabledAndResolvableThroughTheCatalogApi() { + // 经真实查询路径取,而不是直接读表——顺带覆盖 enabled 过滤与排序索引。 + List models = catalog.listEnabledModels(MediaKind.VIDEO); + assertThat(models).hasSize(2); + assertThat(models).extracting(CatalogRepository.ModelRow::providerCode) + .containsOnly(EXPECTED_PROVIDER_CODE); + // sort_order 决定客户端的展示顺序:2.5 在前。 + assertThat(models.get(0).code()).isEqualTo("seedance-2-5"); + } + + @Test + void onlyVideoIsAvailableUntilAnImageAdapterLands() { + // 播种一个没有适配器的 media_kind 会让客户端渲染出一个点了没反应的入口。 + // 图片模型待 gpt-image-2 适配层落地后由后续迁移追加。 + assertThat(catalog.availableMediaKinds()).containsExactly("video"); + } + + @Test + void noStylesAreSeededAndThatDoesNotBlockJobCreation() { + // 风格是产品内容决策,无法从服务商文档推导;style_id 可空,故空表不阻塞。 + Integer styles = jdbcClient.sql( + "SELECT COUNT(*) FROM creation.generation_styles") + .query(Integer.class) + .single(); + assertThat(styles).isZero(); + } + + @Test + void modelIdsCarryTheirVersionSoTheVersionSnapshotIsMeaningful() { + // 目录没有独立版本列,CatalogRepository 以 provider_model_name 兼作版本 + // 快照。这只在模型 ID 自带版本时成立——两个 ID 的尾部即日期版本。 + List models = catalog.listEnabledModels(MediaKind.VIDEO); + assertThat(models).allSatisfy(model -> + assertThat(model.modelVersion()).isEqualTo(model.providerModelName())); + assertThat(models).extracting(CatalogRepository.ModelRow::providerModelName) + .allMatch(name -> name.matches(".*-\\d{6}$"), + "模型 ID 尾部应为 6 位日期版本"); + } +} diff --git a/patbond-user/src/main/resources/db/migration/V7__creation_catalog_seed.sql b/patbond-user/src/main/resources/db/migration/V7__creation_catalog_seed.sql new file mode 100644 index 0000000..967816d --- /dev/null +++ b/patbond-user/src/main/resources/db/migration/V7__creation_catalog_seed.sql @@ -0,0 +1,42 @@ +/* + V7: creation catalog seed (iteration-4 T4-01b)。 + + 目录是生产参照数据(任务表按复合外键指向它),故进版本化迁移链而非 db/dev。 + + 与目标模型 patbond_postgresql.sql:1495 的种子**有意不同**:正典那份用的是 + `provider_code = 'fixture'` 与占位模型名(`patbond-image-v1` / `patbond-video-v1'), + 那是在服务商未定时写下的占位。现已接入真实服务商,故按实际开通的模型播种。 + + Scope notes: + - **只播种视频模型**。图片生成(gpt-image-2)尚未接入,待其适配层落地后由后续 + 迁移追加——播种一个没有适配器的模型会让任务卡在「找不到 provider」,而症状 + (任务永远排队)离根因很远。 + - **不播种风格(generation_styles)**。风格是产品内容决策(提供哪些艺术风格、 + 叫什么、配图是什么),无法从服务商文档推导;正典那 4 条风格均为 `image` 类且 + 其 `preview_asset_id` 指向 demo 资产,而本链路不含该 demo 种子,照抄会违反外键。 + `generation_jobs.style_id` 可空,故空风格表不阻塞任何功能。 + - `provider_code` 必须与 patbond-ai 适配器的 `PROVIDER_CODE` 逐字一致 + (`volcengine-seedance`)。对不上时 Java 侧按任务行冻结的 + `provider_code_snapshot` 查不到适配器,任务会永远停在排队——而这个症状离 + 「两处字符串不一致」这个根因很远,故在此显式提醒。 + - id 使用显式固定 UUID 而非列默认值:种子行需要稳定标识以便后续迁移与测试引用。 + 取 `17000000-0000-7000-8000-0000000000 1x` 段,与正典占位行(…0001/…0002) + 区分开,避免有人拿本链路与正典逐行对比时把两者混为一谈。 + + 两个模型的能力差异**不进本表**:正典的 generation_models 没有能力列,本迁移 + 不发明超出已评审模型的列。差异(2.5 时长上限 30 秒但无 4k;2.0 有 4k 但时长 + 上限 15 秒;2.5 图生视频时 ratio 锁 adaptive)由 patbond-ai 的 + `app/providers/volcengine/capabilities.py` 承载并在提交前本地预校验。 + 取证见 patbond-doc docs/development/providers/volcengine-seedance.md §5。 +*/ + +INSERT INTO creation.generation_models + (id, code, display_name, provider_code, provider_model_name, media_kind, sort_order) +VALUES + -- 2.5:时长可到 30 秒、支持 mov 输出与全模态参考(图 0-30/视频 0-10/音频 0-10); + -- 1080p 为 10bit + H.265/HEVC,少数播放环境可能不兼容。列为首选。 + ('17000000-0000-7000-8000-000000000011', 'seedance-2-5', '豆包 Seedance 2.5', + 'volcengine-seedance', 'doubao-seedance-2-5-260628', 'video', 10), + -- 2.0:时长上限 15 秒,但支持 4k(同为 10bit + H.265/HEVC)。 + ('17000000-0000-7000-8000-000000000012', 'seedance-2-0', '豆包 Seedance 2.0', + 'volcengine-seedance', 'doubao-seedance-2-0-260128', 'video', 20);