diff --git a/CHANGELOG.md b/CHANGELOG.md index c57dc315c..7a8c982cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ # CHANGELOG +## v1.6.7 + +- 修复旧版工作台曾占用 Schema v51 导致本地启动和线上更新校验失败的问题;迁移器会在精确匹配旧账本且工作台表完整时自动无损归一到 v54,再继续执行 v51–v53。 +- 修复服务端出站 URL 对 DNS 解析结果的私网 IP 拦截被注释的问题,恢复对环回、内网和链路本地地址的拒绝,避免 SSRF。 +- 修复 Quality checks 中工作台测试模块路径、懒加载画布路由合同、模型促销标签和影视剧作工坊完整性基线问题。 +- 正式镜像包含影视剧作工坊内置 Skill;服务启动时同步技能目录、版本和文件记录,并保留用户加入/收藏状态。 +- 升级注意:数据库 Schema 为 v54;生产 Compose 的 `migrate` 服务会在 Backend 启动前执行 `migrate-schema up`,旧 v51 工作台账本会自动兼容升级,无需手工修改 `schema_migrations`。 + ## v1.6.6.1 +- 修复旧版工作台曾占用 Schema v51 导致本地启动和线上更新校验失败的问题;迁移器会在精确匹配旧账本且表完整时自动无损归一到 v54,再继续执行 v51–v53。 - 修复服务端出站 URL 对 DNS 解析结果的私网 IP 拦截被注释的问题,恢复对环回、内网和链路本地地址的拒绝,避免 SSRF。 - 修复质量检查:工作台测试模块路径、懒加载画布路由合同和模型促销标签断言;修正影视剧作工坊内置脚本树完整性基线。 - 修复创作页上一条任务仍在生成时无法提交下一条任务的问题。 diff --git a/VERSION b/VERSION index 631f57b5a..a28c543f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v1.6.6.1 +v1.6.7 diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go index 725a04da8..829db7d66 100644 --- a/backend/internal/database/migrations.go +++ b/backend/internal/database/migrations.go @@ -29,6 +29,9 @@ const prefixedIDSequenceReconcileChecksum = "sha256:prefixed-id-sequence-reconci const skillLibraryCategoriesChecksum = "sha256:skill-library-categories-v39-20260926" const builtinSkillTombstonesChecksum = "sha256:builtin-skill-tombstones-v40-20260927" const resourceThumbnailChecksum = "sha256:resource-thumbnail-v41-20260927" +const workbenchToolsMigrationName = "workbench_tools_and_presets" +const workbenchToolsMigrationChecksum = "sha256:workbench-tools-presets-v54-20261014" +const legacyWorkbenchToolsMigrationChecksum = "sha256:workbench-tools-presets-v51-20261014" const postgresSchemaMigrationLockID int64 = 73123910420260830 @@ -178,7 +181,7 @@ var schemaMigrations = []migration{ {version: 53, name: "user_login_logs", checksum: "sha256:user-login-logs-v53", apply: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.LoginLog{}) }}, - {version: 54, name: "workbench_tools_and_presets", checksum: "sha256:workbench-tools-presets-v54-20261014", apply: func(tx *gorm.DB) error { + {version: 54, name: workbenchToolsMigrationName, checksum: workbenchToolsMigrationChecksum, apply: func(tx *gorm.DB) error { return tx.AutoMigrate(&model.WorkbenchTool{}, &model.WorkbenchPreset{}, &model.Task{}) }}, } @@ -510,6 +513,47 @@ func migrateSchemaV12(tx *gorm.DB) error { return nil } +func normalizeLegacyWorkbenchMigration(tx *gorm.DB) error { + var legacy schemaMigration + if err := tx.First(&legacy, "version = ?", 51).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return fmt.Errorf("读取旧工作台迁移记录:%w", err) + } + if legacy.Name != workbenchToolsMigrationName || legacy.Checksum != legacyWorkbenchToolsMigrationChecksum { + return nil + } + + var later []schemaMigration + if err := tx.Where("version > ?", legacy.Version).Order("version asc").Find(&later).Error; err != nil { + return fmt.Errorf("读取旧工作台迁移后的记录:%w", err) + } + if len(later) > 0 { + return fmt.Errorf("数据库迁移 51 使用旧工作台身份,但已存在后续迁移记录 %d;拒绝自动改写迁移账本", later[0].Version) + } + if !tx.Migrator().HasTable(&model.WorkbenchTool{}) || !tx.Migrator().HasTable(&model.WorkbenchPreset{}) { + return fmt.Errorf("数据库迁移 51 记录为旧工作台迁移,但工作台表不完整;请从备份恢复后再升级") + } + + result := tx.Model(&schemaMigration{}). + Where("version = ? AND name = ? AND checksum = ?", legacy.Version, legacy.Name, legacy.Checksum). + Updates(map[string]any{ + "version": workbenchToolsMigrationVersion, + "name": workbenchToolsMigrationName, + "checksum": workbenchToolsMigrationChecksum, + }) + if result.Error != nil { + return fmt.Errorf("归一化旧工作台迁移记录:%w", result.Error) + } + if result.RowsAffected != 1 { + return fmt.Errorf("归一化旧工作台迁移记录影响 %d 行,拒绝继续启动", result.RowsAffected) + } + return nil +} + +const workbenchToolsMigrationVersion int64 = 54 + func MigrateSchema(db *gorm.DB) error { return db.Transaction(func(tx *gorm.DB) error { if tx.Dialector.Name() == "postgres" { @@ -520,6 +564,9 @@ func MigrateSchema(db *gorm.DB) error { if err := tx.AutoMigrate(&schemaMigration{}); err != nil { return fmt.Errorf("初始化数据库迁移记录:%w", err) } + if err := normalizeLegacyWorkbenchMigration(tx); err != nil { + return err + } plan, err := migrationsForDatabase(tx) if err != nil { return err diff --git a/backend/internal/database/migrations_test.go b/backend/internal/database/migrations_test.go index 48110879f..e493f095e 100644 --- a/backend/internal/database/migrations_test.go +++ b/backend/internal/database/migrations_test.go @@ -98,6 +98,53 @@ func TestMigrateSchemaRecordsAndValidatesVersion(t *testing.T) { } } +func TestMigrateSchemaNormalizesLegacyWorkbenchMigrationV51(t *testing.T) { + db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-legacy-workbench-v51?mode=memory&cache=shared"}) + if err != nil { + t.Fatal(err) + } + if err := MigrateSchema(db); err != nil { + t.Fatal(err) + } + if err := db.Where("version IN ?", []int64{51, 52, 53}).Delete(&schemaMigration{}).Error; err != nil { + t.Fatal(err) + } + if err := db.Model(&schemaMigration{}).Where("version = ?", 54).Updates(map[string]any{ + "version": 51, + "name": workbenchToolsMigrationName, + "checksum": legacyWorkbenchToolsMigrationChecksum, + }).Error; err != nil { + t.Fatal(err) + } + + if err := MigrateSchema(db); err != nil { + t.Fatalf("legacy workbench migration should be normalized: %v", err) + } + status, err := ReadSchemaStatus(db) + if err != nil { + t.Fatal(err) + } + if !status.Ready || status.Current != CurrentSchemaVersion { + t.Fatalf("unexpected normalized schema status: %#v", status) + } + var normalized schemaMigration + if err := db.First(&normalized, "version = ?", workbenchToolsMigrationVersion).Error; err != nil { + t.Fatal(err) + } + if normalized.Name != workbenchToolsMigrationName || normalized.Checksum != workbenchToolsMigrationChecksum { + t.Fatalf("normalized workbench migration = %#v", normalized) + } + for _, item := range schemaMigrations { + var count int64 + if err := db.Model(&schemaMigration{}).Where("version = ? AND name = ? AND checksum = ?", item.version, item.name, item.checksum).Count(&count).Error; err != nil { + t.Fatal(err) + } + if item.version != workbenchToolsMigrationVersion && count != 1 { + t.Fatalf("migration %d was not restored: %#v", item.version, item) + } + } +} + func TestMigrateSchemaV35UpgradesExistingDatabase(t *testing.T) { db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-auth-notifications-v35?mode=memory&cache=shared"}) if err != nil { diff --git a/docs/content/docs/backend/backend-database.mdx b/docs/content/docs/backend/backend-database.mdx index c471f632f..b63c49cd9 100644 --- a/docs/content/docs/backend/backend-database.mdx +++ b/docs/content/docs/backend/backend-database.mdx @@ -76,6 +76,8 @@ Schema 45 新增 `upload_reservations`:`id`、`user_id`、`size`、`day`、`ex 渠道模型的文本能力 JSON 还可声明 `contextWindowTokens` 与 `maxOutputTokens`。这两个字段是模型能力合同,不是数据库或 Agent checkpoint 的容量配置;旧记录读取时补 128K / 16K 默认值。运行状态、journal 和 transcript 的持久化边界仍由既有 runtime 表及其 512KB checkpoint 校验负责,本阶段没有通过扩大 checkpoint 绕过模型上下文限制。 +历史发布曾使用版本 51 `workbench_tools_and_presets`,后续版本将个人资料安全、头像预设和登录日志依次固定为 51–53,并将工作台迁移规范为 54。迁移器仅在版本 51 的名称为 `workbench_tools_and_presets` 且校验和精确为 `sha256:workbench-tools-presets-v51-20261014`、不存在更高迁移记录、且工作台表完整时,将该已成功应用记录在同一事务中归一到规范版本 54,然后继续应用 51–53;既有应用时间保留。其他未知名称、校验和或部分迁移状态仍拒绝启动,不通过删除、改名或伪造记录绕过检查。 + 历史发布曾使用版本 6 `asset_library_folders`,之后合并引入了上述 6/7 顺序冲突。迁移器仅在该历史记录的名称和校验和均精确匹配时选择历史顺序:保留版本 6 的素材目录记录及时间,在版本 7 应用 `resource_playback_variant`,再继续版本 8。新数据库及已采用“6 播放副本、7 素材目录”的数据库保持原顺序;状态检查与启动校验采用同一套识别规则。未知名称、被改动的校验和或冲突的版本 7 记录仍会拒绝迁移,不通过删除、改名或伪造已应用记录绕过检查。 升级前备份数据库,迁移命令必须沿用部署实际配置的数据库连接和数据目录。开发环境启用自动迁移时重新启动新代码即可;关闭自动迁移的部署先使用新构建的 `migrate-schema up`,然后 `migrate-schema verify`,成功后启动 Backend。迁移与记录写入在事务中执行;此修复不提供降级迁移,回退旧二进制可能无法识别历史顺序,应恢复匹配的备份和版本,不手工改迁移表。