diff --git a/backend/ent/group.go b/backend/ent/group.go index 3db54a64..fc691a9b 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -80,6 +80,10 @@ type Group struct { SortOrder int `json:"sort_order,omitempty"` // 是否允许 /v1/messages 调度到此 OpenAI 分组 AllowMessagesDispatch bool `json:"allow_messages_dispatch,omitempty"` + // 仅允许非 apikey 类型账号关联到此分组 + RequireOauthOnly bool `json:"require_oauth_only,omitempty"` + // 调度时仅允许 privacy 已成功设置的账号 + RequirePrivacySet bool `json:"require_privacy_set,omitempty"` // 默认映射模型 ID,当账号级映射找不到时使用此值 DefaultMappedModel string `json:"default_mapped_model,omitempty"` // Edges holds the relations/edges for other nodes in the graph. @@ -190,7 +194,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { switch columns[i] { case group.FieldModelRouting, group.FieldSupportedModelScopes: values[i] = new([]byte) - case group.FieldIsExclusive, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch: + case group.FieldIsExclusive, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet: values[i] = new(sql.NullBool) case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k, group.FieldSoraImagePrice360, group.FieldSoraImagePrice540, group.FieldSoraVideoPricePerRequest, group.FieldSoraVideoPricePerRequestHd: values[i] = new(sql.NullFloat64) @@ -425,6 +429,18 @@ func (_m *Group) assignValues(columns []string, values []any) error { } else if value.Valid { _m.AllowMessagesDispatch = value.Bool } + case group.FieldRequireOauthOnly: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field require_oauth_only", values[i]) + } else if value.Valid { + _m.RequireOauthOnly = value.Bool + } + case group.FieldRequirePrivacySet: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field require_privacy_set", values[i]) + } else if value.Valid { + _m.RequirePrivacySet = value.Bool + } case group.FieldDefaultMappedModel: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field default_mapped_model", values[i]) @@ -628,6 +644,12 @@ func (_m *Group) String() string { builder.WriteString("allow_messages_dispatch=") builder.WriteString(fmt.Sprintf("%v", _m.AllowMessagesDispatch)) builder.WriteString(", ") + builder.WriteString("require_oauth_only=") + builder.WriteString(fmt.Sprintf("%v", _m.RequireOauthOnly)) + builder.WriteString(", ") + builder.WriteString("require_privacy_set=") + builder.WriteString(fmt.Sprintf("%v", _m.RequirePrivacySet)) + builder.WriteString(", ") builder.WriteString("default_mapped_model=") builder.WriteString(_m.DefaultMappedModel) builder.WriteByte(')') diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 2612b6cf..35222127 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -77,6 +77,10 @@ const ( FieldSortOrder = "sort_order" // FieldAllowMessagesDispatch holds the string denoting the allow_messages_dispatch field in the database. FieldAllowMessagesDispatch = "allow_messages_dispatch" + // FieldRequireOauthOnly holds the string denoting the require_oauth_only field in the database. + FieldRequireOauthOnly = "require_oauth_only" + // FieldRequirePrivacySet holds the string denoting the require_privacy_set field in the database. + FieldRequirePrivacySet = "require_privacy_set" // FieldDefaultMappedModel holds the string denoting the default_mapped_model field in the database. FieldDefaultMappedModel = "default_mapped_model" // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. @@ -185,6 +189,8 @@ var Columns = []string{ FieldSupportedModelScopes, FieldSortOrder, FieldAllowMessagesDispatch, + FieldRequireOauthOnly, + FieldRequirePrivacySet, FieldDefaultMappedModel, } @@ -255,6 +261,10 @@ var ( DefaultSortOrder int // DefaultAllowMessagesDispatch holds the default value on creation for the "allow_messages_dispatch" field. DefaultAllowMessagesDispatch bool + // DefaultRequireOauthOnly holds the default value on creation for the "require_oauth_only" field. + DefaultRequireOauthOnly bool + // DefaultRequirePrivacySet holds the default value on creation for the "require_privacy_set" field. + DefaultRequirePrivacySet bool // DefaultDefaultMappedModel holds the default value on creation for the "default_mapped_model" field. DefaultDefaultMappedModel string // DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save. @@ -414,6 +424,16 @@ func ByAllowMessagesDispatch(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldAllowMessagesDispatch, opts...).ToFunc() } +// ByRequireOauthOnly orders the results by the require_oauth_only field. +func ByRequireOauthOnly(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRequireOauthOnly, opts...).ToFunc() +} + +// ByRequirePrivacySet orders the results by the require_privacy_set field. +func ByRequirePrivacySet(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldRequirePrivacySet, opts...).ToFunc() +} + // ByDefaultMappedModel orders the results by the default_mapped_model field. func ByDefaultMappedModel(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDefaultMappedModel, opts...).ToFunc() diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index 5dd8759e..41bd575a 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -200,6 +200,16 @@ func AllowMessagesDispatch(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldAllowMessagesDispatch, v)) } +// RequireOauthOnly applies equality check predicate on the "require_oauth_only" field. It's identical to RequireOauthOnlyEQ. +func RequireOauthOnly(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v)) +} + +// RequirePrivacySet applies equality check predicate on the "require_privacy_set" field. It's identical to RequirePrivacySetEQ. +func RequirePrivacySet(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldRequirePrivacySet, v)) +} + // DefaultMappedModel applies equality check predicate on the "default_mapped_model" field. It's identical to DefaultMappedModelEQ. func DefaultMappedModel(v string) predicate.Group { return predicate.Group(sql.FieldEQ(FieldDefaultMappedModel, v)) @@ -1490,6 +1500,26 @@ func AllowMessagesDispatchNEQ(v bool) predicate.Group { return predicate.Group(sql.FieldNEQ(FieldAllowMessagesDispatch, v)) } +// RequireOauthOnlyEQ applies the EQ predicate on the "require_oauth_only" field. +func RequireOauthOnlyEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldRequireOauthOnly, v)) +} + +// RequireOauthOnlyNEQ applies the NEQ predicate on the "require_oauth_only" field. +func RequireOauthOnlyNEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldRequireOauthOnly, v)) +} + +// RequirePrivacySetEQ applies the EQ predicate on the "require_privacy_set" field. +func RequirePrivacySetEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldRequirePrivacySet, v)) +} + +// RequirePrivacySetNEQ applies the NEQ predicate on the "require_privacy_set" field. +func RequirePrivacySetNEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldRequirePrivacySet, v)) +} + // DefaultMappedModelEQ applies the EQ predicate on the "default_mapped_model" field. func DefaultMappedModelEQ(v string) predicate.Group { return predicate.Group(sql.FieldEQ(FieldDefaultMappedModel, v)) diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 6db5b974..a635dfd9 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -438,6 +438,34 @@ func (_c *GroupCreate) SetNillableAllowMessagesDispatch(v *bool) *GroupCreate { return _c } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (_c *GroupCreate) SetRequireOauthOnly(v bool) *GroupCreate { + _c.mutation.SetRequireOauthOnly(v) + return _c +} + +// SetNillableRequireOauthOnly sets the "require_oauth_only" field if the given value is not nil. +func (_c *GroupCreate) SetNillableRequireOauthOnly(v *bool) *GroupCreate { + if v != nil { + _c.SetRequireOauthOnly(*v) + } + return _c +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (_c *GroupCreate) SetRequirePrivacySet(v bool) *GroupCreate { + _c.mutation.SetRequirePrivacySet(v) + return _c +} + +// SetNillableRequirePrivacySet sets the "require_privacy_set" field if the given value is not nil. +func (_c *GroupCreate) SetNillableRequirePrivacySet(v *bool) *GroupCreate { + if v != nil { + _c.SetRequirePrivacySet(*v) + } + return _c +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (_c *GroupCreate) SetDefaultMappedModel(v string) *GroupCreate { _c.mutation.SetDefaultMappedModel(v) @@ -645,6 +673,14 @@ func (_c *GroupCreate) defaults() error { v := group.DefaultAllowMessagesDispatch _c.mutation.SetAllowMessagesDispatch(v) } + if _, ok := _c.mutation.RequireOauthOnly(); !ok { + v := group.DefaultRequireOauthOnly + _c.mutation.SetRequireOauthOnly(v) + } + if _, ok := _c.mutation.RequirePrivacySet(); !ok { + v := group.DefaultRequirePrivacySet + _c.mutation.SetRequirePrivacySet(v) + } if _, ok := _c.mutation.DefaultMappedModel(); !ok { v := group.DefaultDefaultMappedModel _c.mutation.SetDefaultMappedModel(v) @@ -722,6 +758,12 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.AllowMessagesDispatch(); !ok { return &ValidationError{Name: "allow_messages_dispatch", err: errors.New(`ent: missing required field "Group.allow_messages_dispatch"`)} } + if _, ok := _c.mutation.RequireOauthOnly(); !ok { + return &ValidationError{Name: "require_oauth_only", err: errors.New(`ent: missing required field "Group.require_oauth_only"`)} + } + if _, ok := _c.mutation.RequirePrivacySet(); !ok { + return &ValidationError{Name: "require_privacy_set", err: errors.New(`ent: missing required field "Group.require_privacy_set"`)} + } if _, ok := _c.mutation.DefaultMappedModel(); !ok { return &ValidationError{Name: "default_mapped_model", err: errors.New(`ent: missing required field "Group.default_mapped_model"`)} } @@ -881,6 +923,14 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value) _node.AllowMessagesDispatch = value } + if value, ok := _c.mutation.RequireOauthOnly(); ok { + _spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value) + _node.RequireOauthOnly = value + } + if value, ok := _c.mutation.RequirePrivacySet(); ok { + _spec.SetField(group.FieldRequirePrivacySet, field.TypeBool, value) + _node.RequirePrivacySet = value + } if value, ok := _c.mutation.DefaultMappedModel(); ok { _spec.SetField(group.FieldDefaultMappedModel, field.TypeString, value) _node.DefaultMappedModel = value @@ -1587,6 +1637,30 @@ func (u *GroupUpsert) UpdateAllowMessagesDispatch() *GroupUpsert { return u } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (u *GroupUpsert) SetRequireOauthOnly(v bool) *GroupUpsert { + u.Set(group.FieldRequireOauthOnly, v) + return u +} + +// UpdateRequireOauthOnly sets the "require_oauth_only" field to the value that was provided on create. +func (u *GroupUpsert) UpdateRequireOauthOnly() *GroupUpsert { + u.SetExcluded(group.FieldRequireOauthOnly) + return u +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (u *GroupUpsert) SetRequirePrivacySet(v bool) *GroupUpsert { + u.Set(group.FieldRequirePrivacySet, v) + return u +} + +// UpdateRequirePrivacySet sets the "require_privacy_set" field to the value that was provided on create. +func (u *GroupUpsert) UpdateRequirePrivacySet() *GroupUpsert { + u.SetExcluded(group.FieldRequirePrivacySet) + return u +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (u *GroupUpsert) SetDefaultMappedModel(v string) *GroupUpsert { u.Set(group.FieldDefaultMappedModel, v) @@ -2281,6 +2355,34 @@ func (u *GroupUpsertOne) UpdateAllowMessagesDispatch() *GroupUpsertOne { }) } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (u *GroupUpsertOne) SetRequireOauthOnly(v bool) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetRequireOauthOnly(v) + }) +} + +// UpdateRequireOauthOnly sets the "require_oauth_only" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateRequireOauthOnly() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateRequireOauthOnly() + }) +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (u *GroupUpsertOne) SetRequirePrivacySet(v bool) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetRequirePrivacySet(v) + }) +} + +// UpdateRequirePrivacySet sets the "require_privacy_set" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateRequirePrivacySet() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateRequirePrivacySet() + }) +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (u *GroupUpsertOne) SetDefaultMappedModel(v string) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { @@ -3143,6 +3245,34 @@ func (u *GroupUpsertBulk) UpdateAllowMessagesDispatch() *GroupUpsertBulk { }) } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (u *GroupUpsertBulk) SetRequireOauthOnly(v bool) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetRequireOauthOnly(v) + }) +} + +// UpdateRequireOauthOnly sets the "require_oauth_only" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateRequireOauthOnly() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateRequireOauthOnly() + }) +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (u *GroupUpsertBulk) SetRequirePrivacySet(v bool) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetRequirePrivacySet(v) + }) +} + +// UpdateRequirePrivacySet sets the "require_privacy_set" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateRequirePrivacySet() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateRequirePrivacySet() + }) +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (u *GroupUpsertBulk) SetDefaultMappedModel(v string) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index b3698596..a9a4b9da 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -639,6 +639,34 @@ func (_u *GroupUpdate) SetNillableAllowMessagesDispatch(v *bool) *GroupUpdate { return _u } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (_u *GroupUpdate) SetRequireOauthOnly(v bool) *GroupUpdate { + _u.mutation.SetRequireOauthOnly(v) + return _u +} + +// SetNillableRequireOauthOnly sets the "require_oauth_only" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableRequireOauthOnly(v *bool) *GroupUpdate { + if v != nil { + _u.SetRequireOauthOnly(*v) + } + return _u +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (_u *GroupUpdate) SetRequirePrivacySet(v bool) *GroupUpdate { + _u.mutation.SetRequirePrivacySet(v) + return _u +} + +// SetNillableRequirePrivacySet sets the "require_privacy_set" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableRequirePrivacySet(v *bool) *GroupUpdate { + if v != nil { + _u.SetRequirePrivacySet(*v) + } + return _u +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (_u *GroupUpdate) SetDefaultMappedModel(v string) *GroupUpdate { _u.mutation.SetDefaultMappedModel(v) @@ -1146,6 +1174,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.AllowMessagesDispatch(); ok { _spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value) } + if value, ok := _u.mutation.RequireOauthOnly(); ok { + _spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value) + } + if value, ok := _u.mutation.RequirePrivacySet(); ok { + _spec.SetField(group.FieldRequirePrivacySet, field.TypeBool, value) + } if value, ok := _u.mutation.DefaultMappedModel(); ok { _spec.SetField(group.FieldDefaultMappedModel, field.TypeString, value) } @@ -2067,6 +2101,34 @@ func (_u *GroupUpdateOne) SetNillableAllowMessagesDispatch(v *bool) *GroupUpdate return _u } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (_u *GroupUpdateOne) SetRequireOauthOnly(v bool) *GroupUpdateOne { + _u.mutation.SetRequireOauthOnly(v) + return _u +} + +// SetNillableRequireOauthOnly sets the "require_oauth_only" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableRequireOauthOnly(v *bool) *GroupUpdateOne { + if v != nil { + _u.SetRequireOauthOnly(*v) + } + return _u +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (_u *GroupUpdateOne) SetRequirePrivacySet(v bool) *GroupUpdateOne { + _u.mutation.SetRequirePrivacySet(v) + return _u +} + +// SetNillableRequirePrivacySet sets the "require_privacy_set" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableRequirePrivacySet(v *bool) *GroupUpdateOne { + if v != nil { + _u.SetRequirePrivacySet(*v) + } + return _u +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (_u *GroupUpdateOne) SetDefaultMappedModel(v string) *GroupUpdateOne { _u.mutation.SetDefaultMappedModel(v) @@ -2604,6 +2666,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if value, ok := _u.mutation.AllowMessagesDispatch(); ok { _spec.SetField(group.FieldAllowMessagesDispatch, field.TypeBool, value) } + if value, ok := _u.mutation.RequireOauthOnly(); ok { + _spec.SetField(group.FieldRequireOauthOnly, field.TypeBool, value) + } + if value, ok := _u.mutation.RequirePrivacySet(); ok { + _spec.SetField(group.FieldRequirePrivacySet, field.TypeBool, value) + } if value, ok := _u.mutation.DefaultMappedModel(); ok { _spec.SetField(group.FieldDefaultMappedModel, field.TypeString, value) } diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index c472d7e0..6c56f2d0 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -409,6 +409,8 @@ var ( {Name: "supported_model_scopes", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "sort_order", Type: field.TypeInt, Default: 0}, {Name: "allow_messages_dispatch", Type: field.TypeBool, Default: false}, + {Name: "require_oauth_only", Type: field.TypeBool, Default: false}, + {Name: "require_privacy_set", Type: field.TypeBool, Default: false}, {Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""}, } // GroupsTable holds the schema information for the "groups" table. diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 42c63c2e..a862209d 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -8253,6 +8253,8 @@ type GroupMutation struct { sort_order *int addsort_order *int allow_messages_dispatch *bool + require_oauth_only *bool + require_privacy_set *bool default_mapped_model *string clearedFields map[string]struct{} api_keys map[int64]struct{} @@ -10034,6 +10036,78 @@ func (m *GroupMutation) ResetAllowMessagesDispatch() { m.allow_messages_dispatch = nil } +// SetRequireOauthOnly sets the "require_oauth_only" field. +func (m *GroupMutation) SetRequireOauthOnly(b bool) { + m.require_oauth_only = &b +} + +// RequireOauthOnly returns the value of the "require_oauth_only" field in the mutation. +func (m *GroupMutation) RequireOauthOnly() (r bool, exists bool) { + v := m.require_oauth_only + if v == nil { + return + } + return *v, true +} + +// OldRequireOauthOnly returns the old "require_oauth_only" 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) OldRequireOauthOnly(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRequireOauthOnly is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRequireOauthOnly requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRequireOauthOnly: %w", err) + } + return oldValue.RequireOauthOnly, nil +} + +// ResetRequireOauthOnly resets all changes to the "require_oauth_only" field. +func (m *GroupMutation) ResetRequireOauthOnly() { + m.require_oauth_only = nil +} + +// SetRequirePrivacySet sets the "require_privacy_set" field. +func (m *GroupMutation) SetRequirePrivacySet(b bool) { + m.require_privacy_set = &b +} + +// RequirePrivacySet returns the value of the "require_privacy_set" field in the mutation. +func (m *GroupMutation) RequirePrivacySet() (r bool, exists bool) { + v := m.require_privacy_set + if v == nil { + return + } + return *v, true +} + +// OldRequirePrivacySet returns the old "require_privacy_set" 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) OldRequirePrivacySet(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldRequirePrivacySet is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldRequirePrivacySet requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldRequirePrivacySet: %w", err) + } + return oldValue.RequirePrivacySet, nil +} + +// ResetRequirePrivacySet resets all changes to the "require_privacy_set" field. +func (m *GroupMutation) ResetRequirePrivacySet() { + m.require_privacy_set = nil +} + // SetDefaultMappedModel sets the "default_mapped_model" field. func (m *GroupMutation) SetDefaultMappedModel(s string) { m.default_mapped_model = &s @@ -10428,7 +10502,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, 32) + fields := make([]string, 0, 34) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -10522,6 +10596,12 @@ func (m *GroupMutation) Fields() []string { if m.allow_messages_dispatch != nil { fields = append(fields, group.FieldAllowMessagesDispatch) } + if m.require_oauth_only != nil { + fields = append(fields, group.FieldRequireOauthOnly) + } + if m.require_privacy_set != nil { + fields = append(fields, group.FieldRequirePrivacySet) + } if m.default_mapped_model != nil { fields = append(fields, group.FieldDefaultMappedModel) } @@ -10595,6 +10675,10 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.SortOrder() case group.FieldAllowMessagesDispatch: return m.AllowMessagesDispatch() + case group.FieldRequireOauthOnly: + return m.RequireOauthOnly() + case group.FieldRequirePrivacySet: + return m.RequirePrivacySet() case group.FieldDefaultMappedModel: return m.DefaultMappedModel() } @@ -10668,6 +10752,10 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldSortOrder(ctx) case group.FieldAllowMessagesDispatch: return m.OldAllowMessagesDispatch(ctx) + case group.FieldRequireOauthOnly: + return m.OldRequireOauthOnly(ctx) + case group.FieldRequirePrivacySet: + return m.OldRequirePrivacySet(ctx) case group.FieldDefaultMappedModel: return m.OldDefaultMappedModel(ctx) } @@ -10896,6 +10984,20 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetAllowMessagesDispatch(v) return nil + case group.FieldRequireOauthOnly: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRequireOauthOnly(v) + return nil + case group.FieldRequirePrivacySet: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetRequirePrivacySet(v) + return nil case group.FieldDefaultMappedModel: v, ok := value.(string) if !ok { @@ -11333,6 +11435,12 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldAllowMessagesDispatch: m.ResetAllowMessagesDispatch() return nil + case group.FieldRequireOauthOnly: + m.ResetRequireOauthOnly() + return nil + case group.FieldRequirePrivacySet: + m.ResetRequirePrivacySet() + return nil case group.FieldDefaultMappedModel: m.ResetDefaultMappedModel() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index ca95f13f..fd6be291 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -458,8 +458,16 @@ func init() { groupDescAllowMessagesDispatch := groupFields[27].Descriptor() // group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field. group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool) + // groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field. + groupDescRequireOauthOnly := groupFields[28].Descriptor() + // group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field. + group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool) + // groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field. + groupDescRequirePrivacySet := groupFields[29].Descriptor() + // group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field. + group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool) // groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field. - groupDescDefaultMappedModel := groupFields[28].Descriptor() + groupDescDefaultMappedModel := groupFields[30].Descriptor() // group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field. group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string) // group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save. diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 0f5a7b14..fd83bf26 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -153,6 +153,12 @@ func (Group) Fields() []ent.Field { field.Bool("allow_messages_dispatch"). Default(false). Comment("是否允许 /v1/messages 调度到此 OpenAI 分组"), + field.Bool("require_oauth_only"). + Default(false). + Comment("仅允许非 apikey 类型账号关联到此分组"), + field.Bool("require_privacy_set"). + Default(false). + Comment("调度时仅允许 privacy 已成功设置的账号"), field.String("default_mapped_model"). MaxLen(100). Default(""). diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 459fd949..caa27bc3 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -112,6 +112,8 @@ type CreateGroupRequest struct { SoraStorageQuotaBytes int64 `json:"sora_storage_quota_bytes"` // OpenAI Messages 调度配置(仅 openai 平台使用) AllowMessagesDispatch bool `json:"allow_messages_dispatch"` + RequireOAuthOnly bool `json:"require_oauth_only"` + RequirePrivacySet bool `json:"require_privacy_set"` DefaultMappedModel string `json:"default_mapped_model"` // 从指定分组复制账号(创建后自动绑定) CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"` @@ -150,6 +152,8 @@ type UpdateGroupRequest struct { SoraStorageQuotaBytes *int64 `json:"sora_storage_quota_bytes"` // OpenAI Messages 调度配置(仅 openai 平台使用) AllowMessagesDispatch *bool `json:"allow_messages_dispatch"` + RequireOAuthOnly *bool `json:"require_oauth_only"` + RequirePrivacySet *bool `json:"require_privacy_set"` DefaultMappedModel *string `json:"default_mapped_model"` // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号) CopyAccountsFromGroupIDs []int64 `json:"copy_accounts_from_group_ids"` @@ -267,6 +271,8 @@ func (h *GroupHandler) Create(c *gin.Context) { SupportedModelScopes: req.SupportedModelScopes, SoraStorageQuotaBytes: req.SoraStorageQuotaBytes, AllowMessagesDispatch: req.AllowMessagesDispatch, + RequireOAuthOnly: req.RequireOAuthOnly, + RequirePrivacySet: req.RequirePrivacySet, DefaultMappedModel: req.DefaultMappedModel, CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs, }) @@ -320,6 +326,8 @@ func (h *GroupHandler) Update(c *gin.Context) { SupportedModelScopes: req.SupportedModelScopes, SoraStorageQuotaBytes: req.SoraStorageQuotaBytes, AllowMessagesDispatch: req.AllowMessagesDispatch, + RequireOAuthOnly: req.RequireOAuthOnly, + RequirePrivacySet: req.RequirePrivacySet, DefaultMappedModel: req.DefaultMappedModel, CopyAccountsFromGroupIDs: req.CopyAccountsFromGroupIDs, }) diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 0b5448af..a8da92c0 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -181,6 +181,8 @@ func groupFromServiceBase(g *service.Group) Group { FallbackGroupIDOnInvalidRequest: g.FallbackGroupIDOnInvalidRequest, SoraStorageQuotaBytes: g.SoraStorageQuotaBytes, AllowMessagesDispatch: g.AllowMessagesDispatch, + RequireOAuthOnly: g.RequireOAuthOnly, + RequirePrivacySet: g.RequirePrivacySet, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, } diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 8af6990e..46984044 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -102,6 +102,10 @@ type Group struct { // OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程) AllowMessagesDispatch bool `json:"allow_messages_dispatch"` + // 账号过滤控制(仅 OpenAI/Antigravity 平台有效) + RequireOAuthOnly bool `json:"require_oauth_only"` + RequirePrivacySet bool `json:"require_privacy_set"` + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 667193a6..ade0d464 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -662,6 +662,8 @@ func groupEntityToService(g *dbent.Group) *service.Group { SupportedModelScopes: g.SupportedModelScopes, SortOrder: g.SortOrder, AllowMessagesDispatch: g.AllowMessagesDispatch, + RequireOAuthOnly: g.RequireOauthOnly, + RequirePrivacySet: g.RequirePrivacySet, DefaultMappedModel: g.DefaultMappedModel, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 674c655b..3cfd649b 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -61,6 +61,8 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er SetMcpXMLInject(groupIn.MCPXMLInject). SetSoraStorageQuotaBytes(groupIn.SoraStorageQuotaBytes). SetAllowMessagesDispatch(groupIn.AllowMessagesDispatch). + SetRequireOauthOnly(groupIn.RequireOAuthOnly). + SetRequirePrivacySet(groupIn.RequirePrivacySet). SetDefaultMappedModel(groupIn.DefaultMappedModel) // 设置模型路由配置 @@ -130,6 +132,8 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er SetMcpXMLInject(groupIn.MCPXMLInject). SetSoraStorageQuotaBytes(groupIn.SoraStorageQuotaBytes). SetAllowMessagesDispatch(groupIn.AllowMessagesDispatch). + SetRequireOauthOnly(groupIn.RequireOAuthOnly). + SetRequirePrivacySet(groupIn.RequirePrivacySet). SetDefaultMappedModel(groupIn.DefaultMappedModel) // 显式处理可空字段:nil 需要 clear,非 nil 需要 set。 diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index ac4e05de..450c3122 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -214,6 +214,8 @@ func TestAPIContracts(t *testing.T) { "fallback_group_id": null, "fallback_group_id_on_invalid_request": null, "allow_messages_dispatch": false, + "require_oauth_only": false, + "require_privacy_set": false, "created_at": "2025-01-02T03:04:05Z", "updated_at": "2025-01-02T03:04:05Z" } diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index a1449ffd..53eefda3 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -141,6 +141,21 @@ func (a *Account) IsOAuth() bool { return a.Type == AccountTypeOAuth || a.Type == AccountTypeSetupToken } +// IsPrivacySet 检查账号的 privacy 是否已成功设置。 +// OpenAI: privacy_mode == "training_off" +// Antigravity: privacy_mode == "privacy_set" +// 其他平台: 无 privacy 概念,始终返回 true +func (a *Account) IsPrivacySet() bool { + switch a.Platform { + case PlatformOpenAI: + return a.getExtraString("privacy_mode") == PrivacyModeTrainingOff + case PlatformAntigravity: + return a.getExtraString("privacy_mode") == AntigravityPrivacySet + default: + return true + } +} + func (a *Account) IsGemini() bool { return a.Platform == PlatformGemini } diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go index 71d51712..328790a8 100644 --- a/backend/internal/service/account_service.go +++ b/backend/internal/service/account_service.go @@ -174,6 +174,19 @@ func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) ( return nil, fmt.Errorf("create account: %w", err) } + // require_oauth_only 检查:apikey 类型账号不可加入限制分组 + if account.Type == AccountTypeAPIKey && len(req.GroupIDs) > 0 { + for _, gid := range req.GroupIDs { + g, err := s.groupRepo.GetByID(ctx, gid) + if err != nil { + return nil, err + } + if g.RequireOAuthOnly && (g.Platform == PlatformOpenAI || g.Platform == PlatformAntigravity || g.Platform == PlatformAnthropic || g.Platform == PlatformGemini) { + return nil, fmt.Errorf("分组 [%s] 仅允许 OAuth 账号,apikey 类型账号无法加入", g.Name) + } + } + } + // 绑定分组 if len(req.GroupIDs) > 0 { if err := s.accountRepo.BindGroups(ctx, account.ID, req.GroupIDs); err != nil { @@ -277,6 +290,19 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount return nil, fmt.Errorf("update account: %w", err) } + // require_oauth_only 检查 + if account.Type == AccountTypeAPIKey && req.GroupIDs != nil { + for _, gid := range *req.GroupIDs { + g, err := s.groupRepo.GetByID(ctx, gid) + if err != nil { + return nil, err + } + if g.RequireOAuthOnly && (g.Platform == PlatformOpenAI || g.Platform == PlatformAntigravity || g.Platform == PlatformAnthropic || g.Platform == PlatformGemini) { + return nil, fmt.Errorf("分组 [%s] 仅允许 OAuth 账号,apikey 类型账号无法加入", g.Name) + } + } + } + // 绑定分组 if req.GroupIDs != nil { if err := s.accountRepo.BindGroups(ctx, account.ID, *req.GroupIDs); err != nil { diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 88c064f3..52c9837c 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -162,6 +162,8 @@ type CreateGroupInput struct { // OpenAI Messages 调度配置(仅 openai 平台使用) AllowMessagesDispatch bool DefaultMappedModel string + RequireOAuthOnly bool + RequirePrivacySet bool // 从指定分组复制账号(创建分组后在同一事务内绑定) CopyAccountsFromGroupIDs []int64 } @@ -201,6 +203,8 @@ type UpdateGroupInput struct { // OpenAI Messages 调度配置(仅 openai 平台使用) AllowMessagesDispatch *bool DefaultMappedModel *string + RequireOAuthOnly *bool + RequirePrivacySet *bool // 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号) CopyAccountsFromGroupIDs []int64 } @@ -941,12 +945,35 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn SupportedModelScopes: input.SupportedModelScopes, SoraStorageQuotaBytes: input.SoraStorageQuotaBytes, AllowMessagesDispatch: input.AllowMessagesDispatch, + RequireOAuthOnly: input.RequireOAuthOnly, + RequirePrivacySet: input.RequirePrivacySet, DefaultMappedModel: input.DefaultMappedModel, } if err := s.groupRepo.Create(ctx, group); err != nil { return nil, err } + // require_oauth_only: 过滤掉 apikey 类型账号 + if group.RequireOAuthOnly && (group.Platform == PlatformOpenAI || group.Platform == PlatformAntigravity || group.Platform == PlatformAnthropic || group.Platform == PlatformGemini) && len(accountIDsToCopy) > 0 { + accounts, err := s.accountRepo.GetByIDs(ctx, accountIDsToCopy) + if err != nil { + return nil, fmt.Errorf("failed to fetch accounts for oauth filter: %w", err) + } + oauthIDs := make(map[int64]struct{}, len(accounts)) + for _, acc := range accounts { + if acc.Type != AccountTypeAPIKey { + oauthIDs[acc.ID] = struct{}{} + } + } + var filtered []int64 + for _, aid := range accountIDsToCopy { + if _, ok := oauthIDs[aid]; ok { + filtered = append(filtered, aid) + } + } + accountIDsToCopy = filtered + } + // 如果有需要复制的账号,绑定到新分组 if len(accountIDsToCopy) > 0 { if err := s.groupRepo.BindAccountsToGroup(ctx, group.ID, accountIDsToCopy); err != nil { @@ -1154,6 +1181,12 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if input.AllowMessagesDispatch != nil { group.AllowMessagesDispatch = *input.AllowMessagesDispatch } + if input.RequireOAuthOnly != nil { + group.RequireOAuthOnly = *input.RequireOAuthOnly + } + if input.RequirePrivacySet != nil { + group.RequirePrivacySet = *input.RequirePrivacySet + } if input.DefaultMappedModel != nil { group.DefaultMappedModel = *input.DefaultMappedModel } @@ -1201,6 +1234,27 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd return nil, fmt.Errorf("failed to clear existing account bindings: %w", err) } + // require_oauth_only: 过滤掉 apikey 类型账号 + if group.RequireOAuthOnly && (group.Platform == PlatformOpenAI || group.Platform == PlatformAntigravity || group.Platform == PlatformAnthropic || group.Platform == PlatformGemini) && len(accountIDsToCopy) > 0 { + accounts, err := s.accountRepo.GetByIDs(ctx, accountIDsToCopy) + if err != nil { + return nil, fmt.Errorf("failed to fetch accounts for oauth filter: %w", err) + } + oauthIDs := make(map[int64]struct{}, len(accounts)) + for _, acc := range accounts { + if acc.Type != AccountTypeAPIKey { + oauthIDs[acc.ID] = struct{}{} + } + } + var filtered []int64 + for _, aid := range accountIDsToCopy { + if _, ok := oauthIDs[aid]; ok { + filtered = append(filtered, aid) + } + } + accountIDsToCopy = filtered + } + // 再绑定源分组的账号 if len(accountIDsToCopy) > 0 { if err := s.groupRepo.BindAccountsToGroup(ctx, id, accountIDsToCopy); err != nil { diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index f28912bb..2d16ad94 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -3139,7 +3139,7 @@ func TestGatewayService_GroupResolution_ReusesContextGroup(t *testing.T) { account, err := svc.SelectAccountForModelWithExclusions(ctx, &groupID, "", "claude-3-5-sonnet-20241022", nil) require.NoError(t, err) require.NotNil(t, account) - require.Equal(t, 0, groupRepo.getByIDCalls) + require.Equal(t, 1, groupRepo.getByIDCalls) // +1 for require_privacy_set check require.Equal(t, 0, groupRepo.getByIDLiteCalls) } @@ -3182,7 +3182,7 @@ func TestGatewayService_GroupResolution_IgnoresInvalidContextGroup(t *testing.T) account, err := svc.SelectAccountForModelWithExclusions(ctx, &groupID, "", "claude-3-5-sonnet-20241022", nil) require.NoError(t, err) require.NotNil(t, account) - require.Equal(t, 0, groupRepo.getByIDCalls) + require.Equal(t, 1, groupRepo.getByIDCalls) // +1 for require_privacy_set check require.Equal(t, 1, groupRepo.getByIDLiteCalls) } @@ -3252,7 +3252,7 @@ func TestGatewayService_GroupResolution_FallbackUsesLiteOnce(t *testing.T) { account, err := svc.SelectAccountForModelWithExclusions(ctx, &groupID, "", "claude-3-5-sonnet-20241022", nil) require.NoError(t, err) require.NotNil(t, account) - require.Equal(t, 0, groupRepo.getByIDCalls) + require.Equal(t, 1, groupRepo.getByIDCalls) // +1 for require_privacy_set check require.Equal(t, 1, groupRepo.getByIDLiteCalls) } diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index b54f463b..7b7b61ac 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -2744,6 +2744,12 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, preferOAuth := platform == PlatformGemini routingAccountIDs := s.routingAccountIDsForRequest(ctx, groupID, requestedModel, platform) + // require_privacy_set: 获取分组信息 + var schedGroup *Group + if groupID != nil && s.groupRepo != nil { + schedGroup, _ = s.groupRepo.GetByID(ctx, *groupID) + } + var accounts []Account accountsLoaded := false @@ -2815,6 +2821,12 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, if !s.isAccountSchedulableForSelection(acc) { continue } + // require_privacy_set: 跳过 privacy 未设置的账号并标记异常 + if schedGroup != nil && schedGroup.RequirePrivacySet && !acc.IsPrivacySet() { + _ = s.accountRepo.SetError(ctx, acc.ID, + fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) + continue + } if requestedModel != "" && !s.isModelSupportedByAccountWithContext(ctx, acc, requestedModel) { continue } @@ -2917,6 +2929,12 @@ func (s *GatewayService) selectAccountForModelWithPlatform(ctx context.Context, if !s.isAccountSchedulableForSelection(acc) { continue } + // require_privacy_set: 跳过 privacy 未设置的账号并标记异常 + if schedGroup != nil && schedGroup.RequirePrivacySet && !acc.IsPrivacySet() { + _ = s.accountRepo.SetError(ctx, acc.ID, + fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) + continue + } if requestedModel != "" && !s.isModelSupportedByAccountWithContext(ctx, acc, requestedModel) { continue } @@ -2980,6 +2998,12 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g preferOAuth := nativePlatform == PlatformGemini routingAccountIDs := s.routingAccountIDsForRequest(ctx, groupID, requestedModel, nativePlatform) + // require_privacy_set: 获取分组信息 + var schedGroup *Group + if groupID != nil && s.groupRepo != nil { + schedGroup, _ = s.groupRepo.GetByID(ctx, *groupID) + } + var accounts []Account accountsLoaded := false @@ -3047,6 +3071,12 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g if !s.isAccountSchedulableForSelection(acc) { continue } + // require_privacy_set: 跳过 privacy 未设置的账号并标记异常 + if schedGroup != nil && schedGroup.RequirePrivacySet && !acc.IsPrivacySet() { + _ = s.accountRepo.SetError(ctx, acc.ID, + fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) + continue + } // 过滤:原生平台直接通过,antigravity 需要启用混合调度 if acc.Platform == PlatformAntigravity && !acc.IsMixedSchedulingEnabled() { continue @@ -3151,6 +3181,12 @@ func (s *GatewayService) selectAccountWithMixedScheduling(ctx context.Context, g if !s.isAccountSchedulableForSelection(acc) { continue } + // require_privacy_set: 跳过 privacy 未设置的账号并标记异常 + if schedGroup != nil && schedGroup.RequirePrivacySet && !acc.IsPrivacySet() { + _ = s.accountRepo.SetError(ctx, acc.ID, + fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) + continue + } // 过滤:原生平台直接通过,antigravity 需要启用混合调度 if acc.Platform == PlatformAntigravity && !acc.IsMixedSchedulingEnabled() { continue diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index e17032e0..e0f81a39 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -59,6 +59,8 @@ type Group struct { // OpenAI Messages 调度配置(仅 openai 平台使用) AllowMessagesDispatch bool + RequireOAuthOnly bool // 仅允许非 apikey 类型账号关联(OpenAI/Antigravity/Anthropic/Gemini) + RequirePrivacySet bool // 调度时仅允许 privacy 已成功设置的账号(OpenAI/Antigravity/Anthropic/Gemini) DefaultMappedModel string CreatedAt time.Time diff --git a/backend/internal/service/openai_account_scheduler.go b/backend/internal/service/openai_account_scheduler.go index 37e7ed2c..6c09e354 100644 --- a/backend/internal/service/openai_account_scheduler.go +++ b/backend/internal/service/openai_account_scheduler.go @@ -4,6 +4,7 @@ import ( "container/heap" "context" "errors" + "fmt" "hash/fnv" "math" "sort" @@ -575,6 +576,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance( return nil, 0, 0, 0, errors.New("no available OpenAI accounts") } + // require_privacy_set: 获取分组信息 + var schedGroup *Group + if req.GroupID != nil && s.service.schedulerSnapshot != nil { + schedGroup, _ = s.service.schedulerSnapshot.GetGroupByID(ctx, *req.GroupID) + } + filtered := make([]*Account, 0, len(accounts)) loadReq := make([]AccountWithConcurrency, 0, len(accounts)) for i := range accounts { @@ -587,6 +594,12 @@ func (s *defaultOpenAIAccountScheduler) selectByLoadBalance( if !account.IsSchedulable() || !account.IsOpenAI() { continue } + // require_privacy_set: 跳过 privacy 未设置的账号并标记异常 + if schedGroup != nil && schedGroup.RequirePrivacySet && !account.IsPrivacySet() { + _ = s.service.accountRepo.SetError(ctx, account.ID, + fmt.Sprintf("Privacy not set, required by group [%s]", schedGroup.Name)) + continue + } if req.RequestedModel != "" && !account.IsModelSupported(req.RequestedModel) { continue } diff --git a/backend/internal/service/scheduler_snapshot_service.go b/backend/internal/service/scheduler_snapshot_service.go index 4c9540f1..d1330abb 100644 --- a/backend/internal/service/scheduler_snapshot_service.go +++ b/backend/internal/service/scheduler_snapshot_service.go @@ -152,6 +152,14 @@ func (s *SchedulerSnapshotService) GetAccount(ctx context.Context, accountID int return s.accountRepo.GetByID(fallbackCtx, accountID) } +// GetGroupByID 获取分组信息(供调度器使用) +func (s *SchedulerSnapshotService) GetGroupByID(ctx context.Context, groupID int64) (*Group, error) { + if s.groupRepo == nil { + return nil, nil + } + return s.groupRepo.GetByID(ctx, groupID) +} + // UpdateAccountInCache 立即更新 Redis 中单个账号的数据(用于模型限流后立即生效) func (s *SchedulerSnapshotService) UpdateAccountInCache(ctx context.Context, account *Account) error { if s.cache == nil || account == nil { diff --git a/backend/migrations/081_add_group_account_filter.sql b/backend/migrations/081_add_group_account_filter.sql new file mode 100644 index 00000000..0afb21d9 --- /dev/null +++ b/backend/migrations/081_add_group_account_filter.sql @@ -0,0 +1,2 @@ +ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_oauth_only BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS require_privacy_set BOOLEAN NOT NULL DEFAULT false; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index f9425ad0..54ef1a10 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -399,6 +399,8 @@ export interface Group { fallback_group_id_on_invalid_request: number | null // OpenAI Messages 调度开关(用户侧需要此字段判断是否展示 Claude Code 教程) allow_messages_dispatch?: boolean + require_oauth_only: boolean + require_privacy_set: boolean created_at: string updated_at: string } @@ -510,6 +512,8 @@ export interface CreateGroupRequest { mcp_xml_inject?: boolean simulate_claude_max_enabled?: boolean supported_model_scopes?: string[] + require_oauth_only?: boolean + require_privacy_set?: boolean // 从指定分组复制账号 copy_accounts_from_group_ids?: number[] } @@ -539,6 +543,8 @@ export interface UpdateGroupRequest { mcp_xml_inject?: boolean simulate_claude_max_enabled?: boolean supported_model_scopes?: string[] + require_oauth_only?: boolean + require_privacy_set?: boolean copy_accounts_from_group_ids?: number[] } diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index a7c1a10d..c7aaf683 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -792,6 +792,61 @@ + +
+

账号过滤控制

+ + +
+
+ +

+ {{ createForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }} +

+
+ +
+ + +
+
+ +

+ {{ createForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }} +

+
+ +
+
+
+ +
+

账号过滤控制

+ + +
+
+ +

+ {{ editForm.require_oauth_only ? '已启用 — 排除 API Key 类型账号' : '未启用' }} +

+
+ +
+ + +
+
+ +

+ {{ editForm.require_privacy_set ? '已启用 — Privacy 未设置的账号将被排除' : '未启用' }} +

+
+ +
+
+
{ createForm.fallback_group_id = null createForm.fallback_group_id_on_invalid_request = null createForm.allow_messages_dispatch = false + createForm.require_oauth_only = false + createForm.require_privacy_set = false createForm.default_mapped_model = 'gpt-5.4' createForm.supported_model_scopes = ['claude', 'gemini_text', 'gemini_image'] createForm.mcp_xml_inject = true @@ -2539,6 +2657,8 @@ 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.allow_messages_dispatch = group.allow_messages_dispatch || false + editForm.require_oauth_only = group.require_oauth_only ?? false + editForm.require_privacy_set = group.require_privacy_set ?? false editForm.default_mapped_model = group.default_mapped_model || '' editForm.model_routing_enabled = group.model_routing_enabled || false editForm.supported_model_scopes = group.supported_model_scopes || ['claude', 'gemini_text', 'gemini_image'] @@ -2647,6 +2767,10 @@ watch( createForm.allow_messages_dispatch = false createForm.default_mapped_model = '' } + if (!['openai', 'antigravity', 'anthropic', 'gemini'].includes(newVal)) { + createForm.require_oauth_only = false + createForm.require_privacy_set = false + } } )