package app import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "strings" "time" "gorm.io/gorm" "yingce/backend/internal/model" "yingce/backend/internal/repository" ) type CreationGuard struct { ExecutionEpoch int64 `json:"executionEpoch"` Owner string `json:"owner"` } type CreationRequest struct { CreationGuard ClientKey string `json:"clientKey"` CanvasID string `json:"canvasId"` Revision int64 `json:"revision"` ExpectedEpoch int64 `json:"expectedEpoch"` State map[string]any `json:"state"` Status string `json:"status"` ProposalVersion int64 `json:"proposalVersion"` Proposal json.RawMessage `json:"proposal"` Ops []CreationCanvasOp `json:"ops"` ItemKey string `json:"itemKey"` Request CreateTaskRequest `json:"request"` SubmissionIDs []string `json:"submissionIds"` SubmissionID string `json:"submissionId"` ExpectedSnapshotHash string `json:"expectedSnapshotHash"` Document json.RawMessage `json:"document"` } type CreationRunOutput struct { model.CreationRun State map[string]any `json:"state"` } type CreationQuote struct { Model string `json:"model"` BillingMode string `json:"billingMode"` Quantity int64 `json:"quantity"` AmountMicrocredits int64 `json:"amountMicrocredits"` Estimated bool `json:"estimated"` ExpiresAt time.Time `json:"expiresAt"` QuoteHash string `json:"quoteHash"` Options map[string]any `json:"options,omitempty"` } type CreationSubmissionOutput struct { model.CreationSubmission Quote CreationQuote `json:"quote"` } type CreationDetail struct { Run CreationRunOutput `json:"run"` Submissions []CreationSubmissionOutput `json:"submissions"` } type creationTaskPreparation struct{ Order *model.BillingOrder } func creationConflict(message string) error { return &AppError{Status: 409, Code: 409, Message: message} } func creationError(err error) error { if errors.Is(err, repository.ErrInsufficientCredits) { return BadAuthRequest("积分不足,请充值后重试") } if errors.Is(err, repository.ErrActiveTaskLimit) { return BadAuthRequest("同时排队或运行的任务已达到上限") } if errors.Is(err, repository.ErrLogicalModelUnavailable) { return creationConflict("模型已更新或停用,请重新准备报价") } if errors.Is(err, repository.ErrCreationConflict) { return creationConflict("创作状态已变化,请重新读取后继续") } if errors.Is(err, gorm.ErrRecordNotFound) { return &AppError{Status: 404, Code: 404, Message: "创作记录不存在或无权访问"} } return err } func creationHash(value any) string { b, _ := json.Marshal(value) h := sha256.Sum256(b) return hex.EncodeToString(h[:]) } func creationRunOutput(run model.CreationRun) CreationRunOutput { state := map[string]any{} _ = json.Unmarshal([]byte(run.StateJSON), &state) return CreationRunOutput{run, state} } func creationSubmissionOutput(item model.CreationSubmission) CreationSubmissionOutput { var quote CreationQuote _ = json.Unmarshal([]byte(item.QuoteJSON), "e) return CreationSubmissionOutput{item, quote} } func validateCreationGuard(run *model.CreationRun, guard CreationGuard) error { if guard.Owner == "" || run.ExecutionOwner != guard.Owner || run.ExecutionEpoch != guard.ExecutionEpoch || run.LeaseExpiresAt == nil || !run.LeaseExpiresAt.After(time.Now()) { return creationConflict("执行控制权已过期,请在当前页面重新接管") } return nil } func validateCreationJSON(value any) error { b, err := json.Marshal(value) if err != nil || len(b) > 1<<20 { return BadAuthRequest("创作内容超过限制或格式无效") } var decoded any _ = json.Unmarshal(b, &decoded) var visit func(any) bool visit = func(v any) bool { switch item := v.(type) { case map[string]any: for key, child := range item { switch strings.ToLower(key) { case "apikey", "secretkey", "authorization", "cookie", "headers", "baseurl", "token", "accesstoken", "refresh_token": if child != nil && child != "" { return false } } if !visit(child) { return false } } case []any: for _, child := range item { if !visit(child) { return false } } case string: if strings.HasPrefix(item, "data:") || strings.Contains(item, "X-Amz-Signature=") || strings.Contains(item, "X-Tos-Signature=") { return false } } return true } if !visit(decoded) { return BadAuthRequest("创作记录不能包含密钥、内嵌媒体或临时签名链接") } return nil } func (s *Service) CreateCreationRun(userID string, req CreationRequest) (*CreationDetail, error) { if strings.TrimSpace(userID) == "" || strings.TrimSpace(req.ClientKey) == "" || len(req.ClientKey) > 120 { return nil, BadAuthRequest("缺少稳定会话键") } if err := validateCreationJSON(req.State); err != nil { return nil, err } if req.CanvasID != "" { if _, err := s.repo.CanvasProjectForUser(userID, req.CanvasID); err != nil { return nil, creationError(err) } } b, _ := json.Marshal(req.State) run := model.CreationRun{ID: newID(), UserID: userID, ClientKey: req.ClientKey, CreateHash: creationHash([]any{req.CanvasID, req.State}), CanvasID: req.CanvasID, Revision: 1, Status: "idle", StateJSON: string(b)} if old, e := s.repo.CreationRunByClientKey(userID, req.ClientKey); e == nil { if old.CreateHash != run.CreateHash { return nil, creationConflict("同一会话键的内容不同") } return s.GetCreationRun(userID, old.ID) } else if !errors.Is(e, gorm.ErrRecordNotFound) { return nil, e } s.storageMu.Lock() defer s.storageMu.Unlock() if err := s.validateCreationStorage(s.repo, userID, true, int64(len(b))); err != nil { return nil, err } if err := s.repo.CreateCreationRun(&run); err != nil { return nil, creationError(err) } return s.GetCreationRun(userID, run.ID) } func (s *Service) GetCreationRun(userID, id string) (*CreationDetail, error) { run, err := s.repo.CreationRun(userID, id) if err != nil { return nil, creationError(err) } items, err := s.repo.CreationSubmissions(userID, id) if err != nil { return nil, err } out := &CreationDetail{Run: creationRunOutput(*run), Submissions: []CreationSubmissionOutput{}} for _, item := range items { out.Submissions = append(out.Submissions, creationSubmissionOutput(item)) } return out, nil } func (s *Service) ListCreationRuns(userID string) (map[string]any, error) { items, err := s.repo.CreationRuns(userID) out := []CreationRunOutput{} for _, run := range items { out = append(out, creationRunOutput(run)) } return map[string]any{"runs": out}, err } func (s *Service) ChangeCreationRun(userID, id, action string, req CreationRequest) (any, error) { if action == "save" || action == "proposal-approve" || action == "proposal-invalidate" { s.storageMu.Lock() defer s.storageMu.Unlock() } var out any err := s.repo.MutateCreationRun(userID, id, func(run *model.CreationRun, repo *repository.Repository) error { previousBytes := len(run.StateJSON) + len(run.ApprovedOperationsJSON) + len(run.ApprovedCanvasJSON) now := time.Now() if action == "claim" { if req.Owner == "" || len(req.Owner) > 120 || req.ExpectedEpoch != run.ExecutionEpoch { return creationConflict("执行代次已变化") } run.ExecutionEpoch++ run.ExecutionOwner = req.Owner until := now.Add(45 * time.Second) run.LeaseExpiresAt = &until } else { if err := validateCreationGuard(run, req.CreationGuard); err != nil { return err } switch action { case "heartbeat": until := now.Add(45 * time.Second) run.LeaseExpiresAt = &until out = map[string]any{"leaseExpiresAt": until} return nil case "release": run.LeaseExpiresAt = nil out = map[string]any{"released": true} return nil case "save": if req.Revision != run.Revision { return repository.ErrCreationConflict } if err := validateCreationJSON(req.State); err != nil { return err } if !strings.Contains("|idle|running|waiting_answer|waiting_proposal|waiting_canvas|waiting_payment|waiting_task|paused|completed|cancelled|", "|"+req.Status+"|") || req.Status == "" { return BadAuthRequest("创作状态无效") } if run.Status == "cancelled" && req.Status != "cancelled" { return creationConflict("已取消会话不能继续执行") } b, _ := json.Marshal(req.State) run.StateJSON = string(b) run.Status = req.Status case "proposal-approve": if req.Revision != run.Revision || req.ProposalVersion <= 0 { return repository.ErrCreationConflict } if err := validateCreationOps(req.Ops); err != nil { return err } if err := validateCreationJSON(req.Proposal); err != nil { return err } for _, op := range req.Ops { if key := stringValue(op.Metadata["storageKey"]); key != "" { if !strings.HasPrefix(key, "resource:") { return BadAuthRequest("已有素材必须来自当前账号资源库") } resource, e := repo.ResourceForUser(userID, strings.TrimPrefix(key, "resource:")) if e != nil { return e } if resource.Status != model.ResourceStatusReady { return BadAuthRequest("已有素材尚未就绪") } } } hash := creationHash([]any{req.Proposal, req.Ops}) if req.ProposalVersion <= run.ApprovedProposalVersion { if req.ProposalVersion == run.ApprovedProposalVersion && hash == run.ApprovedProposalHash { out = creationRunOutput(*run) return nil } return repository.ErrCreationConflict } if err := repo.RevokeCreationSubmissions(id); err != nil { return err } b, _ := json.Marshal(req.Ops) run.ApprovedOperationsJSON = string(b) run.ApprovedProposalVersion = req.ProposalVersion run.ApprovedProposalHash = hash run.ApprovedAt = &now run.ApprovedCanvasJSON = "" if run.CanvasID != "" { canvas, e := repo.CanvasProjectForUser(userID, run.CanvasID) if e != nil { return e } baseline, e := creationApprovalBaseline(canvas.PayloadJSON, req.Ops) if e != nil { return e } run.ApprovedCanvasJSON = baseline } case "proposal-invalidate": if req.Revision != run.Revision { return repository.ErrCreationConflict } if err := repo.RevokeCreationSubmissions(id); err != nil { return err } run.ApprovedAt = nil run.ApprovedProposalHash = "" run.ApprovedOperationsJSON = "" run.ApprovedCanvasJSON = "" default: return BadAuthRequest("未知创作操作") } } run.Revision++ if action == "save" || action == "proposal-approve" { delta := int64(len(run.StateJSON) + len(run.ApprovedOperationsJSON) + len(run.ApprovedCanvasJSON) - previousBytes) if e := s.validateCreationStorage(repo, userID, false, delta); e != nil { return e } } out = creationRunOutput(*run) return nil }) return out, creationError(err) } func (s *Service) prepareCreationTask(userID string, req CreateTaskRequest) (*model.Task, *model.BillingOrder, string, error) { config, _ := req.Input["config"].(map[string]any) if req.LogicalModelID == "" && strings.TrimSpace(stringValue(config["channelId"])) == "" { return nil, nil, "", BadAuthRequest("智能创作目前仅支持后端受管模型,请在原入口使用其他渠道") } if taskInputUsesWorkflowProvider(req.Input) || isTextReplayTaskRequest(req.Input) { return nil, nil, "", BadAuthRequest("智能创作不支持本机、工作流或文本回放任务") } // System selectors are the only authority; discard frontend sentinel credentials. safeConfig := map[string]any{} for _, key := range []string{"channelId", "channelModelKey", "model", "priceTierId", "apiFormat", "interfaceType", "size", "quality", "transparentBackground", "count", "videoSeconds", "vquality", "videoGenerateAudio", "videoWatermark", "videoArkPrivateAssetUpload", "systemPrompt"} { if value, ok := config[key]; ok { safeConfig[key] = value } } config = safeConfig req.Input["config"] = safeConfig if err := validateCreationJSON(req); err != nil { return nil, nil, "", err } if req.Type != "canvas_text" && req.Type != "text" && req.Type != "canvas_image" && req.Type != "canvas_video" { return nil, nil, "", BadAuthRequest("智能创作任务类型不受支持") } expectedMode := map[string]string{"text": "text", "canvas_text": "text", "canvas_image": "image", "canvas_video": "video"}[req.Type] if stringValue(req.Input["mode"]) != expectedMode || strings.TrimSpace(stringValue(req.Input["prompt"])) != strings.TrimSpace(req.Prompt) { return nil, nil, "", BadAuthRequest("任务类型、模式和实际提示词必须一致") } if expectedMode != "text" && req.Input["agentRequests"] != nil { return nil, nil, "", BadAuthRequest("媒体任务不允许携带独立模型协议请求") } if count := stringValue(config["count"]); count != "" && count != "1" { return nil, nil, "", BadAuthRequest("每个获批执行项只能生成一个产物") } if req.Input["mask"] != nil { return nil, nil, "", BadAuthRequest("本期智能创作暂不支持蒙版任务") } if expectedMode != "text" { canvas, e := s.repo.CanvasProjectForUser(userID, req.ProjectID) if e != nil { return nil, nil, "", creationError(e) } doc, e := creationDocument(canvas.PayloadJSON) if e != nil { return nil, nil, "", e } nodes, e := creationObjects(doc["nodes"]) if e != nil { return nil, nil, "", e } if refs, ok := req.Input["referenceImages"].([]any); ok { for _, raw := range refs { ref, _ := raw.(map[string]any) node := nodes[stringValue(ref["id"])] meta, _ := node["metadata"].(map[string]any) if node == nil || node["type"] != "image" || meta["status"] != "success" || stringValue(ref["storageKey"]) != stringValue(meta["storageKey"]) { return nil, nil, "", creationConflict("参考素材已变化或尚未就绪") } } } } for _, name := range []string{"referenceImages", "referenceVideos", "referenceAudios"} { if list, ok := req.Input[name].([]any); ok { for _, raw := range list { media, ok := raw.(map[string]any) if !ok { return nil, nil, "", BadAuthRequest("素材引用格式无效") } key := stringValue(media["storageKey"]) if !strings.HasPrefix(key, "resource:") { return nil, nil, "", BadAuthRequest("请先将参考素材保存到当前账号资源库") } resource, err := s.repo.ResourceForUser(userID, strings.TrimPrefix(key, "resource:")) if err != nil { return nil, nil, "", creationError(err) } if resource.Status != "ready" { return nil, nil, "", BadAuthRequest("参考素材尚未就绪") } delete(media, "url") delete(media, "dataUrl") media["bytes"] = resource.Size media["durationMs"] = resource.DurationMs } } } prepared := &creationTaskPreparation{} req.creationPrepare = prepared task, err := s.CreateTask(userID, req) if err != nil { return nil, nil, "", err } var input map[string]any _ = json.Unmarshal([]byte(task.InputJSON), &input) resolved, _ := input["config"].(map[string]any) for _, key := range []string{"size", "videoSeconds", "vquality", "quality", "count"} { if requested := stringValue(config[key]); requested != "" && !strings.EqualFold(requested, stringValue(resolved[key])) { return nil, nil, "", creationConflict("模型解析后的生成规格与请求不同,请调整方案后重新报价") } } channel, channelErr := s.repo.SystemChannel(stringValue(resolved["channelId"])) if channelErr != nil { return nil, nil, "", channelErr } if expectedMode == "text" { var typed canvasGenerationInput if err = json.Unmarshal([]byte(task.InputJSON), &typed); err != nil { return nil, nil, "", err } if len(typed.ReferenceImages) > 0 { cm, e := s.repo.ChannelModelByKey(channel.ID, stringValue(resolved["model"])) if e != nil { return nil, nil, "", e } profile, e := DecodeModelCapabilityConfig(cm.CapabilityConfigJSON) if e != nil || profile == nil || profile.Text == nil || profile.Text.References.MaxImages < len(typed.ReferenceImages) { return nil, nil, "", BadAuthRequest("当前文本模型未配置足够的图片理解能力") } } if err = validateAgentResourcePlaceholders(typed); err != nil { return nil, nil, "", err } } sig, err := s.repo.CreationPriceSignature(task, stringValue(resolved["channelId"]), stringValue(resolved["model"])) return task, prepared.Order, sig, err } func creationQuoteFor(task *model.Task, order *model.BillingOrder, signature string, expires time.Time) CreationQuote { quote := CreationQuote{Model: task.Model, BillingMode: "free", Quantity: 1, ExpiresAt: expires} if order != nil { quote.BillingMode = order.BillingMode quote.Quantity = order.Quantity quote.AmountMicrocredits = order.AmountMicrocredits quote.Estimated = order.BillingMode == "token" } var input map[string]any _ = json.Unmarshal([]byte(task.InputJSON), &input) config, _ := input["config"].(map[string]any) quote.Options = map[string]any{} for _, key := range []string{"size", "videoSeconds", "vquality", "quality", "maxTokens"} { if value, ok := config[key]; ok { quote.Options[key] = value } } quote.QuoteHash = creationHash([]any{signature, quote.BillingMode, quote.Quantity, quote.AmountMicrocredits, quote.Options}) return quote } func validateCreationSubmissionScope(run *model.CreationRun, version int64, req CreateTaskRequest) error { if run.Status == "paused" || run.Status == "cancelled" || run.Status == "completed" { return creationConflict("请先恢复创作任务") } if req.Type == "canvas_text" || req.Type == "text" { if req.ProjectID != "" && req.ProjectID != run.CanvasID { return creationConflict("规划任务的画布关联不匹配") } return nil } if run.CanvasID == "" || req.ProjectID != run.CanvasID { return creationConflict("媒体任务必须提交到当前创作画布") } if run.ApprovedAt == nil || version != run.ApprovedProposalVersion || run.ApprovedProposalHash == "" { return creationConflict("请先确认当前方案") } var ops []CreationCanvasOp _ = json.Unmarshal([]byte(run.ApprovedOperationsJSON), &ops) nodeID := stringValue(req.Input["nodeId"]) metadata, _ := req.Input["metadata"].(map[string]any) if nodeID == "" { nodeID = stringValue(metadata["nodeId"]) } for _, op := range ops { if op.ID != nodeID || nodeID == "" { continue } if op.Type != "add_node" && op.Type != "update_node" { continue } meta := op.Metadata if nested, ok := op.Patch["metadata"].(map[string]any); ok { meta = mergeCreationMaps(nested, meta) } prompt := stringValue(meta["prompt"]) if prompt == "" { prompt = stringValue(meta["text"]) } if strings.TrimSpace(req.Prompt) != strings.TrimSpace(prompt) || prompt == "" { return creationConflict("任务提示词已超出获批方案") } if stringValue(meta["model"]) == "" || stringValue(meta["model"]) != req.Model { return creationConflict("模型与已批准方案不同") } config, _ := req.Input["config"].(map[string]any) for _, key := range []string{"size", "videoSeconds", "vquality", "quality"} { metadataKey := key if key == "videoSeconds" { metadataKey = "seconds" } approved := strings.TrimSpace(stringValue(meta[metadataKey])) candidate := strings.TrimSpace(stringValue(config[key])) if metadataKey == "quality" { approvedNorm := strings.ToLower(approved) candidateNorm := strings.ToLower(candidate) // auto/any 与空缺在图片生成中等价,前端 omittedImageQuality 可能会省略默认 quality if (approvedNorm == "auto" || approvedNorm == "any" || approvedNorm == "") && (candidateNorm == "auto" || candidateNorm == "any" || candidateNorm == "") { continue } if approvedNorm == candidateNorm { continue } } if approved != "" && approved != candidate { return creationConflict("生成规格与已批准方案不同") } } refs, _ := meta["referenceNodeIds"].([]any) images, _ := req.Input["referenceImages"].([]any) if len(refs) != len(images) { return creationConflict("参考素材数量与已批准方案不同") } for index, ref := range refs { image, _ := images[index].(map[string]any) if stringValue(ref) != stringValue(image["id"]) { return creationConflict("参考素材与已批准方案不同") } } for _, kind := range []string{"referenceVideos", "referenceAudios"} { if values, ok := req.Input[kind].([]any); ok && len(values) > 0 { return creationConflict("本期方案尚未授权视频或音频参考输入") } } return nil } return creationConflict("任务节点不在已批准方案范围内") } func (s *Service) PrepareCreationSubmission(userID, id string, req CreationRequest) (*CreationSubmissionOutput, error) { if req.ItemKey == "" || len(req.ItemKey) > 160 { return nil, BadAuthRequest("缺少稳定执行项键") } if strings.HasPrefix(req.ItemKey, "requote:") { return nil, BadAuthRequest("该执行项键由报价刷新接口保留") } run, err := s.repo.CreationRun(userID, id) if err != nil { return nil, creationError(err) } if err = validateCreationGuard(run, req.CreationGuard); err != nil { return nil, err } if err = validateCreationSubmissionScope(run, req.ProposalVersion, req.Request); err != nil { return nil, err } item, original, err := s.buildCreationSubmission(userID, run, req.ItemKey, req.ProposalVersion, req.Request) if err != nil { return nil, err } s.storageMu.Lock() defer s.storageMu.Unlock() err = s.repo.MutateCreationRun(userID, id, func(current *model.CreationRun, repo *repository.Repository) error { if e := validateCreationGuard(current, req.CreationGuard); e != nil { return e } if e := validateCreationSubmissionScope(current, req.ProposalVersion, original); e != nil { return e } items, e := repo.CreationSubmissions(userID, id) if e != nil { return e } for _, old := range items { if old.ItemKey == req.ItemKey { if old.RequestHash != item.RequestHash || old.ProposalVersion != item.ProposalVersion { return repository.ErrCreationConflict } item = old return nil } } if e = s.validateCreationStorage(repo, userID, false, int64(len(item.RequestJSON)+len(item.QuoteJSON)+len(item.PriceSignature))); e != nil { return e } return repo.SaveCreationSubmission(&item) }) if err != nil { return nil, creationError(err) } out := creationSubmissionOutput(item) return &out, nil } func (s *Service) buildCreationSubmission(userID string, run *model.CreationRun, itemKey string, proposalVersion int64, request CreateTaskRequest) (model.CreationSubmission, CreateTaskRequest, error) { // JSON normalization also isolates the immutable snapshot from mutable caller maps. b, err := json.Marshal(request) if err != nil { return model.CreationSubmission{}, CreateTaskRequest{}, err } var normalized CreateTaskRequest if err = json.Unmarshal(b, &normalized); err != nil { return model.CreationSubmission{}, normalized, err } task, order, signature, err := s.prepareCreationTask(userID, normalized) if err != nil { return model.CreationSubmission{}, normalized, err } expires := time.Now().Add(5 * time.Minute) quote := creationQuoteFor(task, order, signature, expires) quoteJSON, _ := json.Marshal(quote) requestJSON, _ := json.Marshal(normalized) item := model.CreationSubmission{ID: newID(), UserID: userID, RunID: run.ID, ItemKey: itemKey, ProposalVersion: proposalVersion, ProposalHash: run.ApprovedProposalHash, RequestJSON: string(requestJSON), RequestHash: creationHash(normalized), QuoteJSON: string(quoteJSON), PriceSignature: signature, ExpiresAt: expires} return item, normalized, nil } // Refresh creates an unapproved successor. The old quote and approval remain immutable. func (s *Service) RefreshCreationSubmission(userID, id string, req CreationRequest) (*CreationSubmissionOutput, error) { run, err := s.repo.CreationRun(userID, id) if err != nil { return nil, creationError(err) } if err = validateCreationGuard(run, req.CreationGuard); err != nil { return nil, err } old, err := s.repo.CreationSubmission(userID, id, req.SubmissionID) if err != nil { return nil, creationError(err) } if old.TaskID != nil { return nil, creationConflict("任务已提交,请查看原任务,不需要刷新报价") } successorKey := "requote:" + old.ID items, err := s.repo.CreationSubmissions(userID, id) if err != nil { return nil, err } for _, item := range items { if item.ItemKey == successorKey { out := creationSubmissionOutput(item) return &out, nil } } if old.RevokedAt != nil { return nil, creationConflict("原方案已撤销,请重新准备生成项") } var request CreateTaskRequest if err = json.Unmarshal([]byte(old.RequestJSON), &request); err != nil { return nil, err } if err = validateCreationSubmissionScope(run, old.ProposalVersion, request); err != nil { return nil, err } if old.ProposalVersion > 0 && old.ProposalHash != run.ApprovedProposalHash { return nil, creationConflict("原方案已变化,请重新准备生成项") } item, normalized, err := s.buildCreationSubmission(userID, run, successorKey, old.ProposalVersion, request) if err != nil { return nil, err } s.storageMu.Lock() defer s.storageMu.Unlock() err = s.repo.MutateCreationRun(userID, id, func(current *model.CreationRun, repo *repository.Repository) error { if e := validateCreationGuard(current, req.CreationGuard); e != nil { return e } fresh, e := repo.CreationSubmission(userID, id, old.ID) if e != nil { return e } if fresh.TaskID != nil { return creationConflict("任务已经提交,不能刷新报价") } all, e := repo.CreationSubmissions(userID, id) if e != nil { return e } for _, existing := range all { if existing.ItemKey == successorKey { item = existing return nil } } if fresh.RevokedAt != nil { return creationConflict("原报价已撤销") } if e = validateCreationSubmissionScope(current, fresh.ProposalVersion, normalized); e != nil { return e } if fresh.ProposalVersion > 0 && fresh.ProposalHash != current.ApprovedProposalHash { return repository.ErrCreationConflict } if e = s.validateCreationStorage(repo, userID, false, int64(len(item.RequestJSON)+len(item.QuoteJSON)+len(item.PriceSignature))); e != nil { return e } now := time.Now() fresh.RevokedAt = &now if e = repo.SaveCreationSubmission(fresh); e != nil { return e } return repo.SaveCreationSubmission(&item) }) if err != nil { return nil, creationError(err) } out := creationSubmissionOutput(item) return &out, nil } func (s *Service) validateCreationStorage(repo *repository.Repository, userID string, creating bool, delta int64) error { usage, err := repo.UserStorageUsage(userID) if err != nil { return err } _, bytes, err := repo.CreationStorageUsage(userID) if err != nil { return err } policy, err := s.RuntimePolicy() if err != nil { return err } return validateStructuredStorageQuotaWithPolicy(usage, "canvas", creating, bytes+delta, policy.Resource) } func (s *Service) ApproveCreationSubmissions(userID, id string, req CreationRequest) (map[string]any, error) { if len(req.SubmissionIDs) == 0 || len(req.SubmissionIDs) > 20 { return nil, BadAuthRequest("请选择 1 到 20 项生成任务") } // Compute current prices before the write transaction; transaction checks their config signatures again. prepared := map[string]*model.Task{} signatures := map[string]string{} for _, sid := range req.SubmissionIDs { item, err := s.repo.CreationSubmission(userID, id, sid) if err != nil { return nil, creationError(err) } var request CreateTaskRequest _ = json.Unmarshal([]byte(item.RequestJSON), &request) task, order, sig, err := s.prepareCreationTask(userID, request) if err != nil { return nil, err } if creationQuoteFor(task, order, sig, item.ExpiresAt).QuoteHash != creationSubmissionOutput(*item).Quote.QuoteHash { return nil, creationConflict("报价已变化,请重新准备并确认") } prepared[sid] = task signatures[sid] = sig } out := []CreationSubmissionOutput{} err := s.repo.MutateCreationRun(userID, id, func(run *model.CreationRun, repo *repository.Repository) error { if e := validateCreationGuard(run, req.CreationGuard); e != nil { return e } now := time.Now() for _, sid := range req.SubmissionIDs { item, e := repo.CreationSubmission(userID, id, sid) if e != nil { return e } if item.RevokedAt != nil || !item.ExpiresAt.After(now) { return creationConflict("报价已过期或撤销") } var request CreateTaskRequest _ = json.Unmarshal([]byte(item.RequestJSON), &request) if e = validateCreationSubmissionScope(run, item.ProposalVersion, request); e != nil { return e } if item.ProposalVersion > 0 && item.ProposalHash != run.ApprovedProposalHash { return repository.ErrCreationConflict } if e = checkCreationPriceSignature(repo, prepared[sid], signatures[sid]); e != nil { return e } if item.ApprovedAt == nil { item.ApprovedAt = &now if e = repo.SaveCreationSubmission(item); e != nil { return e } } out = append(out, creationSubmissionOutput(*item)) } return nil }) return map[string]any{"submissions": out}, creationError(err) } func checkCreationPriceSignature(repo *repository.Repository, task *model.Task, want string) error { var input map[string]any _ = json.Unmarshal([]byte(task.InputJSON), &input) config, _ := input["config"].(map[string]any) got, err := repo.CreationPriceSignature(task, stringValue(config["channelId"]), stringValue(config["model"])) if err != nil { return err } if got != want { return creationConflict("模型或价格配置已更新,请重新确认") } return nil } func (s *Service) ExecuteCreationSubmission(userID, id string, req CreationRequest) (*model.Task, error) { run, err := s.repo.CreationRun(userID, id) if err != nil { return nil, creationError(err) } if err = validateCreationGuard(run, req.CreationGuard); err != nil { return nil, err } item, err := s.repo.CreationSubmission(userID, id, req.SubmissionID) if err != nil { return nil, creationError(err) } if item.TaskID != nil { task, e := s.repo.TaskForUser(userID, *item.TaskID) if e != nil { return nil, e } return taskForOutput(*task), nil } var request CreateTaskRequest if err = json.Unmarshal([]byte(item.RequestJSON), &request); err != nil { return nil, err } if err = validateCreationSubmissionScope(run, item.ProposalVersion, request); err != nil { return nil, err } task, order, signature, err := s.prepareCreationTask(userID, request) if err != nil { return nil, err } if creationQuoteFor(task, order, signature, item.ExpiresAt).QuoteHash != creationSubmissionOutput(*item).Quote.QuoteHash { return nil, creationConflict("报价已变化,请重新确认") } var input map[string]any _ = json.Unmarshal([]byte(task.InputJSON), &input) if err = s.protectTaskSecrets(input); err != nil { return nil, err } b, _ := json.Marshal(input) task.InputJSON = string(b) task.CreationSubmissionID = &item.ID if order != nil { task.BillingOrderID = order.ID } policy, err := s.RuntimePolicy() if err != nil { return nil, err } s.storageMu.Lock() defer s.storageMu.Unlock() err = s.repo.MutateCreationRun(userID, id, func(current *model.CreationRun, repo *repository.Repository) error { if e := validateCreationGuard(current, req.CreationGuard); e != nil { return e } fresh, e := repo.CreationSubmission(userID, id, item.ID) if e != nil { return e } if fresh.TaskID != nil { existing, e := repo.TaskForUser(userID, *fresh.TaskID) if e != nil { return e } task = existing return nil } if fresh.ApprovedAt == nil || fresh.RevokedAt != nil || !fresh.ExpiresAt.After(time.Now()) { return creationConflict("任务尚未批准或报价已过期") } if e = validateCreationSubmissionScope(current, fresh.ProposalVersion, request); e != nil { return e } if fresh.ProposalVersion > 0 && fresh.ProposalHash != current.ApprovedProposalHash { return repository.ErrCreationConflict } if e = checkCreationPriceSignature(repo, task, signature); e != nil { return e } if e = createTaskWithStorageQuotaRepository(repo, task, order, policy); e != nil { return e } fresh.TaskID = &task.ID if e = repo.SaveCreationSubmission(fresh); e != nil { return e } return nil }) if err != nil { return nil, creationError(err) } return taskForOutput(*task), nil }