From 3ecadf4aad86c5befe5a72b0e033ab74ce8bcfcd Mon Sep 17 00:00:00 2001 From: song Date: Mon, 2 Feb 2026 22:20:08 +0800 Subject: [PATCH] chore: apply stashed changes --- backend/ent/group.go | 15 +- backend/ent/group/group.go | 5 + backend/ent/group_create.go | 57 ++++ backend/ent/group_update.go | 41 +++ backend/ent/migrate/schema.go | 1 + backend/ent/mutation.go | 72 ++++- backend/ent/runtime/runtime.go | 4 + backend/ent/schema/group.go | 6 + backend/go.mod | 18 +- backend/go.sum | 52 +++- backend/internal/domain/constants.go | 1 + .../internal/handler/admin/account_handler.go | 4 +- .../internal/handler/admin/group_handler.go | 6 + backend/internal/handler/dto/types.go | 2 + backend/internal/repository/api_key_repo.go | 2 + backend/internal/repository/group_repo.go | 7 +- backend/internal/service/admin_service.go | 10 + .../service/antigravity_gateway_service.go | 293 ++++++++++++++++++ .../service/antigravity_quota_scope.go | 16 + .../internal/service/api_key_auth_cache.go | 3 + .../service/api_key_auth_cache_impl.go | 2 + backend/internal/service/domain_constants.go | 1 + backend/internal/service/gateway_service.go | 38 +++ backend/internal/service/group.go | 4 + backend/internal/service/user.go | 5 + backend/internal/service/user_service.go | 2 +- .../046_add_group_supported_model_scopes.sql | 6 + docs/rename_local_migrations_20260202.sql | 27 ++ .../components/account/CreateAccountModal.vue | 136 +++++++- frontend/src/i18n/locales/en.ts | 21 +- frontend/src/i18n/locales/zh.ts | 19 ++ frontend/src/types/index.ts | 9 +- frontend/src/views/admin/GroupsView.vue | 140 +++++++++ 33 files changed, 997 insertions(+), 28 deletions(-) create mode 100644 backend/migrations/046_add_group_supported_model_scopes.sql create mode 100644 docs/rename_local_migrations_20260202.sql diff --git a/backend/ent/group.go b/backend/ent/group.go index d2b6af9f..1eb05e0e 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -64,6 +64,8 @@ type Group struct { ModelRoutingEnabled bool `json:"model_routing_enabled,omitempty"` // 是否注入 MCP XML 调用协议提示词(仅 antigravity 平台) McpXMLInject bool `json:"mcp_xml_inject,omitempty"` + // 支持的模型系列:claude, gemini_text, gemini_image + SupportedModelScopes []string `json:"supported_model_scopes,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the GroupQuery when eager-loading is set. Edges GroupEdges `json:"edges"` @@ -170,7 +172,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case group.FieldModelRouting: + case group.FieldModelRouting, group.FieldSupportedModelScopes: values[i] = new([]byte) case group.FieldIsExclusive, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject: values[i] = new(sql.NullBool) @@ -353,6 +355,14 @@ func (_m *Group) assignValues(columns []string, values []any) error { } else if value.Valid { _m.McpXMLInject = value.Bool } + case group.FieldSupportedModelScopes: + if value, ok := values[i].(*[]byte); !ok { + return fmt.Errorf("unexpected type %T for field supported_model_scopes", values[i]) + } else if value != nil && len(*value) > 0 { + if err := json.Unmarshal(*value, &_m.SupportedModelScopes); err != nil { + return fmt.Errorf("unmarshal field supported_model_scopes: %w", err) + } + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -517,6 +527,9 @@ func (_m *Group) String() string { builder.WriteString(", ") builder.WriteString("mcp_xml_inject=") builder.WriteString(fmt.Sprintf("%v", _m.McpXMLInject)) + builder.WriteString(", ") + builder.WriteString("supported_model_scopes=") + builder.WriteString(fmt.Sprintf("%v", _m.SupportedModelScopes)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index aa9ff2ab..278b2daf 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -61,6 +61,8 @@ const ( FieldModelRoutingEnabled = "model_routing_enabled" // FieldMcpXMLInject holds the string denoting the mcp_xml_inject field in the database. FieldMcpXMLInject = "mcp_xml_inject" + // FieldSupportedModelScopes holds the string denoting the supported_model_scopes field in the database. + FieldSupportedModelScopes = "supported_model_scopes" // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. EdgeAPIKeys = "api_keys" // EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations. @@ -159,6 +161,7 @@ var Columns = []string{ FieldModelRouting, FieldModelRoutingEnabled, FieldMcpXMLInject, + FieldSupportedModelScopes, } var ( @@ -220,6 +223,8 @@ var ( DefaultModelRoutingEnabled bool // DefaultMcpXMLInject holds the default value on creation for the "mcp_xml_inject" field. DefaultMcpXMLInject bool + // DefaultSupportedModelScopes holds the default value on creation for the "supported_model_scopes" field. + DefaultSupportedModelScopes []string ) // OrderOption defines the ordering options for the Group queries. diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index b1ccc8e3..9d845b61 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -334,6 +334,12 @@ func (_c *GroupCreate) SetNillableMcpXMLInject(v *bool) *GroupCreate { return _c } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (_c *GroupCreate) SetSupportedModelScopes(v []string) *GroupCreate { + _c.mutation.SetSupportedModelScopes(v) + return _c +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate { _c.mutation.AddAPIKeyIDs(ids...) @@ -511,6 +517,10 @@ func (_c *GroupCreate) defaults() error { v := group.DefaultMcpXMLInject _c.mutation.SetMcpXMLInject(v) } + if _, ok := _c.mutation.SupportedModelScopes(); !ok { + v := group.DefaultSupportedModelScopes + _c.mutation.SetSupportedModelScopes(v) + } return nil } @@ -572,6 +582,9 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.McpXMLInject(); !ok { return &ValidationError{Name: "mcp_xml_inject", err: errors.New(`ent: missing required field "Group.mcp_xml_inject"`)} } + if _, ok := _c.mutation.SupportedModelScopes(); !ok { + return &ValidationError{Name: "supported_model_scopes", err: errors.New(`ent: missing required field "Group.supported_model_scopes"`)} + } return nil } @@ -691,6 +704,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldMcpXMLInject, field.TypeBool, value) _node.McpXMLInject = value } + if value, ok := _c.mutation.SupportedModelScopes(); ok { + _spec.SetField(group.FieldSupportedModelScopes, field.TypeJSON, value) + _node.SupportedModelScopes = value + } if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1237,6 +1254,18 @@ func (u *GroupUpsert) UpdateMcpXMLInject() *GroupUpsert { return u } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (u *GroupUpsert) SetSupportedModelScopes(v []string) *GroupUpsert { + u.Set(group.FieldSupportedModelScopes, v) + return u +} + +// UpdateSupportedModelScopes sets the "supported_model_scopes" field to the value that was provided on create. +func (u *GroupUpsert) UpdateSupportedModelScopes() *GroupUpsert { + u.SetExcluded(group.FieldSupportedModelScopes) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1737,6 +1766,20 @@ func (u *GroupUpsertOne) UpdateMcpXMLInject() *GroupUpsertOne { }) } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (u *GroupUpsertOne) SetSupportedModelScopes(v []string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetSupportedModelScopes(v) + }) +} + +// UpdateSupportedModelScopes sets the "supported_model_scopes" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateSupportedModelScopes() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateSupportedModelScopes() + }) +} + // Exec executes the query. func (u *GroupUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -2403,6 +2446,20 @@ func (u *GroupUpsertBulk) UpdateMcpXMLInject() *GroupUpsertBulk { }) } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (u *GroupUpsertBulk) SetSupportedModelScopes(v []string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetSupportedModelScopes(v) + }) +} + +// UpdateSupportedModelScopes sets the "supported_model_scopes" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateSupportedModelScopes() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateSupportedModelScopes() + }) +} + // Exec executes the query. func (u *GroupUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index 332ae52a..9e7246ea 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -10,6 +10,7 @@ import ( "entgo.io/ent/dialect/sql" "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/dialect/sql/sqljson" "entgo.io/ent/schema/field" "github.com/Wei-Shaw/sub2api/ent/account" "github.com/Wei-Shaw/sub2api/ent/apikey" @@ -462,6 +463,18 @@ func (_u *GroupUpdate) SetNillableMcpXMLInject(v *bool) *GroupUpdate { return _u } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (_u *GroupUpdate) SetSupportedModelScopes(v []string) *GroupUpdate { + _u.mutation.SetSupportedModelScopes(v) + return _u +} + +// AppendSupportedModelScopes appends value to the "supported_model_scopes" field. +func (_u *GroupUpdate) AppendSupportedModelScopes(v []string) *GroupUpdate { + _u.mutation.AppendSupportedModelScopes(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate { _u.mutation.AddAPIKeyIDs(ids...) @@ -891,6 +904,14 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.McpXMLInject(); ok { _spec.SetField(group.FieldMcpXMLInject, field.TypeBool, value) } + if value, ok := _u.mutation.SupportedModelScopes(); ok { + _spec.SetField(group.FieldSupportedModelScopes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSupportedModelScopes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, group.FieldSupportedModelScopes, value) + }) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1633,6 +1654,18 @@ func (_u *GroupUpdateOne) SetNillableMcpXMLInject(v *bool) *GroupUpdateOne { return _u } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (_u *GroupUpdateOne) SetSupportedModelScopes(v []string) *GroupUpdateOne { + _u.mutation.SetSupportedModelScopes(v) + return _u +} + +// AppendSupportedModelScopes appends value to the "supported_model_scopes" field. +func (_u *GroupUpdateOne) AppendSupportedModelScopes(v []string) *GroupUpdateOne { + _u.mutation.AppendSupportedModelScopes(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne { _u.mutation.AddAPIKeyIDs(ids...) @@ -2092,6 +2125,14 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if value, ok := _u.mutation.McpXMLInject(); ok { _spec.SetField(group.FieldMcpXMLInject, field.TypeBool, value) } + if value, ok := _u.mutation.SupportedModelScopes(); ok { + _spec.SetField(group.FieldSupportedModelScopes, field.TypeJSON, value) + } + if value, ok := _u.mutation.AppendedSupportedModelScopes(); ok { + _spec.AddModifier(func(u *sql.UpdateBuilder) { + sqljson.Append(u, group.FieldSupportedModelScopes, value) + }) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index 434916f8..d0238545 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -322,6 +322,7 @@ var ( {Name: "model_routing", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "model_routing_enabled", Type: field.TypeBool, Default: false}, {Name: "mcp_xml_inject", Type: field.TypeBool, Default: true}, + {Name: "supported_model_scopes", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index cb654b7b..c7812024 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -5542,6 +5542,8 @@ type GroupMutation struct { model_routing *map[string][]int64 model_routing_enabled *bool mcp_xml_inject *bool + supported_model_scopes *[]string + appendsupported_model_scopes []string clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -6843,6 +6845,57 @@ func (m *GroupMutation) ResetMcpXMLInject() { m.mcp_xml_inject = nil } +// SetSupportedModelScopes sets the "supported_model_scopes" field. +func (m *GroupMutation) SetSupportedModelScopes(s []string) { + m.supported_model_scopes = &s + m.appendsupported_model_scopes = nil +} + +// SupportedModelScopes returns the value of the "supported_model_scopes" field in the mutation. +func (m *GroupMutation) SupportedModelScopes() (r []string, exists bool) { + v := m.supported_model_scopes + if v == nil { + return + } + return *v, true +} + +// OldSupportedModelScopes returns the old "supported_model_scopes" field's value of the Group entity. +// If the Group object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *GroupMutation) OldSupportedModelScopes(ctx context.Context) (v []string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldSupportedModelScopes is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldSupportedModelScopes requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldSupportedModelScopes: %w", err) + } + return oldValue.SupportedModelScopes, nil +} + +// AppendSupportedModelScopes adds s to the "supported_model_scopes" field. +func (m *GroupMutation) AppendSupportedModelScopes(s []string) { + m.appendsupported_model_scopes = append(m.appendsupported_model_scopes, s...) +} + +// AppendedSupportedModelScopes returns the list of values that were appended to the "supported_model_scopes" field in this mutation. +func (m *GroupMutation) AppendedSupportedModelScopes() ([]string, bool) { + if len(m.appendsupported_model_scopes) == 0 { + return nil, false + } + return m.appendsupported_model_scopes, true +} + +// ResetSupportedModelScopes resets all changes to the "supported_model_scopes" field. +func (m *GroupMutation) ResetSupportedModelScopes() { + m.supported_model_scopes = nil + m.appendsupported_model_scopes = nil +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -7201,7 +7254,7 @@ func (m *GroupMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *GroupMutation) Fields() []string { - fields := make([]string, 0, 23) + fields := make([]string, 0, 24) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -7271,6 +7324,9 @@ func (m *GroupMutation) Fields() []string { if m.mcp_xml_inject != nil { fields = append(fields, group.FieldMcpXMLInject) } + if m.supported_model_scopes != nil { + fields = append(fields, group.FieldSupportedModelScopes) + } return fields } @@ -7325,6 +7381,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.ModelRoutingEnabled() case group.FieldMcpXMLInject: return m.McpXMLInject() + case group.FieldSupportedModelScopes: + return m.SupportedModelScopes() } return nil, false } @@ -7380,6 +7438,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldModelRoutingEnabled(ctx) case group.FieldMcpXMLInject: return m.OldMcpXMLInject(ctx) + case group.FieldSupportedModelScopes: + return m.OldSupportedModelScopes(ctx) } return nil, fmt.Errorf("unknown Group field %s", name) } @@ -7550,6 +7610,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetMcpXMLInject(v) return nil + case group.FieldSupportedModelScopes: + v, ok := value.([]string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetSupportedModelScopes(v) + return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -7860,6 +7927,9 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldMcpXMLInject: m.ResetMcpXMLInject() return nil + case group.FieldSupportedModelScopes: + m.ResetSupportedModelScopes() + return nil } return fmt.Errorf("unknown Group field %s", name) } diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 790c1489..8032dc58 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -341,6 +341,10 @@ func init() { groupDescMcpXMLInject := groupFields[19].Descriptor() // group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field. group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool) + // groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field. + groupDescSupportedModelScopes := groupFields[20].Descriptor() + // group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field. + group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string) promocodeFields := schema.PromoCode{}.Fields() _ = promocodeFields // promocodeDescCode is the schema descriptor for code field. diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 020f9f57..8a3c1a90 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -115,6 +115,12 @@ func (Group) Fields() []ent.Field { field.Bool("mcp_xml_inject"). Default(true). Comment("是否注入 MCP XML 调用协议提示词(仅 antigravity 平台)"), + + // 支持的模型系列 (added by migration 046) + field.JSON("supported_model_scopes", []string{}). + Default([]string{"claude", "gemini_text", "gemini_image"}). + SchemaType(map[string]string{dialect.Postgres: "jsonb"}). + Comment("支持的模型系列:claude, gemini_text, gemini_image"), } } diff --git a/backend/go.mod b/backend/go.mod index 329eddfc..9a36a0f1 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,8 @@ go 1.25.6 require ( entgo.io/ent v0.14.5 + github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/dgraph-io/ristretto v0.2.0 github.com/gin-gonic/gin v1.9.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 @@ -11,7 +13,10 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/imroc/req/v3 v3.57.0 github.com/lib/pq v1.10.9 + github.com/pquerna/otp v1.5.0 github.com/redis/go-redis/v9 v9.17.2 + github.com/refraction-networking/utls v1.8.1 + github.com/robfig/cron/v3 v3.0.1 github.com/shirou/gopsutil/v4 v4.25.6 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.11.1 @@ -25,6 +30,7 @@ require ( golang.org/x/sync v0.19.0 golang.org/x/term v0.38.0 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.44.3 ) require ( @@ -36,6 +42,7 @@ require ( github.com/andybalholm/brotli v1.2.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.9.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -46,7 +53,6 @@ require ( github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgraph-io/ristretto v0.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect @@ -97,6 +103,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -106,9 +113,8 @@ require ( github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.57.1 // indirect - github.com/refraction-networking/utls v1.8.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -139,13 +145,15 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/tools v0.39.0 // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/grpc v1.75.1 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 415e73a7..371623ad 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -20,6 +20,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -53,6 +55,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -111,6 +115,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -121,6 +127,9 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo= @@ -141,6 +150,7 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -199,6 +209,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -214,6 +226,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= +github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -224,6 +238,8 @@ github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4Vi github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= @@ -336,8 +352,8 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= @@ -363,8 +379,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= -golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -387,4 +403,32 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY= +modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/backend/internal/domain/constants.go b/backend/internal/domain/constants.go index 4ecea9d8..586e6309 100644 --- a/backend/internal/domain/constants.go +++ b/backend/internal/domain/constants.go @@ -29,6 +29,7 @@ const ( AccountTypeOAuth = "oauth" // OAuth类型账号(full scope: profile + inference) AccountTypeSetupToken = "setup-token" // Setup Token类型账号(inference only scope) AccountTypeAPIKey = "apikey" // API Key类型账号 + AccountTypeUpstream = "upstream" // 上游透传类型账号(通过 Base URL + API Key 连接上游) ) // Redeem type constants diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index bbf5d026..6d42f726 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -84,7 +84,7 @@ type CreateAccountRequest struct { Name string `json:"name" binding:"required"` Notes *string `json:"notes"` Platform string `json:"platform" binding:"required"` - Type string `json:"type" binding:"required,oneof=oauth setup-token apikey"` + Type string `json:"type" binding:"required,oneof=oauth setup-token apikey upstream"` Credentials map[string]any `json:"credentials" binding:"required"` Extra map[string]any `json:"extra"` ProxyID *int64 `json:"proxy_id"` @@ -102,7 +102,7 @@ type CreateAccountRequest struct { type UpdateAccountRequest struct { Name string `json:"name"` Notes *string `json:"notes"` - Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey"` + Type string `json:"type" binding:"omitempty,oneof=oauth setup-token apikey upstream"` Credentials map[string]any `json:"credentials"` Extra map[string]any `json:"extra"` ProxyID *int64 `json:"proxy_id"` diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 32391418..9192fe45 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -45,6 +45,8 @@ type CreateGroupRequest struct { ModelRouting map[string][]int64 `json:"model_routing"` ModelRoutingEnabled bool `json:"model_routing_enabled"` MCPXMLInject *bool `json:"mcp_xml_inject"` + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes []string `json:"supported_model_scopes"` } // UpdateGroupRequest represents update group request @@ -70,6 +72,8 @@ type UpdateGroupRequest struct { ModelRouting map[string][]int64 `json:"model_routing"` ModelRoutingEnabled *bool `json:"model_routing_enabled"` MCPXMLInject *bool `json:"mcp_xml_inject"` + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes *[]string `json:"supported_model_scopes"` } // List handles listing all groups with pagination @@ -177,6 +181,7 @@ func (h *GroupHandler) Create(c *gin.Context) { ModelRouting: req.ModelRouting, ModelRoutingEnabled: req.ModelRoutingEnabled, MCPXMLInject: req.MCPXMLInject, + SupportedModelScopes: req.SupportedModelScopes, }) if err != nil { response.ErrorFrom(c, err) @@ -221,6 +226,7 @@ func (h *GroupHandler) Update(c *gin.Context) { ModelRouting: req.ModelRouting, ModelRoutingEnabled: req.ModelRoutingEnabled, MCPXMLInject: req.MCPXMLInject, + SupportedModelScopes: req.SupportedModelScopes, }) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 7b33e473..ab0b86fe 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -88,6 +88,8 @@ type AdminGroup struct { // MCP XML 协议注入(仅 antigravity 平台使用) MCPXMLInject bool `json:"mcp_xml_inject"` + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes []string `json:"supported_model_scopes"` AccountGroups []AccountGroup `json:"account_groups,omitempty"` AccountCount int64 `json:"account_count,omitempty"` } diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index e3580a67..59f13985 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -140,6 +140,7 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se group.FieldModelRoutingEnabled, group.FieldModelRouting, group.FieldMcpXMLInject, + group.FieldSupportedModelScopes, ) }). Only(ctx) @@ -433,6 +434,7 @@ func groupEntityToService(g *dbent.Group) *service.Group { ModelRouting: g.ModelRouting, ModelRoutingEnabled: g.ModelRoutingEnabled, MCPXMLInject: g.McpXMLInject, + SupportedModelScopes: g.SupportedModelScopes, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, } diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 116e45a3..53624635 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -59,6 +59,9 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er builder = builder.SetModelRouting(groupIn.ModelRouting) } + // 设置支持的模型系列(始终设置,空数组表示不限制) + builder = builder.SetSupportedModelScopes(groupIn.SupportedModelScopes) + created, err := builder.Save(ctx) if err == nil { groupIn.ID = created.ID @@ -89,7 +92,6 @@ func (r *groupRepository) GetByIDLite(ctx context.Context, id int64) (*service.G if err != nil { return nil, translatePersistenceError(err, service.ErrGroupNotFound, nil) } - return groupEntityToService(m), nil } @@ -133,6 +135,9 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er builder = builder.ClearModelRouting() } + // 处理 SupportedModelScopes(始终设置,空数组表示不限制) + builder = builder.SetSupportedModelScopes(groupIn.SupportedModelScopes) + updated, err := builder.Save(ctx) if err != nil { return translatePersistenceError(err, service.ErrGroupNotFound, service.ErrGroupExists) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index d541c73a..1449070e 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -113,6 +113,8 @@ type CreateGroupInput struct { ModelRouting map[string][]int64 ModelRoutingEnabled bool // 是否启用模型路由 MCPXMLInject *bool + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes []string } type UpdateGroupInput struct { @@ -138,6 +140,8 @@ type UpdateGroupInput struct { ModelRouting map[string][]int64 ModelRoutingEnabled *bool // 是否启用模型路由 MCPXMLInject *bool + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes *[]string } type CreateAccountInput struct { @@ -613,6 +617,7 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn FallbackGroupIDOnInvalidRequest: fallbackOnInvalidRequest, ModelRouting: input.ModelRouting, MCPXMLInject: mcpXMLInject, + SupportedModelScopes: input.SupportedModelScopes, } if err := s.groupRepo.Create(ctx, group); err != nil { return nil, err @@ -797,6 +802,11 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd group.MCPXMLInject = *input.MCPXMLInject } + // 支持的模型系列(仅 antigravity 平台使用) + if input.SupportedModelScopes != nil { + group.SupportedModelScopes = *input.SupportedModelScopes + } + if err := s.groupRepo.Update(ctx, group); err != nil { return nil, err } diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index dd52a559..2af9efdb 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -412,6 +412,11 @@ type TestConnectionResult struct { // TestConnection 测试 Antigravity 账号连接(非流式,无重试、无计费) // 支持 Claude 和 Gemini 两种协议,根据 modelID 前缀自动选择 func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account *Account, modelID string) (*TestConnectionResult, error) { + // 上游透传账号使用专用测试方法 + if account.Type == AccountTypeUpstream { + return s.testUpstreamConnection(ctx, account, modelID) + } + // 获取 token if s.tokenProvider == nil { return nil, errors.New("antigravity token provider not configured") @@ -506,6 +511,87 @@ func (s *AntigravityGatewayService) TestConnection(ctx context.Context, account return nil, lastErr } +// testUpstreamConnection 测试上游透传账号连接 +func (s *AntigravityGatewayService) testUpstreamConnection(ctx context.Context, account *Account, modelID string) (*TestConnectionResult, error) { + baseURL := strings.TrimSpace(account.GetCredential("base_url")) + apiKey := strings.TrimSpace(account.GetCredential("api_key")) + if baseURL == "" || apiKey == "" { + return nil, errors.New("upstream account missing base_url or api_key") + } + baseURL = strings.TrimSuffix(baseURL, "/") + + // 使用 Claude 模型进行测试 + if modelID == "" { + modelID = "claude-sonnet-4-20250514" + } + + // 构建最小测试请求 + testReq := map[string]any{ + "model": modelID, + "max_tokens": 1, + "messages": []map[string]any{ + {"role": "user", "content": "."}, + }, + } + requestBody, err := json.Marshal(testReq) + if err != nil { + return nil, fmt.Errorf("构建请求失败: %w", err) + } + + // 构建 HTTP 请求 + upstreamURL := baseURL + "/v1/messages" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(requestBody)) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("x-api-key", apiKey) + req.Header.Set("anthropic-version", "2023-06-01") + + // 代理 URL + proxyURL := "" + if account.ProxyID != nil && account.Proxy != nil { + proxyURL = account.Proxy.URL() + } + + log.Printf("[antigravity-Test-Upstream] account=%s url=%s", account.Name, upstreamURL) + + // 发送请求 + resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency) + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %w", err) + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("API 返回 %d: %s", resp.StatusCode, string(respBody)) + } + + // 提取响应文本 + var respData map[string]any + text := "" + if json.Unmarshal(respBody, &respData) == nil { + if content, ok := respData["content"].([]any); ok && len(content) > 0 { + if block, ok := content[0].(map[string]any); ok { + if t, ok := block["text"].(string); ok { + text = t + } + } + } + } + + return &TestConnectionResult{ + Text: text, + MappedModel: modelID, + }, nil +} + // buildGeminiTestRequest 构建 Gemini 格式测试请求 // 使用最小 token 消耗:输入 "." + maxOutputTokens: 1 func (s *AntigravityGatewayService) buildGeminiTestRequest(projectID, model string) ([]byte, error) { @@ -728,6 +814,11 @@ func isModelNotFoundError(statusCode int, body []byte) bool { // Forward 转发 Claude 协议请求(Claude → Gemini 转换) func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) { + // 上游透传账号直接转发,不走 OAuth token 刷新 + if account.Type == AccountTypeUpstream { + return s.ForwardUpstream(ctx, c, account, body) + } + startTime := time.Now() sessionID := getSessionID(c) prefix := logPrefix(sessionID, account.Name) @@ -1349,6 +1440,208 @@ func stripSignatureSensitiveBlocksFromClaudeRequest(req *antigravity.ClaudeReque return changed, nil } +// ForwardUpstream 透传请求到上游 Antigravity 服务 +// 用于 upstream 类型账号,直接使用 base_url + api_key 转发,不走 OAuth token +func (s *AntigravityGatewayService) ForwardUpstream(ctx context.Context, c *gin.Context, account *Account, body []byte) (*ForwardResult, error) { + startTime := time.Now() + sessionID := getSessionID(c) + prefix := logPrefix(sessionID, account.Name) + + // 获取上游配置 + baseURL := strings.TrimSpace(account.GetCredential("base_url")) + apiKey := strings.TrimSpace(account.GetCredential("api_key")) + if baseURL == "" || apiKey == "" { + return nil, fmt.Errorf("upstream account missing base_url or api_key") + } + baseURL = strings.TrimSuffix(baseURL, "/") + + // 解析请求获取模型信息 + var claudeReq antigravity.ClaudeRequest + if err := json.Unmarshal(body, &claudeReq); err != nil { + return nil, fmt.Errorf("parse claude request: %w", err) + } + if strings.TrimSpace(claudeReq.Model) == "" { + return nil, fmt.Errorf("missing model") + } + originalModel := claudeReq.Model + billingModel := originalModel + + // 构建上游请求 URL + upstreamURL := baseURL + "/v1/messages" + + // 创建请求 + req, err := http.NewRequestWithContext(ctx, http.MethodPost, upstreamURL, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("create upstream request: %w", err) + } + + // 设置请求头 + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("x-api-key", apiKey) // Claude API 兼容 + + // 透传 Claude 相关 headers + if v := c.GetHeader("anthropic-version"); v != "" { + req.Header.Set("anthropic-version", v) + } + if v := c.GetHeader("anthropic-beta"); v != "" { + req.Header.Set("anthropic-beta", v) + } + + // 代理 URL + proxyURL := "" + if account.ProxyID != nil && account.Proxy != nil { + proxyURL = account.Proxy.URL() + } + + // 发送请求 + resp, err := s.httpUpstream.Do(req, proxyURL, account.ID, account.Concurrency) + if err != nil { + log.Printf("%s upstream request failed: %v", prefix, err) + return nil, fmt.Errorf("upstream request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + // 处理错误响应 + if resp.StatusCode >= 400 { + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 2<<20)) + + // 429 错误时标记账号限流 + if resp.StatusCode == http.StatusTooManyRequests { + s.handleUpstreamError(ctx, prefix, account, resp.StatusCode, resp.Header, respBody, AntigravityQuotaScopeClaude) + } + + // 透传上游错误 + c.Header("Content-Type", resp.Header.Get("Content-Type")) + c.Status(resp.StatusCode) + _, _ = c.Writer.Write(respBody) + + return &ForwardResult{ + Model: billingModel, + }, nil + } + + // 处理成功响应(流式/非流式) + var usage *ClaudeUsage + var firstTokenMs *int + + if claudeReq.Stream { + // 流式响应:透传 + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + c.Header("Connection", "keep-alive") + c.Header("X-Accel-Buffering", "no") + c.Status(http.StatusOK) + + usage, firstTokenMs = s.streamUpstreamResponse(c, resp, startTime) + } else { + // 非流式响应:直接透传 + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("read upstream response: %w", err) + } + + // 提取 usage + usage = s.extractClaudeUsage(respBody) + + c.Header("Content-Type", resp.Header.Get("Content-Type")) + c.Status(http.StatusOK) + _, _ = c.Writer.Write(respBody) + } + + // 构建计费结果 + duration := time.Since(startTime) + log.Printf("%s status=success duration_ms=%d", prefix, duration.Milliseconds()) + + return &ForwardResult{ + Model: billingModel, + Stream: claudeReq.Stream, + Duration: duration, + FirstTokenMs: firstTokenMs, + Usage: ClaudeUsage{ + InputTokens: usage.InputTokens, + OutputTokens: usage.OutputTokens, + CacheReadInputTokens: usage.CacheReadInputTokens, + CacheCreationInputTokens: usage.CacheCreationInputTokens, + }, + }, nil +} + +// streamUpstreamResponse 透传上游流式响应并提取 usage +func (s *AntigravityGatewayService) streamUpstreamResponse(c *gin.Context, resp *http.Response, startTime time.Time) (*ClaudeUsage, *int) { + usage := &ClaudeUsage{} + var firstTokenMs *int + var firstTokenRecorded bool + + scanner := bufio.NewScanner(resp.Body) + buf := make([]byte, 0, 64*1024) + scanner.Buffer(buf, 1024*1024) + + for scanner.Scan() { + line := scanner.Bytes() + + // 记录首 token 时间 + if !firstTokenRecorded && len(line) > 0 { + ms := int(time.Since(startTime).Milliseconds()) + firstTokenMs = &ms + firstTokenRecorded = true + } + + // 尝试从 message_delta 或 message_stop 事件提取 usage + if bytes.HasPrefix(line, []byte("data: ")) { + dataStr := bytes.TrimPrefix(line, []byte("data: ")) + var event map[string]any + if json.Unmarshal(dataStr, &event) == nil { + if u, ok := event["usage"].(map[string]any); ok { + if v, ok := u["input_tokens"].(float64); ok && int(v) > 0 { + usage.InputTokens = int(v) + } + if v, ok := u["output_tokens"].(float64); ok && int(v) > 0 { + usage.OutputTokens = int(v) + } + if v, ok := u["cache_read_input_tokens"].(float64); ok && int(v) > 0 { + usage.CacheReadInputTokens = int(v) + } + if v, ok := u["cache_creation_input_tokens"].(float64); ok && int(v) > 0 { + usage.CacheCreationInputTokens = int(v) + } + } + } + } + + // 透传行 + _, _ = c.Writer.Write(line) + _, _ = c.Writer.Write([]byte("\n")) + c.Writer.Flush() + } + + return usage, firstTokenMs +} + +// extractClaudeUsage 从非流式 Claude 响应提取 usage +func (s *AntigravityGatewayService) extractClaudeUsage(body []byte) *ClaudeUsage { + usage := &ClaudeUsage{} + var resp map[string]any + if json.Unmarshal(body, &resp) != nil { + return usage + } + if u, ok := resp["usage"].(map[string]any); ok { + if v, ok := u["input_tokens"].(float64); ok { + usage.InputTokens = int(v) + } + if v, ok := u["output_tokens"].(float64); ok { + usage.OutputTokens = int(v) + } + if v, ok := u["cache_read_input_tokens"].(float64); ok { + usage.CacheReadInputTokens = int(v) + } + if v, ok := u["cache_creation_input_tokens"].(float64); ok { + usage.CacheCreationInputTokens = int(v) + } + } + return usage +} + // ForwardGemini 转发 Gemini 协议请求 func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Context, account *Account, originalModel string, action string, stream bool, body []byte) (*ForwardResult, error) { startTime := time.Now() diff --git a/backend/internal/service/antigravity_quota_scope.go b/backend/internal/service/antigravity_quota_scope.go index 34cd9a4c..e1a0a1f2 100644 --- a/backend/internal/service/antigravity_quota_scope.go +++ b/backend/internal/service/antigravity_quota_scope.go @@ -1,6 +1,7 @@ package service import ( + "slices" "strings" "time" ) @@ -16,6 +17,21 @@ const ( AntigravityQuotaScopeGeminiImage AntigravityQuotaScope = "gemini_image" ) +// IsScopeSupported 检查给定的 scope 是否在分组支持的 scope 列表中 +func IsScopeSupported(supportedScopes []string, scope AntigravityQuotaScope) bool { + if len(supportedScopes) == 0 { + // 未配置时默认全部支持 + return true + } + supported := slices.Contains(supportedScopes, string(scope)) + return supported +} + +// ResolveAntigravityQuotaScope 根据模型名称解析配额域(导出版本) +func ResolveAntigravityQuotaScope(requestedModel string) (AntigravityQuotaScope, bool) { + return resolveAntigravityQuotaScope(requestedModel) +} + // resolveAntigravityQuotaScope 根据模型名称解析配额域 func resolveAntigravityQuotaScope(requestedModel string) (AntigravityQuotaScope, bool) { model := normalizeAntigravityModelName(requestedModel) diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index 5cb2fbfb..b56e7cf3 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -44,6 +44,9 @@ type APIKeyAuthGroupSnapshot struct { ModelRouting map[string][]int64 `json:"model_routing,omitempty"` ModelRoutingEnabled bool `json:"model_routing_enabled"` MCPXMLInject bool `json:"mcp_xml_inject"` + + // 支持的模型系列(仅 antigravity 平台使用) + SupportedModelScopes []string `json:"supported_model_scopes,omitempty"` } // APIKeyAuthCacheEntry 缓存条目,支持负缓存 diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index 5fb4edee..d4b2347e 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -241,6 +241,7 @@ func (s *APIKeyService) snapshotFromAPIKey(apiKey *APIKey) *APIKeyAuthSnapshot { ModelRouting: apiKey.Group.ModelRouting, ModelRoutingEnabled: apiKey.Group.ModelRoutingEnabled, MCPXMLInject: apiKey.Group.MCPXMLInject, + SupportedModelScopes: apiKey.Group.SupportedModelScopes, } } return snapshot @@ -287,6 +288,7 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho ModelRouting: snapshot.Group.ModelRouting, ModelRoutingEnabled: snapshot.Group.ModelRoutingEnabled, MCPXMLInject: snapshot.Group.MCPXMLInject, + SupportedModelScopes: snapshot.Group.SupportedModelScopes, } } return apiKey diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index eee8bddd..218b7aae 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -31,6 +31,7 @@ const ( AccountTypeOAuth = domain.AccountTypeOAuth // OAuth类型账号(full scope: profile + inference) AccountTypeSetupToken = domain.AccountTypeSetupToken // Setup Token类型账号(inference only scope) AccountTypeAPIKey = domain.AccountTypeAPIKey // API Key类型账号 + AccountTypeUpstream = domain.AccountTypeUpstream // 上游透传类型账号(通过 Base URL + API Key 连接上游) ) // Redeem type constants diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 0fb9eced..30078e3c 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -92,6 +92,9 @@ var ( // ErrClaudeCodeOnly 表示分组仅允许 Claude Code 客户端访问 var ErrClaudeCodeOnly = errors.New("this group only allows Claude Code clients") +// ErrModelScopeNotSupported 表示请求的模型系列不在分组支持的范围内 +var ErrModelScopeNotSupported = errors.New("model scope not supported by this group") + // allowedHeaders 白名单headers(参考CRS项目) var allowedHeaders = map[string]bool{ "accept": true, @@ -582,6 +585,13 @@ func (s *GatewayService) SelectAccountWithLoadAwareness(ctx context.Context, gro log.Printf("[ModelRoutingDebug] load-aware enabled: group_id=%v model=%s session=%s platform=%s", derefGroupID(groupID), requestedModel, shortSessionHash(sessionHash), platform) } + // Antigravity 模型系列检查(在账号选择前检查,确保所有代码路径都经过此检查) + if platform == PlatformAntigravity && groupID != nil && requestedModel != "" { + if err := s.checkAntigravityModelScope(ctx, *groupID, requestedModel); err != nil { + return nil, err + } + } + accounts, useMixed, err := s.listSchedulableAccounts(ctx, groupID, platform, hasForcePlatform) if err != nil { return nil, err @@ -1477,6 +1487,13 @@ func shuffleWithinPriority(accounts []*Account) { // selectAccountForModelWithPlatform 选择单平台账户(完全隔离) func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, groupID *int64, sessionHash string, requestedModel string, excludedIDs map[int64]struct{}, platform string) (*Account, error) { + // 对 Antigravity 平台,检查请求的模型系列是否在分组支持范围内 + if platform == PlatformAntigravity && groupID != nil && requestedModel != "" { + if err := s.checkAntigravityModelScope(ctx, *groupID, requestedModel); err != nil { + return nil, err + } + } + preferOAuth := platform == PlatformGemini routingAccountIDs := s.routingAccountIDsForRequest(ctx, groupID, requestedModel, platform) @@ -3898,6 +3915,27 @@ func (s *GatewayService) validateUpstreamBaseURL(raw string) (string, error) { return normalized, nil } +// checkAntigravityModelScope 检查 Antigravity 平台的模型系列是否在分组支持范围内 +func (s *GatewayService) checkAntigravityModelScope(ctx context.Context, groupID int64, requestedModel string) error { + scope, ok := ResolveAntigravityQuotaScope(requestedModel) + if !ok { + return nil // 无法解析 scope,跳过检查 + } + + group, err := s.resolveGroupByID(ctx, groupID) + if err != nil { + return nil // 查询失败时放行 + } + if group == nil { + return nil // 分组不存在时放行 + } + + if !IsScopeSupported(group.SupportedModelScopes, scope) { + return ErrModelScopeNotSupported + } + return nil +} + // GetAvailableModels returns the list of models available for a group // It aggregates model_mapping keys from all schedulable accounts in the group func (s *GatewayService) GetAvailableModels(ctx context.Context, groupID *int64, platform string) []string { diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 7f1825c6..1302047a 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -41,6 +41,10 @@ type Group struct { // MCP XML 协议注入开关(仅 antigravity 平台使用) MCPXMLInject bool + // 支持的模型系列(仅 antigravity 平台使用) + // 可选值: claude, gemini_text, gemini_image + SupportedModelScopes []string + CreatedAt time.Time UpdatedAt time.Time diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index c565607e..0f589eb3 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -21,6 +21,11 @@ type User struct { CreatedAt time.Time UpdatedAt time.Time + // TOTP 双因素认证字段 + TotpSecretEncrypted *string // AES-256-GCM 加密的 TOTP 密钥 + TotpEnabled bool // 是否启用 TOTP + TotpEnabledAt *time.Time // TOTP 启用时间 + APIKeys []APIKey Subscriptions []UserSubscription } diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 99bf7fd0..1bfb392e 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -39,7 +39,7 @@ type UserRepository interface { ExistsByEmail(ctx context.Context, email string) (bool, error) RemoveGroupFromAllowedGroups(ctx context.Context, groupID int64) (int64, error) - // TOTP 相关方法 + // TOTP 双因素认证 UpdateTotpSecret(ctx context.Context, userID int64, encryptedSecret *string) error EnableTotp(ctx context.Context, userID int64) error DisableTotp(ctx context.Context, userID int64) error diff --git a/backend/migrations/046_add_group_supported_model_scopes.sql b/backend/migrations/046_add_group_supported_model_scopes.sql new file mode 100644 index 00000000..0b2b3968 --- /dev/null +++ b/backend/migrations/046_add_group_supported_model_scopes.sql @@ -0,0 +1,6 @@ +-- 添加分组支持的模型系列字段 +ALTER TABLE groups +ADD COLUMN IF NOT EXISTS supported_model_scopes JSONB NOT NULL +DEFAULT '["claude", "gemini_text", "gemini_image"]'::jsonb; + +COMMENT ON COLUMN groups.supported_model_scopes IS '支持的模型系列:claude, gemini_text, gemini_image'; diff --git a/docs/rename_local_migrations_20260202.sql b/docs/rename_local_migrations_20260202.sql new file mode 100644 index 00000000..a5ba2ef1 --- /dev/null +++ b/docs/rename_local_migrations_20260202.sql @@ -0,0 +1,27 @@ +-- 修正 schema_migrations 中“本地改名”的迁移文件名 +-- 适用场景:你已执行过旧文件名的迁移,合并后仅改了自己这边的文件名 + +BEGIN; + +UPDATE schema_migrations +SET filename = '042b_add_ops_system_metrics_switch_count.sql' +WHERE filename = '042_add_ops_system_metrics_switch_count.sql' + AND NOT EXISTS ( + SELECT 1 FROM schema_migrations WHERE filename = '042b_add_ops_system_metrics_switch_count.sql' + ); + +UPDATE schema_migrations +SET filename = '043b_add_group_invalid_request_fallback.sql' +WHERE filename = '043_add_group_invalid_request_fallback.sql' + AND NOT EXISTS ( + SELECT 1 FROM schema_migrations WHERE filename = '043b_add_group_invalid_request_fallback.sql' + ); + +UPDATE schema_migrations +SET filename = '044b_add_group_mcp_xml_inject.sql' +WHERE filename = '044_add_group_mcp_xml_inject.sql' + AND NOT EXISTS ( + SELECT 1 FROM schema_migrations WHERE filename = '044b_add_group_mcp_xml_inject.sql' + ); + +COMMIT; diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 144241ff..580a98fa 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -614,21 +614,87 @@ - +
-
-
+ + + +
+
+ + +
+
+ + +

{{ t('admin.accounts.upstream.baseUrlHint') }}

+
+
+ + +

{{ t('admin.accounts.upstream.apiKeyHint') }}

@@ -1940,6 +2006,9 @@ const customErrorCodeInput = ref(null) const interceptWarmupRequests = ref(false) const autoPauseOnExpired = ref(true) const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling +const antigravityAccountType = ref<'oauth' | 'upstream'>('oauth') // For antigravity: oauth or upstream +const upstreamBaseUrl = ref('') // For upstream type: base URL +const upstreamApiKey = ref('') // For upstream type: API key const tempUnschedEnabled = ref(false) const tempUnschedRules = ref([]) const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one') @@ -2037,7 +2106,13 @@ const form = reactive({ }) // Helper to check if current type needs OAuth flow -const isOAuthFlow = computed(() => accountCategory.value === 'oauth-based') +const isOAuthFlow = computed(() => { + // Antigravity upstream 类型不需要 OAuth 流程 + if (form.platform === 'antigravity' && antigravityAccountType.value === 'upstream') { + return false + } + return accountCategory.value === 'oauth-based' +}) const isManualInputMethod = computed(() => { return oauthFlowRef.value?.inputMethod === 'manual' @@ -2077,10 +2152,15 @@ watch( } ) -// Sync form.type based on accountCategory and addMethod +// Sync form.type based on accountCategory, addMethod, and antigravityAccountType watch( - [accountCategory, addMethod], - ([category, method]) => { + [accountCategory, addMethod, antigravityAccountType], + ([category, method, agType]) => { + // Antigravity upstream 类型 + if (form.platform === 'antigravity' && agType === 'upstream') { + form.type = 'upstream' + return + } if (category === 'oauth-based') { form.type = method as AccountType // 'oauth' or 'setup-token' } else { @@ -2108,9 +2188,10 @@ watch( if (newPlatform !== 'anthropic') { interceptWarmupRequests.value = false } - // Antigravity only supports OAuth + // Antigravity: reset to OAuth by default, but allow upstream selection if (newPlatform === 'antigravity') { accountCategory.value = 'oauth-based' + antigravityAccountType.value = 'oauth' } // Reset OAuth states oauth.resetState() @@ -2343,6 +2424,9 @@ const resetForm = () => { sessionIdleTimeout.value = null tlsFingerprintEnabled.value = false sessionIdMaskingEnabled.value = false + antigravityAccountType.value = 'oauth' + upstreamBaseUrl.value = '' + upstreamApiKey.value = '' tempUnschedEnabled.value = false tempUnschedRules.value = [] geminiOAuthType.value = 'code_assist' @@ -2371,6 +2455,36 @@ const handleSubmit = async () => { return } + // For Antigravity upstream type, create directly + if (form.platform === 'antigravity' && antigravityAccountType.value === 'upstream') { + if (!form.name.trim()) { + appStore.showError(t('admin.accounts.pleaseEnterAccountName')) + return + } + if (!upstreamBaseUrl.value.trim()) { + appStore.showError(t('admin.accounts.upstream.pleaseEnterBaseUrl')) + return + } + if (!upstreamApiKey.value.trim()) { + appStore.showError(t('admin.accounts.upstream.pleaseEnterApiKey')) + return + } + + submitting.value = true + try { + const credentials: Record = { + base_url: upstreamBaseUrl.value.trim(), + api_key: upstreamApiKey.value.trim() + } + await createAccountAndFinish(form.platform, 'upstream', credentials) + } catch (error: any) { + appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate')) + } finally { + submitting.value = false + } + return + } + // For apikey type, create directly if (!apiKeyValue.value.trim()) { appStore.showError(t('admin.accounts.pleaseEnterApiKey')) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 8afaa0c4..7d6c2f5d 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1034,6 +1034,14 @@ export default { tooltip: 'When enabled, if the request contains MCP tools, an XML format call protocol prompt will be injected into the system prompt. Disable this to avoid interference with certain clients.', enabled: 'Enabled', disabled: 'Disabled' + }, + supportedScopes: { + title: 'Supported Model Families', + tooltip: 'Select the model families this group supports. Unchecked families will not be routed to this group.', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: 'Select at least one model family' } }, @@ -1173,7 +1181,9 @@ export default { responsesApi: 'Responses API', googleOauth: 'Google OAuth', codeAssist: 'Code Assist', - antigravityOauth: 'Antigravity OAuth' + antigravityOauth: 'Antigravity OAuth', + upstream: 'Upstream', + upstreamDesc: 'Connect via Base URL + API Key' }, status: { active: 'Active', @@ -1431,6 +1441,15 @@ export default { pleaseEnterApiKey: 'Please enter API Key', apiKeyIsRequired: 'API Key is required', leaveEmptyToKeep: 'Leave empty to keep current key', + // Upstream type + upstream: { + baseUrl: 'Upstream Base URL', + baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://upstream.example.com', + apiKey: 'Upstream API Key', + apiKeyHint: 'API Key for the upstream service', + pleaseEnterBaseUrl: 'Please enter upstream Base URL', + pleaseEnterApiKey: 'Please enter upstream API Key' + }, // OAuth flow oauth: { title: 'Claude Account Authorization', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 43fdfd59..feec93a7 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -1109,6 +1109,14 @@ export default { tooltip: '启用后,当请求包含 MCP 工具时,会在 system prompt 中注入 XML 格式调用协议提示词。关闭此选项可避免对某些客户端造成干扰。', enabled: '已启用', disabled: '已禁用' + }, + supportedScopes: { + title: '支持的模型系列', + tooltip: '选择此分组支持的模型系列。未勾选的系列将不会被路由到此分组。', + claude: 'Claude', + geminiText: 'Gemini Text', + geminiImage: 'Gemini Image', + hint: '至少选择一个模型系列' } }, @@ -1294,6 +1302,8 @@ export default { googleOauth: 'Google OAuth', codeAssist: 'Code Assist', antigravityOauth: 'Antigravity OAuth', + upstream: '对接上游', + upstreamDesc: '通过 Base URL + API Key 连接上游', api_key: 'API Key', cookie: 'Cookie' }, @@ -1563,6 +1573,15 @@ export default { pleaseEnterApiKey: '请输入 API Key', apiKeyIsRequired: 'API Key 是必需的', leaveEmptyToKeep: '留空以保持当前密钥', + // Upstream type + upstream: { + baseUrl: '上游 Base URL', + baseUrlHint: '上游 Antigravity 服务的地址,例如:https://upstream.example.com', + apiKey: '上游 API Key', + apiKeyHint: '上游服务的 API Key', + pleaseEnterBaseUrl: '请输入上游 Base URL', + pleaseEnterApiKey: '请输入上游 API Key' + }, // OAuth flow oauth: { title: 'Claude 账号授权', diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 51e680d7..d65e6b46 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -365,6 +365,11 @@ export interface AdminGroup extends Group { // MCP XML 协议注入(仅 antigravity 平台使用) mcp_xml_inject: boolean + + // 支持的模型系列(仅 antigravity 平台使用) + supported_model_scopes?: string[] + + // 分组下账号数量(仅管理员可见) account_count?: number } @@ -414,6 +419,7 @@ export interface CreateGroupRequest { claude_code_only?: boolean fallback_group_id?: number | null fallback_group_id_on_invalid_request?: number | null + supported_model_scopes?: string[] } export interface UpdateGroupRequest { @@ -433,12 +439,13 @@ export interface UpdateGroupRequest { claude_code_only?: boolean fallback_group_id?: number | null fallback_group_id_on_invalid_request?: number | null + supported_model_scopes?: string[] } // ==================== Account & Proxy Types ==================== export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' -export type AccountType = 'oauth' | 'setup-token' | 'apikey' +export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' export type OAuthAddMethod = 'oauth' | 'setup-token' export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h' diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 323fef75..80ea1f7e 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -404,6 +404,62 @@
+ +
+
+ + +
+ +
+
+

+ {{ t('admin.groups.supportedScopes.tooltip') }} +

+
+
+
+
+
+
+ + + +
+

{{ t('admin.groups.supportedScopes.hint') }}

+
+
@@ -907,6 +963,62 @@
+ +
+
+ + +
+ +
+
+

+ {{ t('admin.groups.supportedScopes.tooltip') }} +

+
+
+
+
+
+
+ + + +
+

{{ t('admin.groups.supportedScopes.hint') }}

+
+
@@ -1402,6 +1514,9 @@ const createForm = reactive({ fallback_group_id_on_invalid_request: null as number | null, // 模型路由开关 model_routing_enabled: false, + // 支持的模型系列(仅 antigravity 平台) + supported_model_scopes: ['claude', 'gemini_text', 'gemini_image'] as string[], + // MCP XML 协议注入开关(仅 antigravity 平台) mcp_xml_inject: true }) @@ -1472,6 +1587,26 @@ const removeSelectedAccount = (ruleIndex: number, accountId: number, isEdit: boo rule.accounts = rule.accounts.filter(a => a.id !== accountId) } +// 切换创建表单的模型系列选择 +const toggleCreateScope = (scope: string) => { + const idx = createForm.supported_model_scopes.indexOf(scope) + if (idx === -1) { + createForm.supported_model_scopes.push(scope) + } else { + createForm.supported_model_scopes.splice(idx, 1) + } +} + +// 切换编辑表单的模型系列选择 +const toggleEditScope = (scope: string) => { + const idx = editForm.supported_model_scopes.indexOf(scope) + if (idx === -1) { + editForm.supported_model_scopes.push(scope) + } else { + editForm.supported_model_scopes.splice(idx, 1) + } +} + // 处理账号搜索输入框聚焦 const onAccountSearchFocus = (ruleIndex: number, isEdit: boolean = false) => { const key = `${isEdit ? 'edit' : 'create'}-${ruleIndex}` @@ -1575,6 +1710,9 @@ const editForm = reactive({ fallback_group_id_on_invalid_request: null as number | null, // 模型路由开关 model_routing_enabled: false, + // 支持的模型系列(仅 antigravity 平台) + supported_model_scopes: ['claude', 'gemini_text', 'gemini_image'] as string[], + // MCP XML 协议注入开关(仅 antigravity 平台) mcp_xml_inject: true }) @@ -1658,6 +1796,7 @@ const closeCreateModal = () => { createForm.claude_code_only = false createForm.fallback_group_id = null createForm.fallback_group_id_on_invalid_request = null + createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image'] createForm.mcp_xml_inject = true createModelRoutingRules.value = [] } @@ -1710,6 +1849,7 @@ const handleEdit = async (group: AdminGroup) => { editForm.fallback_group_id = group.fallback_group_id editForm.fallback_group_id_on_invalid_request = group.fallback_group_id_on_invalid_request editForm.model_routing_enabled = group.model_routing_enabled || false + editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image'] editForm.mcp_xml_inject = group.mcp_xml_inject ?? true // 加载模型路由规则(异步加载账号名称) editModelRoutingRules.value = await convertApiFormatToRoutingRules(group.model_routing)