package scenario import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "git.iwork-ai.com/xdc/iqudo-top1/internal/access" "git.iwork-ai.com/xdc/iqudo-top1/internal/audit" "git.iwork-ai.com/xdc/iqudo-top1/internal/auth" "git.iwork-ai.com/xdc/iqudo-top1/internal/model" "git.iwork-ai.com/xdc/iqudo-top1/internal/response" "git.iwork-ai.com/xdc/iqudo-top1/internal/resultcontract" "github.com/gin-gonic/gin" "gorm.io/datatypes" "gorm.io/gorm" ) type contractInput struct { OutputSchema map[string]interface{} `json:"output_schema"` ResultSchema map[string]interface{} `json:"result_schema"` Rules []ruleInput `json:"rules"` AllowedOrigins []string `json:"allowed_origins"` } type ruleInput struct { RuleKey string `json:"rule_key"` Name string `json:"name"` Condition map[string]interface{} `json:"condition"` Actions []map[string]interface{} `json:"actions"` Priority int `json:"priority"` Status string `json:"status"` } func (h *Handler) GetContract(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) id, ok := contractScenarioID(c) if !ok || !access.CanViewScenario(h.db, p, id) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在") } return } var scenario model.Scenario if err := h.db.Where("id = ? AND tenant_id = ?", id, p.TenantID).First(&scenario).Error; err != nil { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在") return } rules := make([]model.ScenarioRule, 0) if err := h.db.Where("scenario_id = ? AND tenant_id = ?", id, p.TenantID).Order("priority,id").Find(&rules).Error; err != nil { response.Error(c, http.StatusInternalServerError, "QUERY_FAILED", "查询场景规则失败") return } response.OK(c, gin.H{"input_schema": scenario.InputSchema, "output_schema": scenario.OutputSchema, "result_schema": scenario.ResultSchema, "scenario_key": scenario.ScenarioKey, "public_key": scenario.PublicKey, "allowed_origins": scenario.AllowedOrigins, "rules": rules}) } func (h *Handler) ReplaceContract(c *gin.Context) { p, _ := auth.PrincipalFromContext(c) id, ok := contractScenarioID(c) if !ok || !access.CanEditScenario(h.db, p, id) { if ok { response.Error(c, http.StatusNotFound, "NOT_FOUND", "场景不存在或不可编辑") } return } var input contractInput if err := c.ShouldBindJSON(&input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "契约 JSON 格式不正确") return } if input.OutputSchema == nil { input.OutputSchema = map[string]interface{}{"fields": []interface{}{}} } if input.ResultSchema == nil { input.ResultSchema = map[string]interface{}{"fields": []interface{}{}} } if err := validateContractInput(input); err != nil { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", err.Error()) return } seen := map[string]bool{} for _, rule := range input.Rules { if !fieldKeyPattern.MatchString(rule.RuleKey) || rule.Name == "" || seen[rule.RuleKey] { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "规则标识必须唯一且格式正确") return } seen[rule.RuleKey] = true if rule.Status != "" && rule.Status != "active" && rule.Status != "disabled" { response.Error(c, http.StatusBadRequest, "INVALID_ARGUMENT", "规则状态不正确") return } } outputRaw, _ := json.Marshal(input.OutputSchema) resultRaw, _ := json.Marshal(input.ResultSchema) originsRaw, _ := json.Marshal(input.AllowedOrigins) err := h.db.Transaction(func(tx *gorm.DB) error { var oldRules []model.ScenarioRule if err := tx.Where("scenario_id = ? AND tenant_id = ?", id, p.TenantID).Find(&oldRules).Error; err != nil { return err } if err := tx.Model(&model.Scenario{}).Where("id = ? AND tenant_id = ?", id, p.TenantID).Updates(map[string]interface{}{"output_schema": datatypes.JSON(outputRaw), "result_schema": datatypes.JSON(resultRaw), "allowed_origins": datatypes.JSON(originsRaw)}).Error; err != nil { return err } if err := tx.Where("scenario_id = ? AND tenant_id = ?", id, p.TenantID).Delete(&model.ScenarioRule{}).Error; err != nil { return err } for _, rule := range input.Rules { condition, _ := json.Marshal(rule.Condition) actions, _ := json.Marshal(rule.Actions) status := rule.Status if status == "" { status = "active" } row := model.ScenarioRule{TenantID: p.TenantID, ScenarioID: id, RuleKey: rule.RuleKey, Name: rule.Name, Condition: datatypes.JSON(condition), Actions: datatypes.JSON(actions), Priority: rule.Priority, Status: status} if err := tx.Create(&row).Error; err != nil { return err } if err := audit.RecordTx(tx, p, "create", "scenario_rule", row.ID, gin.H{"scenario_id": id, "rule_key": row.RuleKey}); err != nil { return err } } for _, rule := range oldRules { if err := audit.RecordTx(tx, p, "archive", "scenario_rule", rule.ID, gin.H{"scenario_id": id, "rule_key": rule.RuleKey, "name": rule.Name, "priority": rule.Priority, "status": rule.Status}); err != nil { return err } } return audit.RecordTx(tx, p, "update", "scenario", id, gin.H{"contract": true}) }) if err != nil { response.Error(c, http.StatusInternalServerError, "SAVE_FAILED", "保存场景契约失败") return } h.GetContract(c) } func validateContractInput(input contractInput) error { fields, ok := input.OutputSchema["fields"].([]interface{}) if !ok { return fmt.Errorf("output_schema.fields 必须是数组") } outputKeys := map[string]bool{} allowedSources := map[string]bool{"input": true, "derived": true, "form": true, "system": true} for index, raw := range fields { field, ok := raw.(map[string]interface{}) if !ok { return fmt.Errorf("第 %d 个输出字段必须是对象", index+1) } key, _ := field["key"].(string) if !fieldKeyPattern.MatchString(key) || outputKeys[key] { return fmt.Errorf("输出字段标识必须唯一且格式正确") } outputKeys[key] = true source, _ := field["source"].(string) if source == "" { source = "input" } if !allowedSources[source] { return fmt.Errorf("输出字段 %s 使用了不支持的来源 %s", key, source) } if sourceField, exists := field["source_field"]; exists { value, ok := sourceField.(string) if !ok || !fieldKeyPattern.MatchString(value) { return fmt.Errorf("输出字段 %s 的 source_field 格式不正确", key) } } } if err := resultcontract.ValidateSchema(input.ResultSchema); err != nil { return err } for _, origin := range input.AllowedOrigins { if origin == "" || (origin != "*" && !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://")) { return fmt.Errorf("允许域名必须是完整的 http/https Origin") } } for _, rule := range input.Rules { if len(rule.Condition) == 0 { return fmt.Errorf("规则 %s 的条件不能为空", rule.RuleKey) } if err := validateContractCondition(rule.Condition, 0); err != nil { return fmt.Errorf("规则 %s 条件不正确: %w", rule.RuleKey, err) } if len(rule.Actions) == 0 { return fmt.Errorf("规则 %s 至少需要一个动作", rule.RuleKey) } for _, action := range rule.Actions { operation, _ := action["operation"].(string) field, _ := action["field"].(string) if operation != "set" && operation != "append" { return fmt.Errorf("规则 %s 使用了不支持的动作 %s", rule.RuleKey, operation) } if !fieldKeyPattern.MatchString(field) { return fmt.Errorf("规则 %s 的动作目标字段格式不正确", rule.RuleKey) } valuePresent := false if _, exists := action["value"]; exists { valuePresent = true } if source, exists := action["value_from"]; exists { value, ok := source.(string) if !ok || !contextFieldPattern(value) { return fmt.Errorf("规则 %s 的 value_from 格式不正确", rule.RuleKey) } valuePresent = true } if !valuePresent { return fmt.Errorf("规则 %s 的动作缺少 value", rule.RuleKey) } } } return nil } func validateContractCondition(value interface{}, depth int) error { if depth > 12 { return fmt.Errorf("条件嵌套层级过深") } rule, ok := value.(map[string]interface{}) if !ok { return fmt.Errorf("条件必须是对象") } groups := 0 for _, key := range []string{"all", "any"} { if raw, exists := rule[key]; exists { groups++ items, ok := raw.([]interface{}) if !ok || len(items) == 0 || len(items) > 100 { return fmt.Errorf("%s 条件必须是非空数组且最多包含 100 项", key) } for _, item := range items { if err := validateContractCondition(item, depth+1); err != nil { return err } } } } if groups > 1 { return fmt.Errorf("条件不能同时包含 all 和 any") } if groups == 1 { return nil } field, _ := rule["field"].(string) operator, _ := rule["operator"].(string) allowedOperators := map[string]bool{"equals": true, "not_equals": true, "contains": true, "greater_than": true, "less_than": true, "exists": true, "not_exists": true, "in": true} if !contextFieldPattern(field) { return fmt.Errorf("条件字段格式不正确") } if !allowedOperators[operator] { return fmt.Errorf("不支持的条件运算符 %s", operator) } if operator != "exists" && operator != "not_exists" { if _, exists := rule["value"]; !exists { return fmt.Errorf("条件缺少 value") } } return nil } func contextFieldPattern(value string) bool { if fieldKeyPattern.MatchString(value) { return true } for _, prefix := range []string{"input.", "derived.", "form."} { if strings.HasPrefix(value, prefix) && fieldKeyPattern.MatchString(strings.TrimPrefix(value, prefix)) { return true } } return false } func contractScenarioID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.Error(c, http.StatusBadRequest, "INVALID_ID", "场景 ID 不正确") return 0, false } return id, true }