AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
platformid "aigateway.local/core/internal/platform/id"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProviderNotFound = errors.New("provider not found")
|
||||
ErrProviderExists = errors.New("provider already exists")
|
||||
ErrMultipleDefaults = errors.New("multiple default providers")
|
||||
ErrProviderStore = errors.New("provider store unavailable")
|
||||
ErrProviderUpstream = errors.New("provider upstream unavailable")
|
||||
)
|
||||
|
||||
type Record struct {
|
||||
ID string
|
||||
TenantID *string
|
||||
Code string
|
||||
Adapter string
|
||||
BaseURL string
|
||||
EncryptedCredentials []byte
|
||||
CredentialKEKVersion int
|
||||
Capabilities []string
|
||||
Config json.RawMessage
|
||||
Enabled bool
|
||||
Revision int64
|
||||
}
|
||||
|
||||
func (r *Repository) ListModels(ctx context.Context, providerID string) ([]Model, error) {
|
||||
if r.pool == nil {
|
||||
return nil, ErrProviderStore
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id::text, provider_id::text, provider_model_id, owned_by, metadata,
|
||||
enabled, discovered_at, last_seen_at
|
||||
FROM gateway.provider_models
|
||||
WHERE provider_id = $1
|
||||
ORDER BY enabled DESC, provider_model_id`, providerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
models := make([]Model, 0)
|
||||
for rows.Next() {
|
||||
var model Model
|
||||
if err := rows.Scan(
|
||||
&model.ID, &model.ProviderID, &model.ProviderModelID, &model.OwnedBy,
|
||||
&model.Metadata, &model.Enabled, &model.DiscoveredAt, &model.LastSeenAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
models = append(models, model)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (r *Repository) SyncModels(ctx context.Context, providerID, actorID string, models []DiscoveredModel) (ModelSyncResult, error) {
|
||||
if r.pool == nil {
|
||||
return ModelSyncResult{}, ErrProviderStore
|
||||
}
|
||||
eventID, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return ModelSyncResult{}, err
|
||||
}
|
||||
transaction, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer func() { _ = transaction.Rollback(ctx) }()
|
||||
var exists bool
|
||||
if err := transaction.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM gateway.providers WHERE id = $1 AND tenant_id IS NULL
|
||||
)`, providerID).Scan(&exists); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
if !exists {
|
||||
return ModelSyncResult{}, ErrProviderNotFound
|
||||
}
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
UPDATE gateway.provider_models
|
||||
SET enabled = false, updated_at = clock_timestamp()
|
||||
WHERE provider_id = $1 AND enabled = true`, providerID); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
for _, model := range models {
|
||||
id, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return ModelSyncResult{}, err
|
||||
}
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
INSERT INTO gateway.provider_models
|
||||
(id, provider_id, provider_model_id, owned_by, metadata, enabled)
|
||||
VALUES ($1, $2, $3, $4, $5, true)
|
||||
ON CONFLICT (provider_id, provider_model_id) DO UPDATE
|
||||
SET owned_by = EXCLUDED.owned_by,
|
||||
metadata = EXCLUDED.metadata,
|
||||
enabled = true,
|
||||
last_seen_at = clock_timestamp(),
|
||||
updated_at = clock_timestamp()`,
|
||||
id, providerID, model.ProviderModelID, model.OwnedBy, model.Metadata,
|
||||
); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
}
|
||||
result := ModelSyncResult{Discovered: len(models), SyncedAt: time.Now().UTC()}
|
||||
if err := transaction.QueryRow(ctx, `
|
||||
SELECT count(*) FILTER (WHERE enabled), count(*) FILTER (WHERE NOT enabled)
|
||||
FROM gateway.provider_models
|
||||
WHERE provider_id = $1`, providerID).Scan(&result.Active, &result.Disabled); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"provider_id": providerID, "discovered": result.Discovered,
|
||||
"active": result.Active, "disabled": result.Disabled, "actor_id": actorID,
|
||||
})
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
INSERT INTO gateway.outbox_events
|
||||
(event_id, event_type, event_version, aggregate_type, aggregate_id, payload)
|
||||
VALUES ($1, 'provider.models_synced', 1, 'provider', $2, $3)`, eventID, providerID, payload); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
if err := transaction.Commit(ctx); err != nil {
|
||||
return ModelSyncResult{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Repository) RotateCredentials(ctx context.Context, actorID string, rotations []CredentialRotation) error {
|
||||
if r.pool == nil {
|
||||
return ErrProviderStore
|
||||
}
|
||||
if len(rotations) == 0 {
|
||||
return nil
|
||||
}
|
||||
transaction, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer func() { _ = transaction.Rollback(ctx) }()
|
||||
for _, rotation := range rotations {
|
||||
result, err := transaction.Exec(ctx, `
|
||||
UPDATE gateway.providers
|
||||
SET encrypted_credentials = $2,
|
||||
credential_kek_version = $3,
|
||||
revision = revision + 1,
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = $1
|
||||
AND tenant_id IS NULL
|
||||
AND credential_kek_version = $4`,
|
||||
rotation.ProviderID, rotation.Ciphertext, rotation.ToVersion, rotation.FromVersion,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
if result.RowsAffected() != 1 {
|
||||
return fmt.Errorf("%w: provider %s changed during credential rotation", ErrProviderStore, rotation.ProviderID)
|
||||
}
|
||||
eventID, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"provider_id": rotation.ProviderID, "from_version": rotation.FromVersion,
|
||||
"to_version": rotation.ToVersion, "actor_id": actorID,
|
||||
})
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
INSERT INTO gateway.outbox_events
|
||||
(event_id, event_type, event_version, aggregate_type, aggregate_id, payload)
|
||||
VALUES ($1, 'provider.credentials_rotated', 1, 'provider', $2, $3)`,
|
||||
eventID, rotation.ProviderID, payload,
|
||||
); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
}
|
||||
if err := transaction.Commit(ctx); err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewRepository(pool *pgxpool.Pool) *Repository {
|
||||
return &Repository{pool: pool}
|
||||
}
|
||||
|
||||
func (r *Repository) List(ctx context.Context) ([]Record, error) {
|
||||
if r.pool == nil {
|
||||
return nil, ErrProviderStore
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id::text, tenant_id::text, code, adapter, base_url,
|
||||
encrypted_credentials, credential_kek_version, capabilities,
|
||||
config, enabled, revision
|
||||
FROM gateway.providers
|
||||
WHERE tenant_id IS NULL
|
||||
ORDER BY code`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var records []Record
|
||||
for rows.Next() {
|
||||
var record Record
|
||||
if err := rows.Scan(
|
||||
&record.ID, &record.TenantID, &record.Code, &record.Adapter, &record.BaseURL,
|
||||
&record.EncryptedCredentials, &record.CredentialKEKVersion, &record.Capabilities,
|
||||
&record.Config, &record.Enabled, &record.Revision,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
records = append(records, record)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Get(ctx context.Context, id string) (Record, error) {
|
||||
if r.pool == nil {
|
||||
return Record{}, ErrProviderStore
|
||||
}
|
||||
var record Record
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT id::text, tenant_id::text, code, adapter, base_url,
|
||||
encrypted_credentials, credential_kek_version, capabilities,
|
||||
config, enabled, revision
|
||||
FROM gateway.providers
|
||||
WHERE id = $1 AND tenant_id IS NULL`, id).Scan(
|
||||
&record.ID, &record.TenantID, &record.Code, &record.Adapter, &record.BaseURL,
|
||||
&record.EncryptedCredentials, &record.CredentialKEKVersion, &record.Capabilities,
|
||||
&record.Config, &record.Enabled, &record.Revision,
|
||||
)
|
||||
return record, mapProviderError(err)
|
||||
}
|
||||
|
||||
func (r *Repository) Create(ctx context.Context, record Record, actorID string) (Record, error) {
|
||||
if r.pool == nil {
|
||||
return Record{}, ErrProviderStore
|
||||
}
|
||||
id, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
eventID, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
record.ID = id
|
||||
transaction, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer func() { _ = transaction.Rollback(ctx) }()
|
||||
err = transaction.QueryRow(ctx, `
|
||||
INSERT INTO gateway.providers
|
||||
(id, code, adapter, base_url, encrypted_credentials,
|
||||
credential_kek_version, capabilities, config, enabled, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING revision`, record.ID, record.Code, record.Adapter, record.BaseURL,
|
||||
record.EncryptedCredentials, record.CredentialKEKVersion, record.Capabilities,
|
||||
record.Config, record.Enabled, actorID).Scan(&record.Revision)
|
||||
if err != nil {
|
||||
return Record{}, mapProviderError(err)
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{"provider_id": record.ID, "code": record.Code, "revision": record.Revision})
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
INSERT INTO gateway.outbox_events
|
||||
(event_id, event_type, event_version, aggregate_type, aggregate_id, payload)
|
||||
VALUES ($1, 'provider.created', 1, 'provider', $2, $3)`, eventID, record.ID, payload); err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
if err := transaction.Commit(ctx); err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Update(ctx context.Context, record Record, actorID string, replaceCredentials bool) (Record, error) {
|
||||
if r.pool == nil {
|
||||
return Record{}, ErrProviderStore
|
||||
}
|
||||
eventID, err := platformid.NewUUID()
|
||||
if err != nil {
|
||||
return Record{}, err
|
||||
}
|
||||
transaction, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
defer func() { _ = transaction.Rollback(ctx) }()
|
||||
err = transaction.QueryRow(ctx, `
|
||||
UPDATE gateway.providers
|
||||
SET code = $2,
|
||||
adapter = $3,
|
||||
base_url = $4,
|
||||
encrypted_credentials = CASE WHEN $10 THEN $5 ELSE encrypted_credentials END,
|
||||
credential_kek_version = CASE WHEN $10 THEN $6 ELSE credential_kek_version END,
|
||||
capabilities = $7,
|
||||
config = $8,
|
||||
enabled = $9,
|
||||
revision = revision + 1,
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = $1 AND tenant_id IS NULL
|
||||
RETURNING revision`, record.ID, record.Code, record.Adapter, record.BaseURL,
|
||||
record.EncryptedCredentials, record.CredentialKEKVersion, record.Capabilities,
|
||||
record.Config, record.Enabled, replaceCredentials).Scan(&record.Revision)
|
||||
if err != nil {
|
||||
return Record{}, mapProviderError(err)
|
||||
}
|
||||
payload, _ := json.Marshal(map[string]any{
|
||||
"provider_id": record.ID, "code": record.Code, "revision": record.Revision, "actor_id": actorID,
|
||||
})
|
||||
if _, err := transaction.Exec(ctx, `
|
||||
INSERT INTO gateway.outbox_events
|
||||
(event_id, event_type, event_version, aggregate_type, aggregate_id, payload)
|
||||
VALUES ($1, 'provider.updated', 1, 'provider', $2, $3)`, eventID, record.ID, payload); err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
if err := transaction.Commit(ctx); err != nil {
|
||||
return Record{}, fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func mapProviderError(err error) error {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrProviderNotFound
|
||||
}
|
||||
var pgError *pgconn.PgError
|
||||
if errors.As(err, &pgError) && pgError.Code == "23505" {
|
||||
if pgError.ConstraintName == "providers_single_global_default_idx" {
|
||||
return ErrMultipleDefaults
|
||||
}
|
||||
return ErrProviderExists
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %v", ErrProviderStore, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user