package newagenttools import ( "encoding/json" "fmt" "strings" "time" "github.com/LoveLosita/smartflow/backend/model" ) // TaskClassUpsertInput 描述任务类写库工具的标准化入参。 // // 职责边界: // 1. ID=0 表示创建,ID>0 表示更新; // 2. Request 直接复用现有 UserAddTaskClassRequest 语义,避免多套字段定义漂移; // 3. Source 用于记录字段来源(chat/memory/web),不参与业务校验。 type TaskClassUpsertInput struct { ID int Request model.UserAddTaskClassRequest Source string } // TaskClassUpsertPersistResult 描述任务类写入持久层后的结果。 type TaskClassUpsertPersistResult struct { TaskClassID int Created bool } // TaskClassWriteDeps 描述任务类写库工具依赖。 // // 职责边界: // 1. 工具层只负责参数标准化与结果包装,不直接依赖 DAO; // 2. UpsertTaskClass 由启动层注入,便于后续替换为 service/DAO 统一实现。 type TaskClassWriteDeps struct { UpsertTaskClass func(userID int, input TaskClassUpsertInput) (TaskClassUpsertPersistResult, error) } type taskClassValidationResult struct { OK bool `json:"ok"` Issues []string `json:"issues"` } type taskClassUpsertToolResult struct { Tool string `json:"tool"` Success bool `json:"success"` TaskClassID int `json:"task_class_id,omitempty"` Created bool `json:"created,omitempty"` Validation taskClassValidationResult `json:"validation"` Error string `json:"error"` ErrorCode string `json:"error_code"` } func parseTaskClassUpsertInput(args map[string]any) (TaskClassUpsertInput, error) { id := 0 if rawID, exists := args["id"]; exists { parsedID, ok := readUpsertInt(rawID) if !ok { return TaskClassUpsertInput{}, fmt.Errorf("id 参数类型非法,必须为整数") } id = parsedID } taskClassRaw, ok := args["task_class"] if !ok { return TaskClassUpsertInput{}, fmt.Errorf("缺少必填参数 task_class") } taskClassMap, ok := taskClassRaw.(map[string]any) if !ok { return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数类型非法,必须是对象") } // 允许顶层 items 覆盖 task_class.items,便于 LLM 在生成参数时拆分表达。 if rawItems, exists := args["items"]; exists { taskClassMap["items"] = rawItems } normalizeTaskClassPayload(taskClassMap) rawJSON, err := json.Marshal(taskClassMap) if err != nil { return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数序列化失败") } var request model.UserAddTaskClassRequest if err := json.Unmarshal(rawJSON, &request); err != nil { return TaskClassUpsertInput{}, fmt.Errorf("task_class 参数解析失败:%v", err) } source := "" if rawSource, exists := args["source"]; exists { if text, ok := rawSource.(string); ok { source = strings.TrimSpace(text) } } normalizeTaskClassSemanticRequest(&request) return TaskClassUpsertInput{ ID: id, Request: request, Source: source, }, nil } // normalizeTaskClassPayload 对 LLM 常见“近义字段/错层字段”做轻量兼容归一化。 // // 职责边界: // 1. 负责把已知等价字段映射到后端真实契约,减少“明明填了却校验失败”的误伤; // 2. 不负责兜底补齐业务必填项(如 mode/config),这些仍由校验层决定是否报错; // 3. 仅处理本工具已观测到的高频偏差,避免过度“自动纠错”掩盖真实输入问题。 func normalizeTaskClassPayload(taskClassMap map[string]any) { if len(taskClassMap) == 0 { return } // 1. 兼容日期字段错层: // 1.1 若顶层 start_date/end_date 缺失; // 1.2 且 config.start_date/config.end_date 有值; // 1.3 则抬升到顶层,匹配 UserAddTaskClassRequest 契约。 configMap, _ := readAnyMap(taskClassMap["config"]) promoteStringField(taskClassMap, configMap, "start_date") promoteStringField(taskClassMap, configMap, "end_date") // 2. 兼容 items 的语义字段: // 2.1 content 缺失时,尝试从 description/title/name 回填; // 2.2 order 缺失或非法时,按当前顺序补 1..N; // 2.3 失败时不抛错,留给校验层输出明确问题。 normalizeTaskClassItems(taskClassMap) } func promoteStringField(top map[string]any, config map[string]any, key string) { if top == nil { return } if strings.TrimSpace(readAnyString(top[key])) != "" { return } if strings.TrimSpace(readAnyString(config[key])) == "" { return } top[key] = strings.TrimSpace(readAnyString(config[key])) } func normalizeTaskClassItems(taskClassMap map[string]any) { rawItems, exists := taskClassMap["items"] if !exists { return } itemList, ok := rawItems.([]any) if !ok { return } for idx := range itemList { itemMap, ok := itemList[idx].(map[string]any) if !ok { continue } if !hasPositiveInt(itemMap["order"]) { itemMap["order"] = idx + 1 } if strings.TrimSpace(readAnyString(itemMap["content"])) == "" { content := firstNonEmptyString( readAnyString(itemMap["content"]), readAnyString(itemMap["description"]), readAnyString(itemMap["title"]), readAnyString(itemMap["name"]), ) if strings.TrimSpace(content) != "" { itemMap["content"] = strings.TrimSpace(content) } } itemList[idx] = itemMap } taskClassMap["items"] = itemList } func readAnyMap(raw any) (map[string]any, bool) { if raw == nil { return nil, false } value, ok := raw.(map[string]any) return value, ok } func readAnyString(raw any) string { switch value := raw.(type) { case string: return value default: return "" } } // normalizeTaskClassSemanticRequest 归一化任务类语义画像字段。 // // 职责边界: // 1. 负责把 LLM 或用户可能给出的中文/近义值收口成稳定枚举; // 2. 不负责补默认值,字段缺失仍由上层决定是否接受; // 3. 归一化失败时保留原值,交给校验层输出明确错误。 func normalizeTaskClassSemanticRequest(req *model.UserAddTaskClassRequest) { if req == nil { return } if normalized := normalizeSubjectType(req.SubjectType); normalized != "" { req.SubjectType = normalized } if normalized := normalizeLevelValue(req.DifficultyLevel); normalized != "" { req.DifficultyLevel = normalized } if normalized := normalizeLevelValue(req.CognitiveIntensity); normalized != "" { req.CognitiveIntensity = normalized } } func normalizeSubjectType(raw string) string { value := strings.TrimSpace(strings.ToLower(raw)) switch value { case "quantitative", "计算型", "计算", "理工", "理工型": return "quantitative" case "memory", "记忆型", "记忆", "背诵型", "背诵": return "memory" case "reading", "阅读型", "阅读": return "reading" case "mixed", "混合型", "混合": return "mixed" default: return "" } } func normalizeLevelValue(raw string) string { value := strings.TrimSpace(strings.ToLower(raw)) switch value { case "low", "低": return "low" case "medium", "中", "中等": return "medium" case "high", "高": return "high" default: return "" } } func firstNonEmptyString(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return value } } return "" } func hasPositiveInt(raw any) bool { switch value := raw.(type) { case int: return value > 0 case int8: return value > 0 case int16: return value > 0 case int32: return value > 0 case int64: return value > 0 case float32: return int(value) > 0 case float64: return int(value) > 0 default: return false } } func validateTaskClassUpsertRequest(req model.UserAddTaskClassRequest, id int) []string { issues := make([]string, 0) if id < 0 { issues = append(issues, "id 不能小于 0") } if strings.TrimSpace(req.Name) == "" { issues = append(issues, "name 不能为空") } mode := strings.TrimSpace(strings.ToLower(req.Mode)) if mode != "auto" && mode != "manual" { issues = append(issues, "mode 仅支持 auto/manual") } if mode == "auto" { if strings.TrimSpace(req.StartDate) == "" || strings.TrimSpace(req.EndDate) == "" { issues = append(issues, "auto 模式必须提供 start_date/end_date") } else { startDate, err1 := time.ParseInLocation("2006-01-02", strings.TrimSpace(req.StartDate), time.Local) endDate, err2 := time.ParseInLocation("2006-01-02", strings.TrimSpace(req.EndDate), time.Local) if err1 != nil || err2 != nil { issues = append(issues, "start_date/end_date 日期格式非法,需为 YYYY-MM-DD") } else if startDate.After(endDate) { issues = append(issues, "start_date 不能晚于 end_date") } } } if strings.TrimSpace(req.SubjectType) != "" && normalizeSubjectType(req.SubjectType) == "" { issues = append(issues, "subject_type 仅支持 quantitative/memory/reading/mixed") } if strings.TrimSpace(req.DifficultyLevel) != "" && normalizeLevelValue(req.DifficultyLevel) == "" { issues = append(issues, "difficulty_level 仅支持 low/medium/high") } if strings.TrimSpace(req.CognitiveIntensity) != "" && normalizeLevelValue(req.CognitiveIntensity) == "" { issues = append(issues, "cognitive_intensity 仅支持 low/medium/high") } if strings.TrimSpace(req.SubjectType) == "" { issues = append(issues, "subject_type 不能为空") } if strings.TrimSpace(req.DifficultyLevel) == "" { issues = append(issues, "difficulty_level 不能为空") } if strings.TrimSpace(req.CognitiveIntensity) == "" { issues = append(issues, "cognitive_intensity 不能为空") } if req.Config.TotalSlots <= 0 { issues = append(issues, "config.total_slots 必须大于 0") } strategy := strings.TrimSpace(strings.ToLower(req.Config.Strategy)) if strategy != "steady" && strategy != "rapid" { issues = append(issues, "config.strategy 仅支持 steady/rapid") } for _, section := range req.Config.ExcludedSlots { // 1. excluded_slots 在粗排算法中按“半天块索引”解释,而不是原子节次; // 2. 每个块固定映射 2 节:1->1-2,2->3-4,...,6->11-12; // 3. 若放行 7~12,会在 buildTimeGrid 扩展时生成 13~24 节,触发数组越界。 if section < 1 || section > 6 { issues = append(issues, "config.excluded_slots 仅允许 1~6(半天块索引,每块=2节)") break } } for _, dayOfWeek := range req.Config.ExcludedDaysOfWeek { // 1. excluded_days_of_week 属于“整天不可排”的硬约束; // 2. 仅允许 1~7,对应周一到周日; // 3. 非法值会导致粗排过滤口径失真,因此统一在写入口拦截。 if dayOfWeek < 1 || dayOfWeek > 7 { issues = append(issues, "config.excluded_days_of_week 仅允许 1~7(周一到周日)") break } } if len(req.Items) == 0 { issues = append(issues, "items 不能为空") } for index, item := range req.Items { if item.Order <= 0 { issues = append(issues, fmt.Sprintf("items[%d].order 必须大于 0", index)) } if strings.TrimSpace(item.Content) == "" { issues = append(issues, fmt.Sprintf("items[%d].content 不能为空", index)) } } return uniqueTaskClassIssues(issues) } func uniqueTaskClassIssues(issues []string) []string { if len(issues) == 0 { return issues } seen := make(map[string]struct{}, len(issues)) out := make([]string, 0, len(issues)) for _, issue := range issues { trimmed := strings.TrimSpace(issue) if trimmed == "" { continue } if _, ok := seen[trimmed]; ok { continue } seen[trimmed] = struct{}{} out = append(out, trimmed) } return out } func readUpsertUserID(raw any) (int, bool) { return readUpsertInt(raw) } func readUpsertInt(raw any) (int, bool) { switch value := raw.(type) { case int: return value, true case int8: return int(value), true case int16: return int(value), true case int32: return int(value), true case int64: return int(value), true case float64: return int(value), true case float32: return int(value), true default: return 0, false } } func marshalTaskClassUpsertResult(result taskClassUpsertToolResult) string { raw, err := json.Marshal(result) if err != nil { return `{"tool":"upsert_task_class","success":false,"error":"result encode failed","error_code":"encode_failed"}` } return string(raw) }