chore(merge): 整合最新 main 与统一认证变更
ci / verify (pull_request) Successful in 12m18s
ci / verify (pull_request) Successful in 12m18s
合并远端生产 CI、依赖与 API 文档改动,保留统一认证和 SSF 能力。由于远端生产基线已占用 0062,将尚未发布的身份迁移整理为 0063 至 0068 的最终增量 Schema,移除仅用于开发期草稿升级的破坏性迁移步骤。 验证:go test ./...。其余生产门禁在合并提交后继续执行。
This commit is contained in:
@@ -686,14 +686,10 @@ func newIdentityPairingPostgresTestStore(t *testing.T) *Store {
|
||||
for _, migrationName := range []string{
|
||||
"0001_init.sql",
|
||||
"0061_oidc_server_sessions.sql",
|
||||
"0067_identity_configuration_revisions.sql",
|
||||
"0068_identity_onboarding_exchanges.sql",
|
||||
"0069_identity_pairing_cancellation.sql",
|
||||
"0070_identity_secret_cleanup_queue.sql",
|
||||
"0071_identity_pairing_start_reservation.sql",
|
||||
"0072_identity_secret_cleanup_claim_lifecycle.sql",
|
||||
"0073_identity_pairing_start_reservation_upgrade.sql",
|
||||
"0074_identity_pairing_error_categories.sql",
|
||||
"0065_identity_configuration_revisions.sql",
|
||||
"0066_identity_onboarding_exchanges.sql",
|
||||
"0067_identity_secret_cleanup_queue.sql",
|
||||
"0068_identity_pairing_start_reservation.sql",
|
||||
} {
|
||||
migration, err := os.ReadFile(filepath.Join(migrationDirectory, migrationName))
|
||||
if err != nil {
|
||||
|
||||
@@ -16,81 +16,53 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestIdentitySecretCleanupClaimLifecycleUpgradeMigrationExists(t *testing.T) {
|
||||
payload, err := os.ReadFile(identitySecretCleanupMigrationPath(t, "0072_identity_secret_cleanup_claim_lifecycle.sql"))
|
||||
const identitySecretCleanupMigration = "0067_identity_secret_cleanup_queue.sql"
|
||||
|
||||
func TestIdentitySecretCleanupMigrationDefinesClaimLifecycle(t *testing.T) {
|
||||
payload, err := os.ReadFile(identitySecretCleanupMigrationPath(t, identitySecretCleanupMigration))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := strings.ToLower(string(payload))
|
||||
for _, required := range []string{
|
||||
"add column if not exists status text",
|
||||
"add column if not exists claim_token uuid",
|
||||
"add column if not exists lease_expires_at timestamptz",
|
||||
"alter column status set default 'pending'",
|
||||
"alter column status set not null",
|
||||
"gateway_identity_secret_cleanup_status_check",
|
||||
"create table if not exists gateway_identity_secret_cleanup_queue",
|
||||
"secret_ref text primary key",
|
||||
"status text not null default 'pending'",
|
||||
"claim_token uuid",
|
||||
"lease_expires_at timestamptz",
|
||||
"gateway_identity_secret_cleanup_claim_check",
|
||||
"drop index if exists idx_gateway_identity_secret_cleanup_due",
|
||||
"create index idx_gateway_identity_secret_cleanup_due",
|
||||
"idx_gateway_identity_secret_cleanup_due",
|
||||
} {
|
||||
if !strings.Contains(content, required) {
|
||||
t.Fatalf("identity Secret cleanup lifecycle migration is missing %q", required)
|
||||
t.Fatalf("identity Secret cleanup migration is missing %q", required)
|
||||
}
|
||||
}
|
||||
for _, forbidden := range []string{"secret_value", "client_secret", "machine_secret", "token text"} {
|
||||
for _, forbidden := range []string{"secret_value", "client_secret", "machine_secret", "token text", "drop ", "set not null"} {
|
||||
if strings.Contains(content, forbidden) {
|
||||
t.Fatalf("identity Secret cleanup lifecycle migration stores forbidden value field %q", forbidden)
|
||||
t.Fatalf("identity Secret cleanup migration contains forbidden content %q", forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentitySecretCleanupClaimLifecycleUpgradeMigratesLegacyQueue(t *testing.T) {
|
||||
func TestIdentitySecretCleanupMigrationSupportsClaimLifecycle(t *testing.T) {
|
||||
db := newIdentitySecretCleanupMigrationPostgresStore(t)
|
||||
ctx := context.Background()
|
||||
reference := "identity-cleanup-legacy-" + uuid.NewString()
|
||||
applyIdentitySecretCleanupMigration(t, ctx, db.pool, identitySecretCleanupMigration)
|
||||
reference := "identity-cleanup-" + uuid.NewString()
|
||||
|
||||
if _, err := db.pool.Exec(ctx, `
|
||||
CREATE TABLE gateway_identity_secret_cleanup_queue (
|
||||
secret_ref text PRIMARY KEY CHECK (secret_ref ~ '^[a-z0-9][a-z0-9-]{0,127}$'),
|
||||
not_before timestamptz NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);`); err != nil {
|
||||
t.Fatalf("create legacy identity Secret cleanup queue: %v", err)
|
||||
if err := db.QueueIdentitySecretCleanup(ctx, reference, time.Now().Add(-time.Minute)); err != nil {
|
||||
t.Fatalf("queue identity Secret cleanup: %v", err)
|
||||
}
|
||||
if _, err := db.pool.Exec(ctx, `CREATE INDEX idx_gateway_identity_secret_cleanup_due
|
||||
ON gateway_identity_secret_cleanup_queue(not_before,updated_at)`); err != nil {
|
||||
t.Fatalf("create legacy identity Secret cleanup due index: %v", err)
|
||||
}
|
||||
if _, err := db.pool.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
|
||||
VALUES($1,now()-interval '1 minute')`, reference); err != nil {
|
||||
t.Fatalf("seed legacy identity Secret cleanup row: %v", err)
|
||||
}
|
||||
|
||||
applyIdentitySecretCleanupMigration(t, ctx, db.pool, "0072_identity_secret_cleanup_claim_lifecycle.sql")
|
||||
|
||||
var status string
|
||||
var claimToken, leaseExpiresAt *string
|
||||
if err := db.pool.QueryRow(ctx, `
|
||||
SELECT status,claim_token::text,lease_expires_at::text
|
||||
FROM gateway_identity_secret_cleanup_queue WHERE secret_ref=$1`, reference).
|
||||
Scan(&status, &claimToken, &leaseExpiresAt); err != nil {
|
||||
t.Fatalf("read migrated cleanup row: %v", err)
|
||||
}
|
||||
if status != "pending" || claimToken != nil || leaseExpiresAt != nil {
|
||||
t.Fatalf("legacy cleanup row was not backfilled to an unclaimed pending lifecycle")
|
||||
}
|
||||
|
||||
claims, err := db.ClaimIdentitySecretCleanups(ctx, 1, time.Minute)
|
||||
if err != nil {
|
||||
t.Fatalf("claim migrated cleanup row: %v", err)
|
||||
t.Fatalf("claim identity Secret cleanup: %v", err)
|
||||
}
|
||||
if len(claims) != 1 || claims[0].Reference != reference || claims[0].ClaimToken == "" {
|
||||
t.Fatalf("claim migrated cleanup row returned unexpected claim metadata")
|
||||
t.Fatalf("claim identity Secret cleanup returned unexpected metadata: %#v", claims)
|
||||
}
|
||||
completed, err := db.CompleteIdentitySecretCleanup(ctx, reference, claims[0].ClaimToken)
|
||||
if err != nil || !completed {
|
||||
t.Fatalf("complete migrated cleanup claim: completed=%t err=%v", completed, err)
|
||||
t.Fatalf("complete identity Secret cleanup: completed=%t err=%v", completed, err)
|
||||
}
|
||||
|
||||
invalidReference := "identity-cleanup-invalid-" + uuid.NewString()
|
||||
@@ -105,15 +77,11 @@ INSERT INTO gateway_identity_secret_cleanup_queue(
|
||||
SELECT pg_get_indexdef(indexrelid)
|
||||
FROM pg_index
|
||||
WHERE indexrelid='idx_gateway_identity_secret_cleanup_due'::regclass`).Scan(&indexDefinition); err != nil {
|
||||
t.Fatalf("read upgraded cleanup due index: %v", err)
|
||||
t.Fatalf("read cleanup due index: %v", err)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(indexDefinition), "(status, not_before, lease_expires_at, updated_at)") {
|
||||
t.Fatalf("cleanup due index does not cover the claim lifecycle")
|
||||
t.Fatalf("cleanup due index does not cover the claim lifecycle: %q", indexDefinition)
|
||||
}
|
||||
|
||||
// The production migration runner applies each version once. Reapplying here
|
||||
// proves that the upgrade also accepts the already-current 0070 schema shape.
|
||||
applyIdentitySecretCleanupMigration(t, ctx, db.pool, "0072_identity_secret_cleanup_claim_lifecycle.sql")
|
||||
}
|
||||
|
||||
func newIdentitySecretCleanupMigrationPostgresStore(t *testing.T) *Store {
|
||||
@@ -125,12 +93,12 @@ func newIdentitySecretCleanupMigrationPostgresStore(t *testing.T) *Store {
|
||||
ctx := context.Background()
|
||||
admin, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatalf("connect identity Secret cleanup migration test database: %v", err)
|
||||
t.Fatalf("connect identity migration test database: %v", err)
|
||||
}
|
||||
var databaseName string
|
||||
if err := admin.QueryRow(ctx, `SELECT current_database()`).Scan(&databaseName); err != nil {
|
||||
admin.Close()
|
||||
t.Fatalf("read identity Secret cleanup migration test database name: %v", err)
|
||||
t.Fatalf("read identity migration test database name: %v", err)
|
||||
}
|
||||
if !strings.Contains(strings.ToLower(databaseName), "test") {
|
||||
admin.Close()
|
||||
@@ -141,25 +109,25 @@ func newIdentitySecretCleanupMigrationPostgresStore(t *testing.T) *Store {
|
||||
schemaIdentifier := pgx.Identifier{schemaName}.Sanitize()
|
||||
if _, err := admin.Exec(ctx, `CREATE SCHEMA `+schemaIdentifier); err != nil {
|
||||
admin.Close()
|
||||
t.Fatalf("create identity Secret cleanup migration test schema: %v", err)
|
||||
t.Fatalf("create identity migration test schema: %v", err)
|
||||
}
|
||||
config, err := pgxpool.ParseConfig(databaseURL)
|
||||
if err != nil {
|
||||
_, _ = admin.Exec(ctx, `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`)
|
||||
admin.Close()
|
||||
t.Fatalf("parse identity Secret cleanup migration test database URL: %v", err)
|
||||
t.Fatalf("parse identity migration test database URL: %v", err)
|
||||
}
|
||||
config.ConnConfig.RuntimeParams["search_path"] = schemaName
|
||||
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
_, _ = admin.Exec(ctx, `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`)
|
||||
admin.Close()
|
||||
t.Fatalf("connect identity Secret cleanup migration test schema: %v", err)
|
||||
t.Fatalf("connect identity migration test schema: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
pool.Close()
|
||||
if _, err := admin.Exec(context.Background(), `DROP SCHEMA IF EXISTS `+schemaIdentifier+` CASCADE`); err != nil {
|
||||
t.Errorf("drop identity Secret cleanup migration test schema: %v", err)
|
||||
t.Errorf("drop identity migration test schema: %v", err)
|
||||
}
|
||||
admin.Close()
|
||||
})
|
||||
|
||||
@@ -3,6 +3,8 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -116,6 +118,9 @@ func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier,
|
||||
if len(capabilities) == 0 {
|
||||
capabilities = EffectivePlatformModelCapabilities(base.Capabilities, input.CapabilityOverride)
|
||||
}
|
||||
if err := validateEnabledVolcesTextModelCapabilities(ctx, q, input, capabilities); err != nil {
|
||||
return PlatformModel{}, err
|
||||
}
|
||||
billingConfig := input.BillingConfig
|
||||
if len(billingConfig) == 0 {
|
||||
billingConfig = mergeObjects(base.BaseBillingConfig, input.BillingConfigOverride)
|
||||
@@ -262,6 +267,76 @@ RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func validateEnabledVolcesTextModelCapabilities(ctx context.Context, q platformModelQuerier, input CreatePlatformModelInput, capabilities map[string]any) error {
|
||||
// createPlatformModel enables/upserts models unconditionally, so every text
|
||||
// model that reaches this path must already satisfy the Volcengine invariant.
|
||||
if !containsTextOutputModelType(input.ModelType) {
|
||||
return nil
|
||||
}
|
||||
var provider string
|
||||
var baseURL string
|
||||
if err := q.QueryRow(ctx, `SELECT provider, COALESCE(base_url, '') FROM integration_platforms WHERE id = $1::uuid`, input.PlatformID).Scan(&provider, &baseURL); err != nil {
|
||||
return err
|
||||
}
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
baseURL = strings.ToLower(strings.TrimSpace(baseURL))
|
||||
if provider != "volces-openai" && !strings.Contains(baseURL, "volces.com") && !strings.Contains(baseURL, "byteplus.com") {
|
||||
return nil
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, modelType := range append(append(StringList{}, input.ModelType...), "text_generate") {
|
||||
modelType = strings.TrimSpace(modelType)
|
||||
if modelType == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[modelType]; ok {
|
||||
continue
|
||||
}
|
||||
seen[modelType] = struct{}{}
|
||||
capability, _ := capabilities[modelType].(map[string]any)
|
||||
if value, ok := positiveWholeNumber(capability["max_output_tokens"]); ok && value > 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w: enabled Volcengine text model %q requires a positive integer max_output_tokens capability for its text model type or text_generate fallback", ErrInvalidPlatformModelConfiguration, input.ProviderModelName)
|
||||
}
|
||||
|
||||
func containsTextOutputModelType(values StringList) bool {
|
||||
for _, value := range values {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "text_generate", "chat", "responses", "text":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func positiveWholeNumber(value any) (int64, bool) {
|
||||
var number float64
|
||||
switch typed := value.(type) {
|
||||
case int:
|
||||
number = float64(typed)
|
||||
case int32:
|
||||
number = float64(typed)
|
||||
case int64:
|
||||
number = float64(typed)
|
||||
case float64:
|
||||
number = typed
|
||||
case json.Number:
|
||||
parsed, err := typed.Float64()
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
number = parsed
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
if number <= 0 || math.Trunc(number) != number || number > math.MaxInt64 {
|
||||
return 0, false
|
||||
}
|
||||
return int64(number), true
|
||||
}
|
||||
|
||||
func (s *Store) DeletePlatformModel(ctx context.Context, id string) error {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoModelCandidate = errors.New("no enabled platform model matches request")
|
||||
ErrRateLimited = errors.New("rate limit exceeded")
|
||||
ErrNoModelCandidate = errors.New("no enabled platform model matches request")
|
||||
ErrRateLimited = errors.New("rate limit exceeded")
|
||||
ErrInvalidPlatformModelConfiguration = errors.New("invalid platform model configuration")
|
||||
)
|
||||
|
||||
type ModelCandidateUnavailableError struct {
|
||||
|
||||
Reference in New Issue
Block a user