Version: 0.9.45.dev.260427

后端:
1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜
2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写
3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态
4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分
5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定

前端:
6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作

仓库:
7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件

PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
Losita
2026-04-27 01:09:37 +08:00
parent 04b5836b39
commit 66c06eed0a
60 changed files with 9163 additions and 1819 deletions

View File

@@ -0,0 +1,494 @@
package newagenttools
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/model"
"github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule"
)
// 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"`
}
// NewTaskClassUpsertToolHandler 创建 upsert_task_class 工具 handler。
//
// 职责边界:
// 1. 只做参数解析、合法性校验、调用依赖、返回统一 JSON
// 2. 不负责草案生成,草案由 prompt+LLM 完成;
// 3. 不依赖 ScheduleState可在纯聊天场景调用execute 会注入 _user_id
func NewTaskClassUpsertToolHandler(deps TaskClassWriteDeps) ToolHandler {
return func(state *schedule.ScheduleState, args map[string]any) string {
_ = state
if deps.UpsertTaskClass == nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"任务类写库依赖未注入"}},
Error: "任务类写库依赖未注入",
ErrorCode: "dependency_missing",
})
}
userID, ok := readUpsertUserID(args["_user_id"])
if !ok || userID <= 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"无法识别用户身份"}},
Error: "工具调用失败:无法识别用户身份",
ErrorCode: "missing_user_id",
})
}
input, parseErr := parseTaskClassUpsertInput(args)
if parseErr != nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{parseErr.Error()}},
Error: parseErr.Error(),
ErrorCode: "invalid_args",
})
}
issues := validateTaskClassUpsertRequest(input.Request, input.ID)
if len(issues) > 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: issues},
Error: strings.Join(issues, ""),
ErrorCode: "validation_failed",
})
}
result, err := deps.UpsertTaskClass(userID, input)
if err != nil {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"持久化写入失败"}},
Error: err.Error(),
ErrorCode: "persist_failed",
})
}
if result.TaskClassID <= 0 {
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: false,
Validation: taskClassValidationResult{OK: false, Issues: []string{"未返回有效 task_class_id"}},
Error: "写入后未返回有效 task_class_id",
ErrorCode: "invalid_persist_result",
})
}
return marshalTaskClassUpsertResult(taskClassUpsertToolResult{
Tool: "upsert_task_class",
Success: true,
TaskClassID: result.TaskClassID,
Created: result.Created,
Validation: taskClassValidationResult{OK: true, Issues: []string{}},
Error: "",
ErrorCode: "",
})
}
}
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-22->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)
}