Commit e2ec1d30 authored by QTom's avatar QTom Committed by 陈曦
Browse files

feat(group-filter): 分组账号过滤控制 — require_oauth_only + require_privacy_set



为 OpenAI/Antigravity/Anthropic/Gemini 分组新增两个布尔控制字段:
- require_oauth_only: 创建/更新账号绑定分组时拒绝 apikey 类型加入
- require_privacy_set: 调度选号时跳过 privacy 未成功设置的账号并标记 error

后端:Ent schema 新增字段 + 迁移、Group CRUD 全链路透传、
      gateway_service 与 openai_account_scheduler 两套调度路径过滤
前端:创建/编辑表单 toggle 开关(OpenAI/Antigravity/Anthropic/Gemini 平台可见)
Co-Authored-By: default avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent dfbdd8ab
......@@ -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(')')
......
......@@ -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()
......
......@@ -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))
......
......@@ -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) {
......
......@@ -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)
}
......
......@@ -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.
......
......@@ -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
......
......@@ -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.
......
......@@ -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("").
......
......@@ -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,
})
......
......@@ -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,
}
......
......@@ -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"`
}
......
......@@ -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,
......
......@@ -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。
......
......@@ -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"
}
......
......@@ -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
}
......
......@@ -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 {
......
......@@ -163,6 +163,8 @@ type CreateGroupInput struct {
// OpenAI Messages 调度配置(仅 openai 平台使用)
AllowMessagesDispatch bool
DefaultMappedModel string
RequireOAuthOnly bool
RequirePrivacySet bool
// 从指定分组复制账号(创建分组后在同一事务内绑定)
CopyAccountsFromGroupIDs []int64
}
......@@ -202,6 +204,8 @@ type UpdateGroupInput struct {
// OpenAI Messages 调度配置(仅 openai 平台使用)
AllowMessagesDispatch *bool
DefaultMappedModel *string
RequireOAuthOnly *bool
RequirePrivacySet *bool
// 从指定分组复制账号(同步操作:先清空当前分组的账号绑定,再绑定源分组的账号)
CopyAccountsFromGroupIDs []int64
}
......@@ -942,12 +946,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 {
......@@ -1155,6 +1182,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
}
......@@ -1202,6 +1235,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 {
......
......@@ -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)
}
......
......@@ -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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment