fix(model):成员只能看见自己能调用的模型
This commit is contained in:
1 parent
019f2368f8
commit
7c281743ff
10 files changed
+217
-59
No files matched your search
@@ -317,7 +317,7 @@ func HandleGeminiGenerateContent(r *ghttp.Request) {
|
||||
func HandleModels(r *ghttp.Request) {
|
||||
rc := buildRelayContext(r) // 获取 TenantID
|
||||
|
||||
resp, err := handler.HandleModels(r.Context(), rc.TenantID, rc.ApiKeyID, dataProvider)
|
||||
resp, err := handler.HandleModels(r.Context(), rc.TenantID, rc.ApiKeyID, rc.UserID, dataProvider)
|
||||
if err != nil {
|
||||
r.Response.WriteJsonExit(g.Map{
|
||||
"error": g.Map{"type": "internal_error", "message": err.Error()},
|
||||
@@ -371,7 +371,7 @@ func HandleModelDetail(r *ghttp.Request) {
|
||||
func HandleGeminiModels(r *ghttp.Request) {
|
||||
rc := buildRelayContext(r)
|
||||
|
||||
resp, err := handler.HandleGeminiModels(r.Context(), rc.TenantID, rc.ApiKeyID, dataProvider)
|
||||
resp, err := handler.HandleGeminiModels(r.Context(), rc.TenantID, rc.ApiKeyID, rc.UserID, dataProvider)
|
||||
if err != nil {
|
||||
handler.WriteGeminiRelayError(r.Response.Writer, err)
|
||||
return
|
||||
|
||||
@@ -57,6 +57,10 @@ func HandleTaskSubmit(r *ghttp.Request) {
|
||||
statusCode = 403
|
||||
errType = "permission_denied"
|
||||
errMsg = "当前租户未启用该模型,请联系管理员"
|
||||
} else if err == relay_common.ErrMemberModelNotAllowed {
|
||||
statusCode = 403
|
||||
errType = "permission_denied"
|
||||
errMsg = "当前成员无权使用该模型"
|
||||
} else if err == relay_common.ErrChannelUnavailable {
|
||||
statusCode = 503
|
||||
errType = "server_error"
|
||||
@@ -172,6 +176,13 @@ func selectTaskChannel(r *ghttp.Request, body []byte) (*relay_common.ChannelMeta
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查成员模型范围
|
||||
if allowed, err := relayDataProvider.CheckMemberModelAccess(ctx, tenantID, userID, req.Model); err != nil {
|
||||
return nil, err
|
||||
} else if !allowed {
|
||||
return nil, relay_common.ErrMemberModelNotAllowed
|
||||
}
|
||||
|
||||
return &relay_common.ChannelMeta{
|
||||
ChannelID: selection.ChannelID,
|
||||
ChannelType: selection.ChannelType,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
@@ -20,6 +21,9 @@ import (
|
||||
|
||||
type sOpen struct{}
|
||||
|
||||
// openMemberModelCache 成员模型范围缓存(与 relay provider 中的缓存 key 相同)
|
||||
var openMemberModelCache = common.NewCache("member_model", 60*time.Second)
|
||||
|
||||
func New() *sOpen {
|
||||
return &sOpen{}
|
||||
}
|
||||
@@ -283,20 +287,36 @@ func (s *sOpen) OpenMemberModelsUpdate(ctx context.Context, req *v1.OpenMemberMo
|
||||
}
|
||||
|
||||
// Insert new scopes
|
||||
for _, modelID := range req.ModelIDs {
|
||||
if modelID == 0 {
|
||||
continue
|
||||
if len(req.ModelIDs) > 0 {
|
||||
for _, modelID := range req.ModelIDs {
|
||||
if modelID == 0 {
|
||||
continue
|
||||
}
|
||||
_, err = dao.TntMemberModelScopes.Ctx(ctx).Data(do.TntMemberModelScopes{
|
||||
TenantId: tenantID,
|
||||
UserId: req.Id,
|
||||
ModelId: modelID,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 空列表表示禁止所有模型,插入哨兵记录(model_id = -1)
|
||||
_, err = dao.TntMemberModelScopes.Ctx(ctx).Data(do.TntMemberModelScopes{
|
||||
TenantId: tenantID,
|
||||
UserId: req.Id,
|
||||
ModelId: modelID,
|
||||
ModelId: -1,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 清除该成员的模型范围缓存,使下次请求时重新从数据库读取
|
||||
cacheKey := fmt.Sprintf("%d:%d", tenantID, req.Id)
|
||||
openMemberModelCache.Delete(ctx, cacheKey)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -716,7 +716,7 @@ func (p *DataProviderImpl) ResetConsecutiveFailure(ctx context.Context, channelI
|
||||
// GetAvailableModels 实现 DataProvider.GetAvailableModels
|
||||
// 如果 tenantID > 0,返回该租户有权使用的模型列表
|
||||
// 如果 apiKeyID > 0,进一步按 API Key 的模型范围过滤
|
||||
func (p *DataProviderImpl) GetAvailableModels(ctx context.Context, tenantID int64, apiKeyID int64) ([]common.ModelInfo, error) {
|
||||
func (p *DataProviderImpl) GetAvailableModels(ctx context.Context, tenantID, apiKeyID, userID int64) ([]common.ModelInfo, error) {
|
||||
type modelRow struct {
|
||||
ModelId string `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
@@ -821,6 +821,31 @@ func (p *DataProviderImpl) GetAvailableModels(ctx context.Context, tenantID int6
|
||||
}
|
||||
}
|
||||
|
||||
// 按成员模型范围过滤
|
||||
if userID > 0 {
|
||||
allowedNames, err := GetMemberAllowedModelNames(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if allowedNames != nil {
|
||||
if len(allowedNames) == 0 {
|
||||
models = nil
|
||||
} else {
|
||||
allowed := make(map[string]bool, len(allowedNames))
|
||||
for _, n := range allowedNames {
|
||||
allowed[n] = true
|
||||
}
|
||||
filtered := make([]modelRow, 0, len(models))
|
||||
for _, m := range models {
|
||||
if allowed[m.ModelId] {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
models = filtered
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]common.ModelInfo, 0, len(models))
|
||||
for _, m := range models {
|
||||
result = append(result, common.ModelInfo{
|
||||
@@ -1100,8 +1125,57 @@ func (p *DataProviderImpl) InvalidateModelCache(modelName string) {
|
||||
// memberModelCache 成员模型范围缓存(TTL 60s)
|
||||
var memberModelCache = lcommon.NewCache("member_model", 60*time.Second)
|
||||
|
||||
// GetMemberAllowedModelNames 获取成员允许使用的 model_id 列表。
|
||||
// 返回 nil 表示不限制(向后兼容),返回空切片表示禁止所有模型(哨兵),返回非空切片表示允许的模型集合。
|
||||
func GetMemberAllowedModelNames(ctx context.Context, tenantID, userID int64) ([]string, error) {
|
||||
if userID <= 0 || tenantID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%d:%d", tenantID, userID)
|
||||
|
||||
var cachedNames []string
|
||||
if memberModelCache.GetJSON(ctx, cacheKey, &cachedNames) {
|
||||
return cachedNames, nil
|
||||
}
|
||||
|
||||
type scopeRow struct {
|
||||
ModelID int64 `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
}
|
||||
var rows []scopeRow
|
||||
err := g.DB().Model("tnt_member_model_scopes ms").Ctx(ctx).
|
||||
LeftJoin("mdl_models m ON ms.model_id = m.id").
|
||||
Where("ms.tenant_id", tenantID).
|
||||
Where("ms.user_id", userID).
|
||||
Fields("ms.model_id, m.model_id as model_name").
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(rows) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
if r.ModelID == -1 {
|
||||
memberModelCache.Set(ctx, cacheKey, []string{})
|
||||
return []string{}, nil
|
||||
}
|
||||
if r.ModelName != "" {
|
||||
names = append(names, r.ModelName)
|
||||
}
|
||||
}
|
||||
|
||||
memberModelCache.Set(ctx, cacheKey, names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// CheckMemberModelAccess 检查成员是否有权使用指定模型。
|
||||
// 无 scope 记录表示不限制(向后兼容)。
|
||||
// 哨兵 model_id=-1 表示该成员被限制使用所有模型。
|
||||
func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID, userID int64, modelName string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("%d:%d", tenantID, userID)
|
||||
|
||||
@@ -1110,6 +1184,12 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
if len(cachedModelNames) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
// 哨兵值:表示该成员被限制使用所有模型
|
||||
for _, name := range cachedModelNames {
|
||||
if name == "__none__" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
for _, name := range cachedModelNames {
|
||||
if name == modelName {
|
||||
return true, nil
|
||||
@@ -1119,6 +1199,7 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
}
|
||||
|
||||
type scopeRow struct {
|
||||
ModelID int64 `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
}
|
||||
var rows []scopeRow
|
||||
@@ -1126,7 +1207,7 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
LeftJoin("mdl_models m ON ms.model_id = m.id").
|
||||
Where("ms.tenant_id", tenantID).
|
||||
Where("ms.user_id", userID).
|
||||
Fields("m.model_id as model_name").
|
||||
Fields("ms.model_id, m.model_id as model_name").
|
||||
Scan(&rows)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -1134,7 +1215,11 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
|
||||
names := make([]string, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
names = append(names, r.ModelName)
|
||||
if r.ModelID == -1 {
|
||||
names = append(names, "__none__")
|
||||
} else if r.ModelName != "" {
|
||||
names = append(names, r.ModelName)
|
||||
}
|
||||
}
|
||||
|
||||
memberModelCache.Set(ctx, cacheKey, names)
|
||||
@@ -1142,6 +1227,12 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
if len(names) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
// 哨兵值:表示该成员被限制使用所有模型
|
||||
for _, name := range names {
|
||||
if name == "__none__" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
if name == modelName {
|
||||
return true, nil
|
||||
@@ -1150,6 +1241,12 @@ func (p *DataProviderImpl) CheckMemberModelAccess(ctx context.Context, tenantID,
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// InvalidateMemberModelCache 实现 DataProvider.InvalidateMemberModelCache
|
||||
func (p *DataProviderImpl) InvalidateMemberModelCache(ctx context.Context, tenantID, userID int64) {
|
||||
cacheKey := fmt.Sprintf("%d:%d", tenantID, userID)
|
||||
memberModelCache.Delete(ctx, cacheKey)
|
||||
}
|
||||
|
||||
// apiKeyModelCache API Key 模型范围缓存(TTL 60s)
|
||||
var apiKeyModelCache = lcommon.NewCache("apikey_model", 60*time.Second)
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@ package tenant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/qianfree/team-api/internal/dao"
|
||||
do "github.com/qianfree/team-api/internal/model/do"
|
||||
|
||||
v1 "github.com/qianfree/team-api/api/tenant/v1"
|
||||
lcommon "github.com/qianfree/team-api/internal/logic/common"
|
||||
"github.com/qianfree/team-api/internal/middleware"
|
||||
)
|
||||
|
||||
@@ -29,11 +32,16 @@ func (s *sTenant) MemberModelScopes(ctx context.Context, req *v1.TenantMemberMod
|
||||
|
||||
ids := make([]int64, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
ids = append(ids, r.ModelID)
|
||||
if r.ModelID > 0 {
|
||||
ids = append(ids, r.ModelID)
|
||||
}
|
||||
}
|
||||
return &v1.TenantMemberModelScopesRes{ModelIDs: ids}, nil
|
||||
}
|
||||
|
||||
// memberModelScopeCache 成员模型范围缓存(与 relay provider 中的缓存 key 相同)
|
||||
var memberModelScopeCache = lcommon.NewCache("member_model", 60*time.Second)
|
||||
|
||||
// MemberModelScopesSet sets the available models for a member (full replace).
|
||||
func (s *sTenant) MemberModelScopesSet(ctx context.Context, req *v1.TenantMemberModelScopesSetReq) (*v1.TenantMemberModelScopesSetRes, error) {
|
||||
tenantID := middleware.GetTenantID(ctx)
|
||||
@@ -48,8 +56,8 @@ func (s *sTenant) MemberModelScopesSet(ctx context.Context, req *v1.TenantMember
|
||||
return err
|
||||
}
|
||||
|
||||
// Insert new scopes
|
||||
if len(req.ModelIDs) > 0 {
|
||||
// Insert new scopes
|
||||
data := make([]do.TntMemberModelScopes, len(req.ModelIDs))
|
||||
for i, mID := range req.ModelIDs {
|
||||
data[i] = do.TntMemberModelScopes{
|
||||
@@ -62,6 +70,16 @@ func (s *sTenant) MemberModelScopesSet(ctx context.Context, req *v1.TenantMember
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 空列表表示禁止所有模型,插入哨兵记录(model_id = -1)
|
||||
_, err = tx.Model("tnt_member_model_scopes").Ctx(ctx).Data(do.TntMemberModelScopes{
|
||||
TenantId: tenantID,
|
||||
UserId: req.Id,
|
||||
ModelId: -1,
|
||||
}).Insert()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -70,47 +88,9 @@ func (s *sTenant) MemberModelScopesSet(ctx context.Context, req *v1.TenantMember
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清除该成员的模型范围缓存,使下次请求时重新从数据库读取
|
||||
cacheKey := fmt.Sprintf("%d:%d", tenantID, req.Id)
|
||||
memberModelScopeCache.Delete(ctx, cacheKey)
|
||||
|
||||
return &v1.TenantMemberModelScopesSetRes{}, nil
|
||||
}
|
||||
|
||||
// revokeMemberModels removes specified models from a member and cascades to their API keys.
|
||||
func revokeMemberModels(ctx context.Context, tenantID, userID int64, modelIDs []int64) error {
|
||||
if len(modelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
// Remove from member model scopes
|
||||
_, err := tx.Model("tnt_member_model_scopes").Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("user_id", userID).
|
||||
WhereIn("model_id", modelIDs).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cascade: remove model bindings from all keys owned by this user
|
||||
keyIDs, err := tx.Model("api_keys").Ctx(ctx).
|
||||
Where("tenant_id", tenantID).
|
||||
Where("user_id", userID).
|
||||
Where("status", "active").
|
||||
Fields("id").
|
||||
Array()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keyIDs) > 0 {
|
||||
_, err = tx.Model("api_key_model_scopes").Ctx(ctx).
|
||||
WhereIn("api_key_id", keyIDs).
|
||||
WhereIn("model_id", modelIDs).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -258,6 +258,41 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
// 按成员模型范围过滤
|
||||
userID := middleware.GetUserID(ctx)
|
||||
if userID > 0 {
|
||||
var memberScopes []struct {
|
||||
ModelID int64 `json:"model_id"`
|
||||
ModelName string `json:"model_name"`
|
||||
}
|
||||
_ = g.DB().Model("tnt_member_model_scopes ms").Ctx(ctx).
|
||||
LeftJoin("mdl_models m ON ms.model_id = m.id").
|
||||
Where("ms.tenant_id", tenantID).
|
||||
Where("ms.user_id", userID).
|
||||
Fields("ms.model_id, m.model_id as model_name").
|
||||
Scan(&memberScopes)
|
||||
|
||||
if len(memberScopes) > 0 {
|
||||
allowed := make(map[string]bool, len(memberScopes))
|
||||
for _, s := range memberScopes {
|
||||
if s.ModelID == -1 {
|
||||
// 哨兵:禁止所有模型
|
||||
return &v1.TenantAvailableModelsRes{List: nil}, nil
|
||||
}
|
||||
if s.ModelName != "" {
|
||||
allowed[s.ModelName] = true
|
||||
}
|
||||
}
|
||||
filtered := make([]v1.TenantAvailableModelItem, 0, len(list))
|
||||
for _, item := range list {
|
||||
if allowed[item.ModelId] {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
list = filtered
|
||||
}
|
||||
}
|
||||
|
||||
return &v1.TenantAvailableModelsRes{List: list}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ var ErrStreamInterrupted = errors.New("stream interrupted by client disconnect")
|
||||
// ErrTenantModelNotEnabled 租户未启用该模型
|
||||
var ErrTenantModelNotEnabled = errors.New("model not enabled for this tenant")
|
||||
|
||||
// ErrMemberModelNotAllowed 成员无权使用该模型
|
||||
var ErrMemberModelNotAllowed = errors.New("model not allowed for this member")
|
||||
|
||||
// DeprecationInfo 模型弃用信息
|
||||
type DeprecationInfo struct {
|
||||
Deprecated bool
|
||||
@@ -63,7 +66,8 @@ type DataProvider interface {
|
||||
|
||||
// GetAvailableModels 获取指定租户可用的模型列表。
|
||||
// apiKeyID > 0 时进一步按 API Key 的模型范围过滤。
|
||||
GetAvailableModels(ctx context.Context, tenantID int64, apiKeyID int64) ([]ModelInfo, error)
|
||||
// userID > 0 时进一步按成员的模型范围过滤。
|
||||
GetAvailableModels(ctx context.Context, tenantID, apiKeyID, userID int64) ([]ModelInfo, error)
|
||||
|
||||
// GetModelDetail 获取单个模型的详细信息。
|
||||
// tenantID 用于权限校验(检查租户是否有权使用该模型)。
|
||||
@@ -87,6 +91,9 @@ type DataProvider interface {
|
||||
// CheckApiKeyModelAccess 检查 API Key 是否有权使用指定模型。
|
||||
// 无 scope 记录表示不限制(向后兼容)。
|
||||
CheckApiKeyModelAccess(ctx context.Context, apiKeyID int64, modelName string) (bool, error)
|
||||
|
||||
// InvalidateMemberModelCache 清除指定成员的模型范围缓存。
|
||||
InvalidateMemberModelCache(ctx context.Context, tenantID, userID int64)
|
||||
}
|
||||
|
||||
// ApiKeyInfo API Key 验证结果
|
||||
|
||||
@@ -41,8 +41,9 @@ func HandleResponses(ctx context.Context, body []byte, path string, headers http
|
||||
|
||||
// HandleModels 处理 /v1/models 请求(列出可用模型)
|
||||
// tenantID 用于过滤该租户有权使用的模型,apiKeyID 用于进一步按 Key 的模型范围过滤
|
||||
func HandleModels(ctx context.Context, tenantID int64, apiKeyID int64, provider common.DataProvider) (*dto.ModelsResponse, error) {
|
||||
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID)
|
||||
// userID 用于进一步按成员的模型范围过滤
|
||||
func HandleModels(ctx context.Context, tenantID, apiKeyID, userID int64, provider common.DataProvider) (*dto.ModelsResponse, error) {
|
||||
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ func HandleGeminiGenerateContent(ctx context.Context, body []byte, path string,
|
||||
}
|
||||
|
||||
// HandleGeminiModels 处理 GET /v1beta/models 请求(Gemini 原生格式模型列表)
|
||||
func HandleGeminiModels(ctx context.Context, tenantID int64, apiKeyID int64, provider common.DataProvider) (*dto.GeminiModelsResponse, error) {
|
||||
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID)
|
||||
func HandleGeminiModels(ctx context.Context, tenantID, apiKeyID, userID int64, provider common.DataProvider) (*dto.GeminiModelsResponse, error) {
|
||||
models, err := provider.GetAvailableModels(ctx, tenantID, apiKeyID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -71,6 +71,13 @@ func HandleRealtime(w http.ResponseWriter, r *http.Request, rc *RealtimeContext,
|
||||
return nil, nil, fmt.Errorf("model not found: %s", modelName)
|
||||
}
|
||||
|
||||
// 3.1 检查成员模型范围
|
||||
if allowed, err := provider.CheckMemberModelAccess(ctx, rc.TenantID, rc.UserID, modelName); err != nil {
|
||||
return nil, nil, err
|
||||
} else if !allowed {
|
||||
return nil, nil, constant.NewAuthError("model not allowed for this member")
|
||||
}
|
||||
|
||||
// 4. 渠道选择
|
||||
selection, err := provider.GetChannelForModel(ctx, rc.TenantID, rc.UserID, modelName, nil)
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user