后端: 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永远只适合做选择题、判断题,不适合做开放创新题。
219 lines
6.8 KiB
Go
219 lines
6.8 KiB
Go
package conv
|
||
|
||
import (
|
||
"errors"
|
||
"time"
|
||
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/LoveLosita/smartflow/backend/respond"
|
||
)
|
||
|
||
const dateLayout = "2006-01-02"
|
||
|
||
func parseDatePtr(s string) (*time.Time, error) {
|
||
if s == "" {
|
||
return nil, nil
|
||
}
|
||
t, err := time.ParseInLocation(dateLayout, s, time.Local)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &t, nil
|
||
}
|
||
|
||
func ProcessUserAddTaskClassRequest(req *model.UserAddTaskClassRequest, userID int) (*model.TaskClass, []model.TaskClassItem, error) {
|
||
startDate, err := parseDatePtr(req.StartDate)
|
||
if err != nil {
|
||
return nil, nil, respond.WrongParamType
|
||
}
|
||
endDate, err := parseDatePtr(req.EndDate)
|
||
if err != nil {
|
||
return nil, nil, respond.WrongParamType
|
||
}
|
||
//1.填充section1,2
|
||
taskClass := model.TaskClass{
|
||
Name: &req.Name,
|
||
Mode: &req.Mode,
|
||
StartDate: startDate,
|
||
EndDate: endDate,
|
||
SubjectType: stringPtrOrNil(req.SubjectType),
|
||
DifficultyLevel: stringPtrOrNil(req.DifficultyLevel),
|
||
CognitiveIntensity: stringPtrOrNil(req.CognitiveIntensity),
|
||
UserID: &userID,
|
||
}
|
||
//2.填充section3
|
||
taskClass.TotalSlots = &req.Config.TotalSlots
|
||
taskClass.AllowFillerCourse = &req.Config.AllowFillerCourse
|
||
taskClass.Strategy = &req.Config.Strategy
|
||
/*//处理 ExcludedSlots 切片为 JSON 字符串
|
||
if len(req.Config.ExcludedSlots) > 0 {
|
||
//转换为 JSON 字符串
|
||
excludedSlotsJSON := "["
|
||
for i, slot := range req.Config.ExcludedSlots {
|
||
excludedSlotsJSON += string(rune(slot + '0')) //简单转换为字符
|
||
if i != len(req.Config.ExcludedSlots)-1 {
|
||
excludedSlotsJSON += ","
|
||
}
|
||
}
|
||
excludedSlotsJSON += "]"
|
||
taskClass.ExcludedSlots = &excludedSlotsJSON
|
||
} else {
|
||
emptyJSON := "[]"
|
||
taskClass.ExcludedSlots = &emptyJSON
|
||
}*/
|
||
taskClass.ExcludedSlots = req.Config.ExcludedSlots // 直接复用 IntSlice 类型,前端也能正确解析为 []int
|
||
taskClass.ExcludedDaysOfWeek = req.Config.ExcludedDaysOfWeek
|
||
//3.开始构建 items
|
||
var items []model.TaskClassItem
|
||
for _, itemReq := range req.Items {
|
||
item := model.TaskClassItem{ //填充section 2
|
||
Order: &itemReq.Order,
|
||
Content: &itemReq.Content,
|
||
EmbeddedTime: itemReq.EmbeddedTime,
|
||
Status: nil,
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
return &taskClass, items, nil
|
||
}
|
||
|
||
func timeOrZero(t *time.Time) time.Time {
|
||
if t == nil {
|
||
return time.Time{}
|
||
}
|
||
return *t
|
||
}
|
||
|
||
func TaskClassModelToResponse(taskClasses []model.TaskClass) *model.UserGetTaskClassesResponse {
|
||
var resp model.UserGetTaskClassesResponse
|
||
for _, tc := range taskClasses {
|
||
tcResp := model.TaskClassSummary{
|
||
ID: tc.ID,
|
||
Name: *tc.Name,
|
||
Mode: *tc.Mode,
|
||
StartDate: timeOrZero(tc.StartDate),
|
||
EndDate: timeOrZero(tc.EndDate),
|
||
TotalSlots: *tc.TotalSlots,
|
||
Strategy: *tc.Strategy,
|
||
SubjectType: safeStr(tc.SubjectType),
|
||
DifficultyLevel: safeStr(tc.DifficultyLevel),
|
||
CognitiveIntensity: safeStr(tc.CognitiveIntensity),
|
||
}
|
||
resp.TaskClasses = append(resp.TaskClasses, tcResp)
|
||
}
|
||
return &resp
|
||
}
|
||
|
||
func ProcessUserGetCompleteTaskClassRequest(taskClass *model.TaskClass) (*model.UserAddTaskClassRequest, error) {
|
||
if taskClass == nil {
|
||
return nil, errors.New("源数据对象不可为空")
|
||
}
|
||
// 1. 映射基础信息 (处理指针解引用)
|
||
req := &model.UserAddTaskClassRequest{
|
||
Name: safeStr(taskClass.Name),
|
||
Mode: safeStr(taskClass.Mode),
|
||
StartDate: formatTime(taskClass.StartDate),
|
||
EndDate: formatTime(taskClass.EndDate),
|
||
SubjectType: safeStr(taskClass.SubjectType),
|
||
DifficultyLevel: safeStr(taskClass.DifficultyLevel),
|
||
CognitiveIntensity: safeStr(taskClass.CognitiveIntensity),
|
||
}
|
||
// 2. 映射配置信息 (Config Section)
|
||
req.Config = model.UserAddTaskClassConfig{
|
||
TotalSlots: safeInt(taskClass.TotalSlots),
|
||
AllowFillerCourse: safeBool(taskClass.AllowFillerCourse),
|
||
Strategy: safeStr(taskClass.Strategy),
|
||
}
|
||
/*// 3. 处理 ExcludedSlots JSON 字符串 -> []int
|
||
if taskClass.ExcludedSlots != nil && *taskClass.ExcludedSlots != "" {
|
||
var excluded []int
|
||
// 直接使用标准反序列化,比手动处理 rune 字符要健壮得多
|
||
if err := json.Unmarshal([]byte(*taskClass.ExcludedSlots), &excluded); err == nil {
|
||
req.Config.ExcludedSlots = excluded
|
||
}
|
||
}*/
|
||
req.Config.ExcludedSlots = taskClass.ExcludedSlots // 直接复用 IntSlice 类型,前端也能正确解析为 []int
|
||
req.Config.ExcludedDaysOfWeek = taskClass.ExcludedDaysOfWeek
|
||
// 4. 映射子项信息 (Items Section)
|
||
// 此时 items 已经通过 Preload 加载到了 taskClass.Items 中
|
||
req.Items = make([]model.UserAddTaskClassItemRequest, 0, len(taskClass.Items))
|
||
for _, item := range taskClass.Items {
|
||
itemReq := model.UserAddTaskClassItemRequest{
|
||
ID: item.ID, // 填充数据库主键 ID,前端拖拽编排依赖此字段
|
||
Order: safeInt(item.Order),
|
||
Content: safeStr(item.Content),
|
||
EmbeddedTime: item.EmbeddedTime, // 结构体指针直接复用
|
||
}
|
||
req.Items = append(req.Items, itemReq)
|
||
}
|
||
return req, nil
|
||
}
|
||
|
||
// UserInsertTaskItemRequestToModel 用于将填入空闲时段日程的请求转换为 Schedule 模型
|
||
func UserInsertTaskItemRequestToModel(req *model.UserInsertTaskClassItemToScheduleRequest, item *model.TaskClassItem, taskID *int, userID, startSection, endSection int) ([]model.Schedule, *model.ScheduleEvent, error) {
|
||
var schedules []model.Schedule
|
||
for section := startSection; section <= endSection; section++ {
|
||
req1 := &model.Schedule{
|
||
UserID: userID,
|
||
EmbeddedTaskID: taskID,
|
||
Week: req.Week,
|
||
DayOfWeek: req.DayOfWeek,
|
||
Section: section,
|
||
Status: "normal",
|
||
}
|
||
schedules = append(schedules, *req1)
|
||
}
|
||
startTime, endTime, err := RelativeTimeToRealTime(req.Week, req.DayOfWeek, startSection, endSection)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
req2 := &model.ScheduleEvent{
|
||
UserID: userID, // 由调用方填充
|
||
Name: safeStr(item.Content), // 任务内容作为事件名称
|
||
Type: "task",
|
||
RelID: &item.ID, // 关联到 TaskClassItem 的 ID
|
||
CanBeEmbedded: false, // 任务事件允许嵌入其他任务(如果需要的话)
|
||
StartTime: startTime,
|
||
EndTime: endTime,
|
||
}
|
||
return schedules, req2, nil
|
||
}
|
||
|
||
// --- 🛡️ 辅助工具函数:保持代码清爽并防止 Panic ---
|
||
|
||
func safeStr(s *string) string {
|
||
if s == nil {
|
||
return ""
|
||
}
|
||
return *s
|
||
}
|
||
|
||
func safeInt(i *int) int {
|
||
if i == nil {
|
||
return 0
|
||
}
|
||
return *i
|
||
}
|
||
|
||
func stringPtrOrNil(value string) *string {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return &value
|
||
}
|
||
|
||
func safeBool(b *bool) bool {
|
||
if b == nil {
|
||
return true
|
||
}
|
||
return *b
|
||
}
|
||
|
||
func formatTime(t *time.Time) string {
|
||
if t == nil {
|
||
return ""
|
||
}
|
||
// 务必使用 2006-01-02 格式以匹配前端校验
|
||
return t.Format("2006-01-02")
|
||
}
|