refactor(*): 稳定期治理 - 收敛核心架构、错误语义并修复跨项目数据竞态
This commit is contained in:
1 parent
bacd502f5c
commit
b5f3413ded
298 files changed
+12681
-26428
No files matched your search
@@ -12,6 +12,9 @@ import (
|
||||
|
||||
// CreateTask 收敛任务进入系统前的 admission 流程:输入标准化、逻辑模型路由、
|
||||
// 能力/额度校验和持久化。执行阶段由 worker 与 provider 相关模块负责。
|
||||
// CreateTask 校验并创建一条生成任务。
|
||||
// 这是常规模型生成任务的写入口:客户端只提交创作意图,模型、渠道、协议和计价信息必须由服务端目录重新解析,
|
||||
// 以保证“可展示的模型”与“实际执行及扣费的模型”来自同一份有效配置。
|
||||
func (s *Service) CreateTask(userID string, req CreateTaskRequest) (*model.Task, error) {
|
||||
if s.IsDraining() {
|
||||
return nil, &AppError{Status: 503, Code: 503, Message: "服务正在维护,暂不接受新的生成任务", Retryable: true}
|
||||
@@ -152,9 +155,11 @@ func (s *Service) resolveTaskModelSelection(input map[string]any, logicalModelID
|
||||
// 自定义渠道没有系统 channelId;它会在后续由自定义渠道功能开关、
|
||||
// 能力校验和 provider 配置校验共同处理,不能误报为“缺少系统渠道”。
|
||||
if !customChannelTask {
|
||||
if err := s.validateSystemChannelModelSelection(input); err != nil {
|
||||
resolvedInput, err := s.resolveSystemChannelModelSelection(input, taskType, operation)
|
||||
if err != nil {
|
||||
return nil, input, err
|
||||
}
|
||||
return nil, resolvedInput, nil
|
||||
}
|
||||
return nil, input, nil
|
||||
}
|
||||
@@ -284,47 +289,142 @@ func (s *Service) requireCustomChannelsForTaskInput(input map[string]any) error
|
||||
return s.RequireFeature(FeatureCustomChannels)
|
||||
}
|
||||
|
||||
// validateSystemChannelModelSelection 校验系统渠道模型选择的有效性
|
||||
func (s *Service) validateSystemChannelModelSelection(input map[string]any) error {
|
||||
// resolveSystemChannelModelSelection 是系统渠道任务的 admission 边界。
|
||||
// 客户端只负责表达创作参数;模型协议、能力合同、价格档和上游模型标识必须从服务端记录重建,
|
||||
// 避免出现“按一个规格校验/计费,却按另一个规格执行”的跨阶段漂移。
|
||||
func (s *Service) resolveSystemChannelModelSelection(input map[string]any, taskType string, operation string) (map[string]any, error) {
|
||||
config, ok := input["config"].(map[string]any)
|
||||
if !ok {
|
||||
return InvalidModelSelection("缺少模型配置")
|
||||
return input, InvalidModelSelection("缺少模型配置")
|
||||
}
|
||||
|
||||
channelID, _ := config["channelId"].(string)
|
||||
modelKey, _ := config["model"].(string)
|
||||
|
||||
channelID = strings.TrimSpace(channelID)
|
||||
modelKey = strings.TrimSpace(modelKey)
|
||||
channelID := strings.TrimSpace(stringValue(config["channelId"]))
|
||||
modelKey := strings.TrimPrefix(strings.TrimSpace(stringValue(config["model"])), "models/")
|
||||
|
||||
if channelID == "" || modelKey == "" {
|
||||
return InvalidModelSelection("必须指定系统渠道和模型")
|
||||
return input, InvalidModelSelection("必须指定系统渠道和模型")
|
||||
}
|
||||
|
||||
// 验证渠道存在且启用
|
||||
channel, err := s.repo.SystemChannel(channelID)
|
||||
if err != nil {
|
||||
return InvalidModelSelection("指定的渠道不存在")
|
||||
return input, InvalidModelSelection("指定的渠道不存在")
|
||||
}
|
||||
if !channel.Enabled || channel.Scope != model.ChannelScopeSystem {
|
||||
return InvalidModelSelection("指定的渠道不可用")
|
||||
return input, InvalidModelSelection("指定的渠道不可用")
|
||||
}
|
||||
|
||||
// 验证渠道模型存在且启用
|
||||
channelModel, err := s.repo.ChannelModelByKey(channelID, modelKey)
|
||||
if err != nil {
|
||||
return InvalidModelSelection("指定的模型不存在")
|
||||
return input, InvalidModelSelection("指定的模型不存在")
|
||||
}
|
||||
if !channelModel.Enabled {
|
||||
return InvalidModelSelection("指定的模型已停用")
|
||||
return input, InvalidModelSelection("指定的模型已停用")
|
||||
}
|
||||
if channelModel.Protocol == "" {
|
||||
return input, InvalidModelSelection("指定的模型未配置请求协议")
|
||||
}
|
||||
|
||||
// 验证价格配置
|
||||
if !HasValidPrice(channelModel) {
|
||||
return ModelPriceNotConfigured("指定的模型未配置有效价格")
|
||||
nextConfig := make(map[string]any, len(config)+6)
|
||||
for key, value := range config {
|
||||
switch key {
|
||||
case "channelId", "channelModelKey", "priceTierId", "providerModelKey", "apiFormat", "interfaceType", "baseUrl", "allowLocalChannel", "apiKey", "secretKey", "headers", "model", "capabilityConfig":
|
||||
continue
|
||||
default:
|
||||
nextConfig[key] = value
|
||||
}
|
||||
}
|
||||
// capabilityOptions 是路由、校验和计价共同使用的请求规格;存在时必须覆盖 config 中的同名字段,
|
||||
// 不能让两份客户端数据分别驱动计费与真实上游请求。
|
||||
if options, ok := input["capabilityOptions"].(map[string]any); ok {
|
||||
for key, value := range options {
|
||||
canonical := canonicalCapabilityOptionName(key)
|
||||
if isCapabilityOptionFor(channelModel.Capability, canonical) {
|
||||
nextConfig[canonical] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
capabilityConfig, err := normalizedChannelModelCapability(channelModel)
|
||||
if err != nil {
|
||||
return input, InvalidModelSelection("指定的模型能力配置无效,请联系管理员")
|
||||
}
|
||||
applyChannelCapabilityDefaults(nextConfig, channelModel.Capability, capabilityConfig)
|
||||
input["config"] = nextConfig
|
||||
input["capabilityOptions"] = capabilityOptionsFromConfig(channelModel.Capability, nextConfig)
|
||||
|
||||
intent := ModelRequestIntentFromTaskInput(input, taskType, operation)
|
||||
if normalizeCapability(intent.Capability) != normalizeCapability(channelModel.Capability) {
|
||||
return input, ModelCapabilityNotSupported("所选模型与任务能力不匹配")
|
||||
}
|
||||
if normalizeCapability(channelModel.Capability) != "audio" {
|
||||
spec, specErr := CapabilitySpecFromModelCapabilityConfig(capabilityConfig, channelModel.Capability)
|
||||
if specErr != nil {
|
||||
return input, InvalidModelSelection("指定的模型能力配置无效,请联系管理员")
|
||||
}
|
||||
if match := MatchCapability(spec, intent); !match.Matched {
|
||||
return input, ModelCapabilityNotSupported("所选模型不支持当前请求:" + strings.Join(match.Reasons, ";"))
|
||||
}
|
||||
}
|
||||
|
||||
priceTier := channelModelPriceTierForIntent(*channelModel, intent)
|
||||
if priceTier == nil || !ValidatePriceTierPrice(priceTier, channelModel.Capability, channelModel.Protocol) {
|
||||
return input, ModelPriceNotConfigured("指定的模型未配置当前规格的有效价格")
|
||||
}
|
||||
|
||||
nextConfig["channelId"] = channel.ID
|
||||
nextConfig["model"] = channelModel.ModelKey
|
||||
nextConfig["channelModelKey"] = channelModel.ModelKey
|
||||
nextConfig["priceTierId"] = priceTier.ID
|
||||
nextConfig["providerModelKey"] = firstNonEmpty(priceTier.ProviderModelKey, channelModel.ProviderModelKey, channelModel.ModelKey)
|
||||
nextConfig["interfaceType"] = string(channelModel.Protocol)
|
||||
nextConfig["apiFormat"] = channelAPIFormatForProtocol(channel.APIFormat, channelModel.Protocol)
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// applyChannelCapabilityDefaults 只采用管理员保存的能力默认值,且仅填补客户端未表达的参数。
|
||||
// 这些值随后会写回 capabilityOptions,使能力校验、SKU 选择和 provider 执行看到同一份规格。
|
||||
func applyChannelCapabilityDefaults(config map[string]any, capability string, profile *ModelCapabilityConfig) {
|
||||
setDefault := func(key string, value any) {
|
||||
if existing, exists := config[key]; !exists || existing == nil || strings.TrimSpace(fmt.Sprint(existing)) == "" {
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
switch normalizeCapability(capability) {
|
||||
case "image":
|
||||
if profile == nil || profile.Image == nil {
|
||||
return
|
||||
}
|
||||
if profile.Image.Size.Parameter != "none" {
|
||||
setDefault("size", profile.Image.Size.Default)
|
||||
}
|
||||
if profile.Image.Quality.Supported {
|
||||
setDefault("quality", profile.Image.Quality.Default)
|
||||
}
|
||||
setDefault("transparentBackground", profile.Image.TransparentBackground.Default)
|
||||
setDefault("count", 1)
|
||||
case "video":
|
||||
if profile == nil || profile.Video == nil {
|
||||
return
|
||||
}
|
||||
if videoDurationSupported(profile.Video) {
|
||||
setDefault("videoSeconds", profile.Video.Duration.Default)
|
||||
}
|
||||
setDefault("size", profile.Video.DefaultRatio)
|
||||
setDefault("vquality", profile.Video.DefaultResolution)
|
||||
setDefault("videoGenerateAudio", profile.Video.GenerateAudio.Default)
|
||||
setDefault("videoWatermark", profile.Video.Watermark.Default)
|
||||
}
|
||||
}
|
||||
|
||||
func capabilityOptionsFromConfig(capability string, config map[string]any) map[string]any {
|
||||
options := map[string]any{}
|
||||
for key, value := range config {
|
||||
canonical := canonicalCapabilityOptionName(key)
|
||||
if isCapabilityOptionFor(capability, canonical) && value != nil && strings.TrimSpace(fmt.Sprint(value)) != "" {
|
||||
options[canonical] = value
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func taskInputUsesCustomChannel(input map[string]any) bool {
|
||||
@@ -358,6 +458,11 @@ func taskInputUsesWorkflowProvider(input map[string]any) bool {
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// 系统渠道的 interfaceType 是客户端缓存,不是授权事实;必须先走系统模型 admission,
|
||||
// 不能通过伪造工作流协议绕开渠道模型、能力和价格校验。
|
||||
if strings.TrimSpace(stringValue(config["channelId"])) != "" {
|
||||
return false
|
||||
}
|
||||
return isWorkflowProviderInterface(strings.TrimSpace(fmt.Sprint(config["interfaceType"])))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user