From 070d4c34594c87628fef56b6c01156f3329006f6 Mon Sep 17 00:00:00 2001 From: Losita <2810873701@qq.com> Date: Sun, 12 Apr 2026 19:02:54 +0800 Subject: [PATCH] =?UTF-8?q?Version:=200.9.15.dev.260412=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=EF=BC=9A=201.=20=E6=8E=92=E7=A8=8B=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E4=BB=8E=20tools/=20=E6=A0=B9=E7=9B=AE=E5=BD=95=E6=8B=86?= =?UTF-8?q?=E5=88=86=E4=B8=BA=20tools/schedule=20=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=AD=90=E5=8C=85=20-=2012=20=E4=B8=AA=E6=8E=92=E7=A8=8B?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E6=96=87=E4=BB=B6=E7=AD=89=E4=BB=B7=E8=BF=81?= =?UTF-8?q?=E5=85=A5=20tools/schedule/=EF=BC=8Ctools/=20=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E4=BB=85=E4=BF=9D=E7=95=99=20registry.go=20=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E7=BB=9F=E4=B8=80=E6=B3=A8=E5=86=8C=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=20-=20=E6=89=80=E6=9C=89=E4=BE=9D=E8=B5=96=E6=96=B9=EF=BC=88co?= =?UTF-8?q?nv=20/=20model=20/=20node=20/=20prompt=20/=20service=EF=BC=89im?= =?UTF-8?q?port=20=E7=BB=9F=E4=B8=80=E5=88=87=E5=88=B0=20schedule=20?= =?UTF-8?q?=E5=AD=90=E5=8C=85=202.=20Web=20=E6=90=9C=E7=B4=A2=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E9=93=BE=E8=90=BD=E5=9C=B0=EF=BC=88tools/web=20?= =?UTF-8?q?=E5=AD=90=E5=8C=85=EF=BC=89=20-=20=E6=96=B0=E5=A2=9E=20web=5Fse?= =?UTF-8?q?arch=EF=BC=88=E7=BB=93=E6=9E=84=E5=8C=96=E6=A3=80=E7=B4=A2?= =?UTF-8?q?=EF=BC=89=E4=B8=8E=20web=5Ffetch=EF=BC=88=E6=AD=A3=E6=96=87?= =?UTF-8?q?=E6=8A=93=E5=8F=96=EF=BC=89=E4=B8=A4=E4=B8=AA=E8=AF=BB=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8D=9A=E6=9F=A5=20API=20?= =?UTF-8?q?/=20mock=20=E9=99=8D=E7=BA=A7=20-=20=E5=90=AF=E5=8A=A8=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E6=8C=89=E9=85=8D=E7=BD=AE=E9=80=89=E6=8B=A9=20provid?= =?UTF-8?q?er=EF=BC=8C=E6=9C=AA=E8=AF=86=E5=88=AB=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E9=99=8D=E7=BA=A7=E4=B8=BA=20mock=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E9=98=BB=E6=96=AD=E4=B8=BB=E6=B5=81=E7=A8=8B=20-=20?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E6=8F=90=E7=A4=BA=E8=A1=A5=E9=BD=90=20web=20?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E4=BD=BF=E7=94=A8=E7=BA=A6=E6=9D=9F=E4=B8=8E?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=80=BC=E7=A4=BA=E4=BE=8B=20-=20config.exam?= =?UTF-8?q?ple.yaml=20=E8=A1=A5=E9=BD=90=20websearch=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=AE=B5=20=E5=89=8D=E7=AB=AF=EF=BC=9A=E6=97=A0=20=E4=BB=93?= =?UTF-8?q?=E5=BA=93=EF=BC=9A=E6=97=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/cmd/start.go | 28 ++- backend/config.example.yaml | 5 + backend/newAgent/conv/schedule_persist.go | 8 +- backend/newAgent/conv/schedule_preview.go | 10 +- backend/newAgent/conv/schedule_provider.go | 14 +- backend/newAgent/conv/schedule_state.go | 58 ++--- backend/newAgent/model/common_state.go | 4 +- backend/newAgent/model/graph_run_state.go | 23 +- backend/newAgent/model/state_store.go | 18 +- backend/newAgent/node/agent_nodes.go | 4 +- backend/newAgent/node/execute.go | 29 +-- backend/newAgent/node/order_guard.go | 30 +-- backend/newAgent/node/rough_build.go | 22 +- backend/newAgent/prompt/execute.go | 8 +- backend/newAgent/prompt/execute_context.go | 4 + backend/newAgent/tools/registry.go | 127 ++++++---- .../tools/{ => schedule}/arg_guard.go | 2 +- backend/newAgent/tools/{ => schedule}/args.go | 24 +- .../tools/{ => schedule}/compound_tools.go | 16 +- .../tools/{ => schedule}/queue_tools.go | 8 +- .../tools/{ => schedule}/read_filter_tools.go | 8 +- .../tools/{ => schedule}/read_helpers.go | 2 +- .../tools/{ => schedule}/read_tools.go | 2 +- .../tools/{ => schedule}/runtime_queue.go | 2 +- .../newAgent/tools/{ => schedule}/state.go | 2 +- .../newAgent/tools/{ => schedule}/status.go | 2 +- .../tools/{ => schedule}/write_helpers.go | 2 +- .../tools/{ => schedule}/write_tools.go | 2 +- backend/newAgent/tools/web/fetcher.go | 175 ++++++++++++++ backend/newAgent/tools/web/provider.go | 82 +++++++ backend/newAgent/tools/web/provider_bocha.go | 217 +++++++++++++++++ backend/newAgent/tools/web/provider_mock.go | 58 +++++ backend/newAgent/tools/web/tools.go | 227 ++++++++++++++++++ backend/service/agentsvc/agent_newagent.go | 15 +- 34 files changed, 1033 insertions(+), 205 deletions(-) rename backend/newAgent/tools/{ => schedule}/arg_guard.go (98%) rename backend/newAgent/tools/{ => schedule}/args.go (80%) rename backend/newAgent/tools/{ => schedule}/compound_tools.go (98%) rename backend/newAgent/tools/{ => schedule}/queue_tools.go (98%) rename backend/newAgent/tools/{ => schedule}/read_filter_tools.go (99%) rename backend/newAgent/tools/{ => schedule}/read_helpers.go (99%) rename backend/newAgent/tools/{ => schedule}/read_tools.go (99%) rename backend/newAgent/tools/{ => schedule}/runtime_queue.go (99%) rename backend/newAgent/tools/{ => schedule}/state.go (99%) rename backend/newAgent/tools/{ => schedule}/status.go (99%) rename backend/newAgent/tools/{ => schedule}/write_helpers.go (99%) rename backend/newAgent/tools/{ => schedule}/write_tools.go (99%) create mode 100644 backend/newAgent/tools/web/fetcher.go create mode 100644 backend/newAgent/tools/web/provider.go create mode 100644 backend/newAgent/tools/web/provider_bocha.go create mode 100644 backend/newAgent/tools/web/provider_mock.go create mode 100644 backend/newAgent/tools/web/tools.go diff --git a/backend/cmd/start.go b/backend/cmd/start.go index 868b3a5..bb5f97f 100644 --- a/backend/cmd/start.go +++ b/backend/cmd/start.go @@ -17,6 +17,7 @@ import ( "github.com/LoveLosita/smartflow/backend/middleware" newagentconv "github.com/LoveLosita/smartflow/backend/newAgent/conv" newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/web" "github.com/LoveLosita/smartflow/backend/pkg" "github.com/LoveLosita/smartflow/backend/routers" "github.com/LoveLosita/smartflow/backend/service" @@ -139,8 +140,33 @@ func Start() { // newAgent 依赖接线。 agentService.SetAgentStateStore(dao.NewAgentStateStoreAdapter(cacheRepo)) + + // 1. WebSearch provider 初始化:根据配置选择 mock/bocha; + // 2. provider 为 nil 时,web_search / web_fetch 返回"暂未启用",不阻断主流程。 + var webSearchProvider web.SearchProvider + webProvider := viper.GetString("websearch.provider") + switch webProvider { + case "bocha": + bochaKey := viper.GetString("websearch.apiKey") + if bochaKey == "" { + log.Println("WebSearch: 博查 API Key 为空,降级为 mock") + webSearchProvider = &web.MockProvider{} + } else { + webSearchProvider = web.NewBochaProvider(bochaKey, "") + log.Println("WebSearch provider: bocha") + } + case "mock", "": + webSearchProvider = &web.MockProvider{} + log.Println("WebSearch provider: mock(模拟模式)") + default: + // 未识别的 provider 类型降级为 mock 并输出警告。 + log.Printf("WebSearch provider %q 未识别,降级为 mock", webProvider) + webSearchProvider = &web.MockProvider{} + } + agentService.SetToolRegistry(newagenttools.NewDefaultRegistryWithDeps(newagenttools.DefaultRegistryDeps{ - RAGRuntime: ragRuntime, + RAGRuntime: ragRuntime, + WebSearchProvider: webSearchProvider, })) agentService.SetScheduleProvider(newagentconv.NewScheduleProvider(scheduleRepo, taskClassRepo)) agentService.SetSchedulePersistor(newagentconv.NewSchedulePersistorAdapter(manager)) diff --git a/backend/config.example.yaml b/backend/config.example.yaml index 6ebcf67..88b2e59 100644 --- a/backend/config.example.yaml +++ b/backend/config.example.yaml @@ -103,5 +103,10 @@ memory: claimBatch: 1 websearch: + provider: mock # 可选:mock | bocha(mock 为空实现,跑通链路用) + apiKey: "" # 搜索供应商 API Key(bocha 模式必填,否则降级为 mock) + timeout: 10s # 单次搜索请求超时 + fetchTimeout: 15s # 单次 URL 抓取超时 + fetchMaxChars: 4000 # 抓取正文最大字符数 rag: enabled: false diff --git a/backend/newAgent/conv/schedule_persist.go b/backend/newAgent/conv/schedule_persist.go index 8de630a..c874718 100644 --- a/backend/newAgent/conv/schedule_persist.go +++ b/backend/newAgent/conv/schedule_persist.go @@ -7,7 +7,7 @@ import ( baseconv "github.com/LoveLosita/smartflow/backend/conv" "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // SchedulePersistorAdapter 实现 model.SchedulePersistor 接口。 @@ -22,7 +22,7 @@ func NewSchedulePersistorAdapter(manager *dao.RepoManager) *SchedulePersistorAda } // PersistScheduleChanges 实现 model.SchedulePersistor 接口。 -func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, original, modified *newagenttools.ScheduleState, userID int) error { +func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, original, modified *schedule.ScheduleState, userID int) error { return PersistScheduleChanges(ctx, a.manager, original, modified, userID) } @@ -35,8 +35,8 @@ func (a *SchedulePersistorAdapter) PersistScheduleChanges(ctx context.Context, o func PersistScheduleChanges( ctx context.Context, manager *dao.RepoManager, - original *newagenttools.ScheduleState, - modified *newagenttools.ScheduleState, + original *schedule.ScheduleState, + modified *schedule.ScheduleState, userID int, ) error { changes := DiffScheduleState(original, modified) diff --git a/backend/newAgent/conv/schedule_preview.go b/backend/newAgent/conv/schedule_preview.go index d955e92..46d3b84 100644 --- a/backend/newAgent/conv/schedule_preview.go +++ b/backend/newAgent/conv/schedule_preview.go @@ -5,7 +5,7 @@ import ( "time" "github.com/LoveLosita/smartflow/backend/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // ScheduleStateToPreview 将 newAgent 的 ScheduleState 转换为前端预览缓存格式。 @@ -16,7 +16,7 @@ import ( // 3. Day → (Week, DayOfWeek) 通过 ScheduleState.DayToWeekDay 转换; // 4. 转换失败的 slot(day_index 无效)静默跳过。 func ScheduleStateToPreview( - state *newagenttools.ScheduleState, + state *schedule.ScheduleState, userID int, conversationID string, taskClassIDs []int, @@ -30,7 +30,7 @@ func ScheduleStateToPreview( for i := range state.Tasks { t := &state.Tasks[i] // 待安排且无位置的任务不生成 entry。 - if newagenttools.IsPendingTask(*t) { + if schedule.IsPendingTask(*t) { continue } @@ -116,6 +116,6 @@ func ScheduleStateToPreview( // 1. 新语义下,显式 suggested 直接输出为建议态; // 2. 兼容旧快照:pending+Slots、existing+Duration>0 的 task_item 也继续按 suggested 输出; // 3. 这样前端预览口径可以在迁移期保持稳定,不会因为状态枚举切换而抖动。 -func shouldMarkSuggestedInPreview(t newagenttools.ScheduleTask) bool { - return newagenttools.IsSuggestedTask(t) +func shouldMarkSuggestedInPreview(t schedule.ScheduleTask) bool { + return schedule.IsSuggestedTask(t) } diff --git a/backend/newAgent/conv/schedule_provider.go b/backend/newAgent/conv/schedule_provider.go index d1c6d2b..2fe4927 100644 --- a/backend/newAgent/conv/schedule_provider.go +++ b/backend/newAgent/conv/schedule_provider.go @@ -9,7 +9,7 @@ import ( baseconv "github.com/LoveLosita/smartflow/backend/conv" "github.com/LoveLosita/smartflow/backend/dao" "github.com/LoveLosita/smartflow/backend/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // ScheduleProvider 实现 model.ScheduleStateProvider 接口。 @@ -39,7 +39,7 @@ func NewScheduleProvider(scheduleDAO *dao.ScheduleDAO, taskClassDAO *dao.TaskCla // 2. task class 无日期信息时,降级到当前周 7 天(兼容普通查询场景)。 // // 日程加载策略:对窗口内每周分别调用 GetUserWeeklySchedule 并合并结果。 -func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) { +func (p *ScheduleProvider) LoadScheduleState(ctx context.Context, userID int) (*schedule.ScheduleState, error) { // 1. 加载用户所有任务类(含 Items 预加载)。 taskClasses, err := p.loadCompleteTaskClasses(ctx, userID) if err != nil { @@ -59,7 +59,7 @@ func (p *ScheduleProvider) LoadScheduleStateForTaskClasses( ctx context.Context, userID int, taskClassIDs []int, -) (*newagenttools.ScheduleState, error) { +) (*schedule.ScheduleState, error) { if len(taskClassIDs) == 0 { return p.LoadScheduleState(ctx, userID) } @@ -82,7 +82,7 @@ func (p *ScheduleProvider) loadScheduleStateWithTaskClasses( ctx context.Context, userID int, taskClasses []model.TaskClass, -) (*newagenttools.ScheduleState, error) { +) (*schedule.ScheduleState, error) { // 1. 确定规划窗口:优先使用 task class 日期范围,降级到当前周。 windowDays, weeks := buildWindowFromTaskClasses(taskClasses) if len(windowDays) == 0 { @@ -236,7 +236,7 @@ func (p *ScheduleProvider) loadCompleteTaskClassesByIDs( } // LoadTaskClassMetas 加载指定任务类的约束元数据(不含 Items、不含日程),供 Plan 阶段提前消费。 -func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]newagenttools.TaskClassMeta, error) { +func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]schedule.TaskClassMeta, error) { if len(taskClassIDs) == 0 { return nil, nil } @@ -244,9 +244,9 @@ func (p *ScheduleProvider) LoadTaskClassMetas(ctx context.Context, userID int, t if err != nil { return nil, fmt.Errorf("加载任务类元数据失败: %w", err) } - metas := make([]newagenttools.TaskClassMeta, 0, len(complete)) + metas := make([]schedule.TaskClassMeta, 0, len(complete)) for _, tc := range complete { - meta := newagenttools.TaskClassMeta{ + meta := schedule.TaskClassMeta{ ID: tc.ID, Name: derefString(tc.Name), } diff --git a/backend/newAgent/conv/schedule_state.go b/backend/newAgent/conv/schedule_state.go index a33b3ba..6d0b80a 100644 --- a/backend/newAgent/conv/schedule_state.go +++ b/backend/newAgent/conv/schedule_state.go @@ -4,7 +4,7 @@ import ( "sort" "github.com/LoveLosita/smartflow/backend/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // WindowDay 表示排课窗口中的一天(相对周 + 周几)。 @@ -24,20 +24,20 @@ func LoadScheduleState( taskClasses []model.TaskClass, extraItemCategories map[int]string, windowDays []WindowDay, -) *newagenttools.ScheduleState { - state := &newagenttools.ScheduleState{ - Window: newagenttools.ScheduleWindow{ +) *schedule.ScheduleState { + state := &schedule.ScheduleState{ + Window: schedule.ScheduleWindow{ TotalDays: len(windowDays), - DayMapping: make([]newagenttools.DayMapping, len(windowDays)), + DayMapping: make([]schedule.DayMapping, len(windowDays)), }, - Tasks: make([]newagenttools.ScheduleTask, 0), + Tasks: make([]schedule.ScheduleTask, 0), } // 1. 构建 day_index 与 (week, day_of_week) 的双向转换基础索引。 dayLookup := make(map[[2]int]int, len(windowDays)) for i, wd := range windowDays { dayIndex := i + 1 - state.Window.DayMapping[i] = newagenttools.DayMapping{ + state.Window.DayMapping[i] = schedule.DayMapping{ DayIndex: dayIndex, Week: wd.Week, DayOfWeek: wd.DayOfWeek, @@ -118,7 +118,7 @@ func LoadScheduleState( } locked := event.Type == "course" && !event.CanBeEmbedded - var slots []newagenttools.TaskSlot + var slots []schedule.TaskSlot for _, g := range groups { if len(g.sections) == 0 { continue @@ -131,12 +131,12 @@ func LoadScheduleState( continue } if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok { - slots = append(slots, newagenttools.TaskSlot{Day: day, SlotStart: start, SlotEnd: end}) + slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end}) } start, end = sec, sec } if day, ok := dayLookup[[2]int{g.week, g.dayOfWeek}]; ok { - slots = append(slots, newagenttools.TaskSlot{Day: day, SlotStart: start, SlotEnd: end}) + slots = append(slots, schedule.TaskSlot{Day: day, SlotStart: start, SlotEnd: end}) } } sort.Slice(slots, func(i, j int) bool { @@ -147,7 +147,7 @@ func LoadScheduleState( }) stateID := nextStateID - state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{ + state.Tasks = append(state.Tasks, schedule.ScheduleTask{ StateID: stateID, Source: "event", SourceID: eventID, @@ -207,12 +207,12 @@ func LoadScheduleState( } if hostStateID, ok := itemIDToEmbedHostStateID[item.ID]; ok { - hostSlots := []newagenttools.TaskSlot(nil) + hostSlots := []schedule.TaskSlot(nil) if hostTask := state.TaskByStateID(hostStateID); hostTask != nil { hostSlots = cloneTaskSlots(hostTask.Slots) } stateID := nextStateID - state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{ + state.Tasks = append(state.Tasks, schedule.ScheduleTask{ StateID: stateID, Source: "task_item", SourceID: item.ID, @@ -230,7 +230,7 @@ func LoadScheduleState( if slots, ok := slotsFromTargetTime(item.EmbeddedTime, dayLookup); ok { stateID := nextStateID - state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{ + state.Tasks = append(state.Tasks, schedule.ScheduleTask{ StateID: stateID, Source: "task_item", SourceID: item.ID, @@ -251,7 +251,7 @@ func LoadScheduleState( } stateID := nextStateID - state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{ + state.Tasks = append(state.Tasks, schedule.ScheduleTask{ StateID: stateID, Source: "task_item", SourceID: item.ID, @@ -269,7 +269,7 @@ func LoadScheduleState( // 仅当该任务类仍有 pending item 时,才把约束暴露给 LLM。 if pendingCount > 0 { - meta := newagenttools.TaskClassMeta{ + meta := schedule.TaskClassMeta{ ID: tc.ID, Name: catName, } @@ -328,12 +328,12 @@ func LoadScheduleState( if cat, exists := itemCategoryLookup[itemID]; exists && cat != "" { category = cat } - hostSlots := []newagenttools.TaskSlot(nil) + hostSlots := []schedule.TaskSlot(nil) if hostTask != nil { hostSlots = cloneTaskSlots(hostTask.Slots) } guestStateID = nextStateID - state.Tasks = append(state.Tasks, newagenttools.ScheduleTask{ + state.Tasks = append(state.Tasks, schedule.ScheduleTask{ StateID: guestStateID, Source: "task_item", SourceID: itemID, @@ -412,7 +412,7 @@ func taskItemName(item model.TaskClassItem) string { func slotsFromTargetTime( target *model.TargetTime, dayLookup map[[2]int]int, -) ([]newagenttools.TaskSlot, bool) { +) ([]schedule.TaskSlot, bool) { if target == nil { return nil, false } @@ -423,7 +423,7 @@ func slotsFromTargetTime( if !ok { return nil, false } - return []newagenttools.TaskSlot{ + return []schedule.TaskSlot{ { Day: day, SlotStart: target.SectionFrom, @@ -471,8 +471,8 @@ type ScheduleChange struct { // DiffScheduleState 比较 original 与 modified,返回需要持久化的变更集合。 func DiffScheduleState( - original *newagenttools.ScheduleState, - modified *newagenttools.ScheduleState, + original *schedule.ScheduleState, + modified *schedule.ScheduleState, ) []ScheduleChange { if original == nil || modified == nil { return nil @@ -534,8 +534,8 @@ func DiffScheduleState( } // indexByStateID 将任务列表按 state_id 建立索引。 -func indexByStateID(state *newagenttools.ScheduleState) map[int]*newagenttools.ScheduleTask { - m := make(map[int]*newagenttools.ScheduleTask, len(state.Tasks)) +func indexByStateID(state *schedule.ScheduleState) map[int]*schedule.ScheduleTask { + m := make(map[int]*schedule.ScheduleTask, len(state.Tasks)) for i := range state.Tasks { m[state.Tasks[i].StateID] = &state.Tasks[i] } @@ -543,7 +543,7 @@ func indexByStateID(state *newagenttools.ScheduleState) map[int]*newagenttools.S } // slotsEqual 判断两个压缩槽位切片是否完全一致。 -func slotsEqual(a, b []newagenttools.TaskSlot) bool { +func slotsEqual(a, b []schedule.TaskSlot) bool { if len(a) != len(b) { return false } @@ -556,18 +556,18 @@ func slotsEqual(a, b []newagenttools.TaskSlot) bool { } // cloneTaskSlots 深拷贝槽位切片。 -func cloneTaskSlots(src []newagenttools.TaskSlot) []newagenttools.TaskSlot { +func cloneTaskSlots(src []schedule.TaskSlot) []schedule.TaskSlot { if len(src) == 0 { return nil } - dst := make([]newagenttools.TaskSlot, len(src)) + dst := make([]schedule.TaskSlot, len(src)) copy(dst, src) return dst } // resolveHostEventID 通过任务的 EmbedHost 反查宿主 event_id。 // 非嵌入任务或宿主不存在时返回 0。 -func resolveHostEventID(task *newagenttools.ScheduleTask, state *newagenttools.ScheduleState) int { +func resolveHostEventID(task *schedule.ScheduleTask, state *schedule.ScheduleState) int { if task == nil || task.EmbedHost == nil { return 0 } @@ -579,7 +579,7 @@ func resolveHostEventID(task *newagenttools.ScheduleTask, state *newagenttools.S } // expandToCoords 将压缩槽位展开成逐节坐标,便于后续持久化层处理。 -func expandToCoords(slots []newagenttools.TaskSlot, state *newagenttools.ScheduleState) []SlotCoord { +func expandToCoords(slots []schedule.TaskSlot, state *schedule.ScheduleState) []SlotCoord { var coords []SlotCoord for _, slot := range slots { week, dow, ok := state.DayToWeekDay(slot.Day) diff --git a/backend/newAgent/model/common_state.go b/backend/newAgent/model/common_state.go index 1e7b256..2e9a661 100644 --- a/backend/newAgent/model/common_state.go +++ b/backend/newAgent/model/common_state.go @@ -3,7 +3,7 @@ package model import ( "strings" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // Phase 表示 agent 主循环当前所处的大阶段。 @@ -92,7 +92,7 @@ type CommonState struct { TaskClassIDs []int `json:"task_class_ids,omitempty"` // TaskClasses 本次排课涉及的任务类约束元数据(含日期、策略、时段预算等), // 在 Service 层从 DB 加载并注入,供 Plan prompt 直接消费,避免 LLM 因信息不足而追问用户。 - TaskClasses []newagenttools.TaskClassMeta `json:"task_classes,omitempty"` + TaskClasses []schedule.TaskClassMeta `json:"task_classes,omitempty"` // NeedsRoughBuild 由 Plan 节点在 plan_done 时写入,标记 Confirm 后是否需要走粗排节点。 // 粗排节点执行完毕后会将此字段重置为 false。 diff --git a/backend/newAgent/model/graph_run_state.go b/backend/newAgent/model/graph_run_state.go index 87c91f6..dec26d5 100644 --- a/backend/newAgent/model/graph_run_state.go +++ b/backend/newAgent/model/graph_run_state.go @@ -7,6 +7,7 @@ import ( infrallm "github.com/LoveLosita/smartflow/backend/infra/llm" newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream" newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // AgentGraphRequest 描述一次 agent graph 运行的请求级输入。 @@ -48,7 +49,7 @@ type RoughBuildFunc func(ctx context.Context, userID int, taskClassIDs []int) ([ // 由 service 层封装 cacheDAO 后注入,execute/deliver 节点可按需调用: // 1. execute 写工具后可实时刷新,保障前端及时看到最新调整; // 2. deliver 结束时再做最终覆盖写,保障收口状态一致。 -type WriteSchedulePreviewFunc func(ctx context.Context, state *newagenttools.ScheduleState, userID int, conversationID string, taskClassIDs []int) error +type WriteSchedulePreviewFunc func(ctx context.Context, state *schedule.ScheduleState, userID int, conversationID string, taskClassIDs []int) error // AgentGraphDeps 描述 graph/node 层运行时真正依赖的可插拔能力。 // @@ -151,8 +152,8 @@ func (d *AgentGraphDeps) ResolveDeliverClient() *infrallm.Client { type AgentGraphRunInput struct { RuntimeState *AgentRuntimeState ConversationContext *ConversationContext - ScheduleState *newagenttools.ScheduleState - OriginalScheduleState *newagenttools.ScheduleState + ScheduleState *schedule.ScheduleState + OriginalScheduleState *schedule.ScheduleState Request AgentGraphRequest Deps AgentGraphDeps } @@ -168,8 +169,8 @@ type AgentGraphState struct { ConversationContext *ConversationContext Request AgentGraphRequest Deps AgentGraphDeps - ScheduleState *newagenttools.ScheduleState // 工具操作的内存数据源,Execute 节点按需加载 - OriginalScheduleState *newagenttools.ScheduleState // 首次加载时的原始快照,供 diff 用 + ScheduleState *schedule.ScheduleState // 工具操作的内存数据源,Execute 节点按需加载 + OriginalScheduleState *schedule.ScheduleState // 首次加载时的原始快照,供 diff 用 } // NewAgentGraphState 把入口参数整理成 graph 内部状态。 @@ -239,7 +240,7 @@ func (s *AgentGraphState) ResolveToolRegistry() *newagenttools.ToolRegistry { // EnsureScheduleState 确保 ScheduleState 已加载。 // 首次调用时通过 ScheduleProvider 从 DB 加载,后续复用内存中的 state。 -func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttools.ScheduleState, error) { +func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*schedule.ScheduleState, error) { if s == nil { return nil, nil } @@ -251,8 +252,8 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo // 3. 因此这里在“已恢复出 ScheduleState、但缺 original”时补一份克隆兜底。 s.OriginalScheduleState = s.ScheduleState.Clone() } - newagenttools.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs) - newagenttools.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs) + schedule.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs) + schedule.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs) return s.ScheduleState, nil } if s.Deps.ScheduleProvider == nil { @@ -260,7 +261,7 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo } userID := flowState.UserID var ( - state *newagenttools.ScheduleState + state *schedule.ScheduleState err error ) // 1. 若 provider 支持按 task_class_ids 精确加载,则优先走 scoped 入口。 @@ -277,7 +278,7 @@ func (s *AgentGraphState) EnsureScheduleState(ctx context.Context) (*newagenttoo s.ScheduleState = state // 保存原始快照,供后续 diff 使用。 s.OriginalScheduleState = state.Clone() - newagenttools.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs) - newagenttools.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs) + schedule.FilterScheduleStateForTaskClassScope(s.ScheduleState, flowState.TaskClassIDs) + schedule.FilterScheduleStateForTaskClassScope(s.OriginalScheduleState, flowState.TaskClassIDs) return state, nil } diff --git a/backend/newAgent/model/state_store.go b/backend/newAgent/model/state_store.go index 07de4f6..334dba8 100644 --- a/backend/newAgent/model/state_store.go +++ b/backend/newAgent/model/state_store.go @@ -3,7 +3,7 @@ package model import ( "context" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + schedule "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // AgentStateSnapshot 是需要持久化的 agent 运行态最小快照。 @@ -14,10 +14,10 @@ import ( // 3. 不保存 Deps(依赖注入,每次由 Service 层重建); // 4. 不保存 ToolSchemas(每次请求由 Service 层重新注入)。 type AgentStateSnapshot struct { - RuntimeState *AgentRuntimeState `json:"runtime_state"` - ConversationContext *ConversationContext `json:"conversation_context"` - ScheduleState *newagenttools.ScheduleState `json:"schedule_state,omitempty"` - OriginalScheduleState *newagenttools.ScheduleState `json:"original_schedule_state,omitempty"` + RuntimeState *AgentRuntimeState `json:"runtime_state"` + ConversationContext *ConversationContext `json:"conversation_context"` + ScheduleState *schedule.ScheduleState `json:"schedule_state,omitempty"` + OriginalScheduleState *schedule.ScheduleState `json:"original_schedule_state,omitempty"` } // AgentStateStore 定义 agent 状态持久化的最小接口。 @@ -58,9 +58,9 @@ type AgentStateStore interface { // 由 DAO 层或 Service 层实现,注入到 AgentGraphDeps 中。 // 使用接口而非具体 DAO 类型,避免 model → dao 的循环依赖。 type ScheduleStateProvider interface { - LoadScheduleState(ctx context.Context, userID int) (*newagenttools.ScheduleState, error) + LoadScheduleState(ctx context.Context, userID int) (*schedule.ScheduleState, error) // LoadTaskClassMetas 只加载指定任务类的约束元数据,供 Plan 节点提前消费。 - LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]newagenttools.TaskClassMeta, error) + LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]schedule.TaskClassMeta, error) } // ScopedScheduleStateProvider 定义“按本轮任务类范围加载 ScheduleState”的可选增强接口。 @@ -70,12 +70,12 @@ type ScheduleStateProvider interface { // 2. 不负责:改变既有 ScheduleStateProvider 的基础能力,老实现仍可只实现 LoadScheduleState; // 3. 兜底策略:若调用方拿到的 provider 不实现该接口,则回退到全量 LoadScheduleState,再走工具层 scope 裁剪。 type ScopedScheduleStateProvider interface { - LoadScheduleStateForTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (*newagenttools.ScheduleState, error) + LoadScheduleStateForTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (*schedule.ScheduleState, error) } // SchedulePersistor 定义持久化 ScheduleState 变更的接口。 // 由 Service 层或 DAO 层实现,注入到 AgentGraphDeps 中。 // 使用接口而非具体 DAO 类型,避免 model → dao 的循环依赖。 type SchedulePersistor interface { - PersistScheduleChanges(ctx context.Context, original, modified *newagenttools.ScheduleState, userID int) error + PersistScheduleChanges(ctx context.Context, original, modified *schedule.ScheduleState, userID int) error } diff --git a/backend/newAgent/node/agent_nodes.go b/backend/newAgent/node/agent_nodes.go index d6d08ff..b8e890b 100644 --- a/backend/newAgent/node/agent_nodes.go +++ b/backend/newAgent/node/agent_nodes.go @@ -7,7 +7,7 @@ import ( "log" newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) // AgentNodes 是 newAgent 通用图的节点容器。 @@ -185,7 +185,7 @@ func (n *AgentNodes) Execute(ctx context.Context, st *newagentmodel.AgentGraphSt } // 按需加载 ScheduleState(首次执行时从 DB 加载,后续复用内存中的 state)。 - var scheduleState *newagenttools.ScheduleState + var scheduleState *schedule.ScheduleState if ss, loadErr := st.EnsureScheduleState(ctx); loadErr != nil { return nil, fmt.Errorf("execute node: 加载日程状态失败: %w", loadErr) } else if ss != nil { diff --git a/backend/newAgent/node/execute.go b/backend/newAgent/node/execute.go index b083e3d..49d57c1 100644 --- a/backend/newAgent/node/execute.go +++ b/backend/newAgent/node/execute.go @@ -15,6 +15,7 @@ import ( newagentprompt "github.com/LoveLosita/smartflow/backend/newAgent/prompt" newagentstream "github.com/LoveLosita/smartflow/backend/newAgent/stream" newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" "github.com/cloudwego/eino/schema" "github.com/google/uuid" ) @@ -52,10 +53,10 @@ type ExecuteNodeInput struct { ChunkEmitter *newagentstream.ChunkEmitter ResumeNode string ToolRegistry *newagenttools.ToolRegistry - ScheduleState *newagenttools.ScheduleState + ScheduleState *schedule.ScheduleState SchedulePersistor newagentmodel.SchedulePersistor WriteSchedulePreview newagentmodel.WriteSchedulePreviewFunc - OriginalScheduleState *newagenttools.ScheduleState + OriginalScheduleState *schedule.ScheduleState AlwaysExecute bool // true 时写工具跳过确认闸门直接执行 } @@ -127,7 +128,7 @@ func RunExecuteNode(ctx context.Context, input ExecuteNodeInput) error { // 1. RoundUsed==0 说明当前还未消耗执行预算; // 2. 此时清理不会影响断线恢复中的中间进度(恢复场景通常 RoundUsed>0)。 if input.ScheduleState != nil && flowState.RoundUsed == 0 { - newagenttools.ResetTaskProcessingQueue(input.ScheduleState) + schedule.ResetTaskProcessingQueue(input.ScheduleState) } if !flowState.AllowReorder && len(flowState.SuggestedOrderBaseline) == 0 { flowState.SuggestedOrderBaseline = buildSuggestedOrderSnapshot(input.ScheduleState) @@ -978,7 +979,7 @@ func renderExecuteStepScope(scope *executeStepScope) string { return strings.Join(parts, ",") } -func buildScopeDaySet(state *newagenttools.ScheduleState, scope *executeStepScope) map[int]struct{} { +func buildScopeDaySet(state *schedule.ScheduleState, scope *executeStepScope) map[int]struct{} { result := make(map[int]struct{}, 16) if state == nil || scope == nil { return result @@ -991,7 +992,7 @@ func buildScopeDaySet(state *newagenttools.ScheduleState, scope *executeStepScop return result } -func dayMatchesScope(state *newagenttools.ScheduleState, scope *executeStepScope, day int) bool { +func dayMatchesScope(state *schedule.ScheduleState, scope *executeStepScope, day int) bool { if state == nil || scope == nil { return true } @@ -1016,7 +1017,7 @@ func dayMatchesScope(state *newagenttools.ScheduleState, scope *executeStepScope return true } -func estimateCandidateDaysFromArgs(state *newagenttools.ScheduleState, args map[string]any) (map[int]struct{}, bool, error) { +func estimateCandidateDaysFromArgs(state *schedule.ScheduleState, args map[string]any) (map[int]struct{}, bool, error) { result := make(map[int]struct{}, 16) if state == nil { return result, false, fmt.Errorf("日程状态为空") @@ -1328,7 +1329,7 @@ func executeToolCall( toolCall *newagentmodel.ToolCallIntent, emitter *newagentstream.ChunkEmitter, registry *newagenttools.ToolRegistry, - scheduleState *newagenttools.ScheduleState, + scheduleState *schedule.ScheduleState, writePreview newagentmodel.WriteSchedulePreviewFunc, ) error { if toolCall == nil { @@ -1454,9 +1455,9 @@ func executePendingTool( runtimeState *newagentmodel.AgentRuntimeState, conversationContext *newagentmodel.ConversationContext, registry *newagenttools.ToolRegistry, - scheduleState *newagenttools.ScheduleState, + scheduleState *schedule.ScheduleState, persistor newagentmodel.SchedulePersistor, - originalState *newagenttools.ScheduleState, + originalState *schedule.ScheduleState, writePreview newagentmodel.WriteSchedulePreviewFunc, emitter *newagentstream.ChunkEmitter, ) error { @@ -1539,7 +1540,7 @@ func executePendingTool( func tryWritePreviewAfterWriteTool( ctx context.Context, flowState *newagentmodel.CommonState, - scheduleState *newagenttools.ScheduleState, + scheduleState *schedule.ScheduleState, registry *newagenttools.ToolRegistry, toolName string, writePreview newagentmodel.WriteSchedulePreviewFunc, @@ -1601,7 +1602,7 @@ func truncateText(text string, maxLen int) string { } // summarizeScheduleStateForDebug 返回内存日程状态的关键计数,用于判断工具是否真的修改了 state。 -func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string { +func summarizeScheduleStateForDebug(state *schedule.ScheduleState) string { if state == nil { return "state=nil" } @@ -1618,11 +1619,11 @@ func summarizeScheduleStateForDebug(state *newagenttools.ScheduleState) string { hasSlot := len(t.Slots) > 0 switch { - case newagenttools.IsPendingTask(*t): + case schedule.IsPendingTask(*t): pendingNoSlot++ - case newagenttools.IsSuggestedTask(*t): + case schedule.IsSuggestedTask(*t): suggestedTotal++ - case newagenttools.IsExistingTask(*t): + case schedule.IsExistingTask(*t): existingTotal++ } diff --git a/backend/newAgent/node/order_guard.go b/backend/newAgent/node/order_guard.go index e9ba083..21932f9 100644 --- a/backend/newAgent/node/order_guard.go +++ b/backend/newAgent/node/order_guard.go @@ -8,7 +8,7 @@ import ( "strings" newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) const ( @@ -21,7 +21,7 @@ type suggestedOrderItem struct { Day int SlotStart int SlotEnd int - Slots []newagenttools.TaskSlot + Slots []schedule.TaskSlot } type orderRestoreResult struct { @@ -126,7 +126,7 @@ func RunOrderGuardNode(ctx context.Context, st *newagentmodel.AgentGraphState) e // 1. 这里只关心 suggested 任务,因为顺序守卫目标是约束“本轮建议层”的相对次序; // 2. 多 slot 任务取“最早 slot”作为排序锚点,保证排序键稳定; // 3. 返回值是 state_id 列表,便于写入 CommonState 做跨节点持久化。 -func buildSuggestedOrderSnapshot(state *newagenttools.ScheduleState) []int { +func buildSuggestedOrderSnapshot(state *schedule.ScheduleState) []int { items := buildSuggestedOrderItems(state) order := make([]int, 0, len(items)) for _, item := range items { @@ -141,7 +141,7 @@ func buildSuggestedOrderSnapshot(state *newagenttools.ScheduleState) []int { // 1. 统一封装顺序守卫和自动复原都需要的排序素材,避免两处逻辑口径漂移; // 2. 排序键保持与历史实现一致:day -> slot_start -> slot_end -> state_id; // 3. 每项附带完整 slots 快照,供“坑位复用式复原”直接使用。 -func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrderItem { +func buildSuggestedOrderItems(state *schedule.ScheduleState) []suggestedOrderItem { if state == nil || len(state.Tasks) == 0 { return nil } @@ -149,7 +149,7 @@ func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrd items := make([]suggestedOrderItem, 0, len(state.Tasks)) for i := range state.Tasks { task := state.Tasks[i] - if !newagenttools.IsSuggestedTask(task) || len(task.Slots) == 0 { + if !schedule.IsSuggestedTask(task) || len(task.Slots) == 0 { continue } day, slotStart, slotEnd := earliestTaskSlot(task.Slots) @@ -178,7 +178,7 @@ func buildSuggestedOrderItems(state *newagenttools.ScheduleState) []suggestedOrd return items } -func earliestTaskSlot(slots []newagenttools.TaskSlot) (day int, slotStart int, slotEnd int) { +func earliestTaskSlot(slots []schedule.TaskSlot) (day int, slotStart int, slotEnd int) { if len(slots) == 0 { return 0, 0, 0 } @@ -250,7 +250,7 @@ func detectRelativeOrderViolation(baseline []int, current []int) (bool, string) // 2. 复用 current 的“坑位序列”(时段集合),按 baseline 顺序重新回填任务; // 3. 回填前校验时长兼容,避免把长任务塞进短坑位; // 4. 回填后再次校验顺序;若失败则回滚,保证状态不会半成功。 -func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baseline []int) orderRestoreResult { +func restoreSuggestedOrderByBaseline(state *schedule.ScheduleState, baseline []int) orderRestoreResult { if state == nil { return orderRestoreResult{Restored: false, Detail: "schedule_state=nil"} } @@ -306,7 +306,7 @@ func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baselin } // 1. 先构建“当前坑位序列”。 - slotPool := make([][]newagenttools.TaskSlot, 0, len(filteredCurrent)) + slotPool := make([][]schedule.TaskSlot, 0, len(filteredCurrent)) for _, currentID := range filteredCurrent { item, ok := itemByID[currentID] if !ok { @@ -343,7 +343,7 @@ func restoreSuggestedOrderByBaseline(state *newagenttools.ScheduleState, baselin } // 3. 执行回填,并在失败时支持回滚。 - beforeSlots := make(map[int][]newagenttools.TaskSlot, len(baselineInScope)) + beforeSlots := make(map[int][]schedule.TaskSlot, len(baselineInScope)) changed := 0 for i, targetID := range baselineInScope { task := state.TaskByStateID(targetID) @@ -401,16 +401,16 @@ func sameIDOrder(left, right []int) bool { return true } -func cloneTaskSlots(slots []newagenttools.TaskSlot) []newagenttools.TaskSlot { +func cloneTaskSlots(slots []schedule.TaskSlot) []schedule.TaskSlot { if len(slots) == 0 { return nil } - copied := make([]newagenttools.TaskSlot, len(slots)) + copied := make([]schedule.TaskSlot, len(slots)) copy(copied, slots) return copied } -func equalTaskSlots(left, right []newagenttools.TaskSlot) bool { +func equalTaskSlots(left, right []schedule.TaskSlot) bool { if len(left) != len(right) { return false } @@ -428,7 +428,7 @@ func equalTaskSlots(left, right []newagenttools.TaskSlot) bool { return true } -func expectedTaskDuration(task newagenttools.ScheduleTask) int { +func expectedTaskDuration(task schedule.ScheduleTask) int { if task.Duration > 0 { return task.Duration } @@ -438,7 +438,7 @@ func expectedTaskDuration(task newagenttools.ScheduleTask) int { return 0 } -func totalSlotDuration(slots []newagenttools.TaskSlot) int { +func totalSlotDuration(slots []schedule.TaskSlot) int { total := 0 for _, slot := range slots { total += slot.SlotEnd - slot.SlotStart + 1 @@ -446,7 +446,7 @@ func totalSlotDuration(slots []newagenttools.TaskSlot) int { return total } -func isSlotsCompatibleWithTask(task newagenttools.ScheduleTask, slots []newagenttools.TaskSlot) bool { +func isSlotsCompatibleWithTask(task schedule.ScheduleTask, slots []schedule.TaskSlot) bool { if len(slots) == 0 { return false } diff --git a/backend/newAgent/node/rough_build.go b/backend/newAgent/node/rough_build.go index 9856d29..ccdb58c 100644 --- a/backend/newAgent/node/rough_build.go +++ b/backend/newAgent/node/rough_build.go @@ -8,7 +8,7 @@ import ( "strings" newagentmodel "github.com/LoveLosita/smartflow/backend/newAgent/model" - newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" ) const ( @@ -205,20 +205,20 @@ func RunRoughBuildNode(ctx context.Context, st *newagentmodel.AgentGraphState) e // 1. 第一轮修复后,粗排成功会把任务直接标记为 suggested; // 2. 为兼容旧快照,仍按“pending 且 Slots 为空”认定真正未覆盖; // 3. 只要这里仍大于 0,就应视为粗排异常,而不是交给 LLM 补排。 -func countPendingTasks(state *newagenttools.ScheduleState, taskClassIDs []int) int { +func countPendingTasks(state *schedule.ScheduleState, taskClassIDs []int) int { if state == nil { return 0 } count := 0 for i := range state.Tasks { task := state.Tasks[i] - if !newagenttools.IsPendingTask(task) { + if !schedule.IsPendingTask(task) { continue } - if len(taskClassIDs) > 0 && !newagenttools.IsTaskInRequestedClassScope(task, taskClassIDs) { + if len(taskClassIDs) > 0 && !schedule.IsTaskInRequestedClassScope(task, taskClassIDs) { continue } - if newagenttools.IsPendingTask(task) { + if schedule.IsPendingTask(task) { count++ } } @@ -234,7 +234,7 @@ func countPendingTasks(state *newagenttools.ScheduleState, taskClassIDs []int) i // 4. suggested 表示“粗排建议位”,后续可用 move/swap/unplace 微调; // 5. 转换失败的条目静默跳过,不中断整体流程。 func applyRoughBuildPlacements( - state *newagenttools.ScheduleState, + state *schedule.ScheduleState, placements []newagentmodel.RoughBuildPlacement, ) roughBuildApplyStats { stats := roughBuildApplyStats{} @@ -262,10 +262,10 @@ func applyRoughBuildPlacements( matched := false for _, index := range taskIndexByItemID[p.TaskItemID] { t := &state.Tasks[index] - t.Slots = []newagenttools.TaskSlot{ + t.Slots = []schedule.TaskSlot{ {Day: day, SlotStart: p.SectionFrom, SlotEnd: p.SectionTo}, } - t.Status = newagenttools.TaskStatusSuggested + t.Status = schedule.TaskStatusSuggested stats.AppliedCount++ matched = true break @@ -294,7 +294,7 @@ func appendPlacementSample(samples []string, placement newagentmodel.RoughBuildP } // summarizeRoughBuildWindow 提供 DayMapping 的紧凑摘要,便于判断窗口是否退化到错误周。 -func summarizeRoughBuildWindow(state *newagenttools.ScheduleState) string { +func summarizeRoughBuildWindow(state *schedule.ScheduleState) string { if state == nil || len(state.Window.DayMapping) == 0 { return "empty" } @@ -311,7 +311,7 @@ func summarizeRoughBuildWindow(state *newagenttools.ScheduleState) string { } // collectScopedTaskSamples 提供当前 state 中可用于匹配的 task_item 样本,便于排查 ID 对不上。 -func collectScopedTaskSamples(state *newagenttools.ScheduleState, taskClassIDs []int) []string { +func collectScopedTaskSamples(state *schedule.ScheduleState, taskClassIDs []int) []string { if state == nil { return nil } @@ -321,7 +321,7 @@ func collectScopedTaskSamples(state *newagenttools.ScheduleState, taskClassIDs [ if task.Source != "task_item" { continue } - if len(taskClassIDs) > 0 && !newagenttools.IsTaskInRequestedClassScope(task, taskClassIDs) { + if len(taskClassIDs) > 0 && !schedule.IsTaskInRequestedClassScope(task, taskClassIDs) { continue } samples = append(samples, fmt.Sprintf( diff --git a/backend/newAgent/prompt/execute.go b/backend/newAgent/prompt/execute.go index ab738dc..684a152 100644 --- a/backend/newAgent/prompt/execute.go +++ b/backend/newAgent/prompt/execute.go @@ -34,6 +34,8 @@ const executeSystemPromptWithPlan = ` 12. 不要把超过 2 条任务打包到 batch_move;大批量调整请改走队列逐项处理。 13. 不要在未获取队首(queue_pop_head)时直接调用 queue_apply_head_move。 14. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 + 15. web_search 仅在“制定学习计划需要查外部资料”时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 + 16. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 执行规则: 1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。 @@ -41,8 +43,8 @@ const executeSystemPromptWithPlan = ` 3. 写操作:action=confirm + tool_call。 4. 缺关键上下文且无法通过工具补齐:action=ask_user。 5. 仅当当前步骤完成时输出 action=next_plan,并在 goal_check 对照 done_when 给出证据。 -6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 -7. 流程应正式终止时输出 action=abort。` + 6. 仅当整体任务完成时输出 action=done,并在 goal_check 总结完成证据。 + 7. 流程应正式终止时输出 action=abort。` const executeSystemPromptReAct = ` 你是 SmartFlow NewAgent 的执行器,当前处于自由执行模式(无预定义 plan 步骤)。 @@ -75,6 +77,8 @@ const executeSystemPromptReAct = ` 11. 不要在同一轮构造大规模 batch_move;batch_move 最多 2 条,超过请走队列逐项处理。 12. 未调用 queue_pop_head 获取 current 前,不要调用 queue_apply_head_move。 13. 工具参数必须严格使用 schema 字段,禁止自造别名;例如 day_from/day_to 非法,必须改用 day_start/day_end。 +14. web_search 仅在"制定学习计划需要查外部资料"时使用(如考试日期、课程信息、校历政策等);日程排布本身(place/move/swap)不需要搜索。 +15. web_search 拿到 summary 后通常已够用;仅当需要页面详细内容时才调用 web_fetch。 执行规则: 1. 只输出严格 JSON,不要输出 markdown,不要在 JSON 外补充文本。 diff --git a/backend/newAgent/prompt/execute_context.go b/backend/newAgent/prompt/execute_context.go index dfa14bc..e9902de 100644 --- a/backend/newAgent/prompt/execute_context.go +++ b/backend/newAgent/prompt/execute_context.go @@ -507,6 +507,10 @@ func renderExecuteToolReturnHint(toolName string) (returnType string, sample str return returnType, "最少上下文切换重排完成:共处理 6 个任务,上下文切换次数 5 -> 2。" case "unplace": return returnType, "已将 [35]... 移除,恢复为待安排状态。" + case "web_search": + return "string(JSON字符串)", `{"tool":"web_search","query":"检索关键词","count":2,"items":[{"title":"搜索结果标题","url":"https://example.com/page","snippet":"摘要片段...","domain":"example.com","published_at":"2025-04-10"}]}` + case "web_fetch": + return "string(JSON字符串)", `{"tool":"web_fetch","url":"https://example.com/page","title":"页面标题","content":"正文内容...","truncated":false}` default: return returnType, "自然语言结果(成功/失败原因/关键数据摘要)。" } diff --git a/backend/newAgent/tools/registry.go b/backend/newAgent/tools/registry.go index 7bee1e3..95741f6 100644 --- a/backend/newAgent/tools/registry.go +++ b/backend/newAgent/tools/registry.go @@ -6,10 +6,12 @@ import ( "strings" infrarag "github.com/LoveLosita/smartflow/backend/infra/rag" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/schedule" + "github.com/LoveLosita/smartflow/backend/newAgent/tools/web" ) // ToolHandler 是所有工具的统一执行签名。 -type ToolHandler func(state *ScheduleState, args map[string]any) string +type ToolHandler func(state *schedule.ScheduleState, args map[string]any) string // ToolSchemaEntry 是注入给模型的工具说明快照。 type ToolSchemaEntry struct { @@ -26,6 +28,9 @@ type ToolSchemaEntry struct { // 3. 后续新增读工具时,应优先在这里扩展依赖而不是走包级全局变量。 type DefaultRegistryDeps struct { RAGRuntime infrarag.Runtime + + // WebSearchProvider Web 搜索供应商。为 nil 时 web_search / web_fetch 返回"暂未启用",不阻断主流程。 + WebSearchProvider web.SearchProvider } // ToolRegistry 管理工具注册、查找与执行。 @@ -60,7 +65,7 @@ func (r *ToolRegistry) Register(name, desc, schemaText string, handler ToolHandl } // Execute 执行指定工具。 -func (r *ToolRegistry) Execute(state *ScheduleState, toolName string, args map[string]any) string { +func (r *ToolRegistry) Execute(state *schedule.ScheduleState, toolName string, args map[string]any) string { handler, ok := r.handlers[toolName] if !ok { return fmt.Sprintf("工具调用失败:未知工具 %q。可用工具:%s", toolName, strings.Join(r.ToolNames(), "、")) @@ -123,72 +128,72 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry { r.Register("get_overview", "获取规划窗口总览(任务视角,全量返回):保留课程占位统计,展开任务清单(过滤课程明细)。", `{"name":"get_overview","parameters":{}}`, - func(state *ScheduleState, args map[string]any) string { - return GetOverview(state) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.GetOverview(state) }, ) r.Register("query_range", "查看某天或某时段的细粒度占用详情。day 必填,slot_start/slot_end 选填(不填查整天)。", `{"name":"query_range","parameters":{"day":{"type":"int","required":true},"slot_start":{"type":"int"},"slot_end":{"type":"int"}}}`, - func(state *ScheduleState, args map[string]any) string { - day, ok := argsInt(args, "day") + func(state *schedule.ScheduleState, args map[string]any) string { + day, ok := schedule.ArgsInt(args, "day") if !ok { return "查询失败:缺少必填参数 day。" } - return QueryRange(state, day, argsIntPtr(args, "slot_start"), argsIntPtr(args, "slot_end")) + return schedule.QueryRange(state, day, schedule.ArgsIntPtr(args, "slot_start"), schedule.ArgsIntPtr(args, "slot_end")) }, ) r.Register("query_available_slots", "查询候选空位池(先返回纯空位,不足再补可嵌入位),适合 move 前的落点筛选。", `{"name":"query_available_slots","parameters":{"span":{"type":"int"},"duration":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"},"section_from":{"type":"int"},"section_to":{"type":"int"}}}`, - func(state *ScheduleState, args map[string]any) string { - return QueryAvailableSlots(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueryAvailableSlots(state, args) }, ) r.Register("query_target_tasks", "查询候选任务集合,可按 status/week/day/task_id/category 筛选;默认自动入队,供后续 queue_pop_head 逐项处理。", `{"name":"query_target_tasks","parameters":{"status":{"type":"string","enum":["all","existing","suggested","pending"]},"category":{"type":"string"},"limit":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"task_ids":{"type":"array","items":{"type":"int"}},"task_id":{"type":"int"},"task_item_ids":{"type":"array","items":{"type":"int"}},"task_item_id":{"type":"int"},"enqueue":{"type":"bool"},"reset_queue":{"type":"bool"}}}`, - func(state *ScheduleState, args map[string]any) string { - return QueryTargetTasks(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueryTargetTasks(state, args) }, ) r.Register("queue_pop_head", "弹出并返回当前队首任务;若已有 current 则复用,保证一次只处理一个任务。", `{"name":"queue_pop_head","parameters":{}}`, - func(state *ScheduleState, args map[string]any) string { - return QueuePopHead(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueuePopHead(state, args) }, ) r.Register("queue_status", "查看当前待处理队列状态(pending/current/completed/skipped)。", `{"name":"queue_status","parameters":{}}`, - func(state *ScheduleState, args map[string]any) string { - return QueueStatus(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueueStatus(state, args) }, ) r.Register("list_tasks", "列出任务清单,可按类别和状态过滤。category 传任务类名称,status 仅支持单值 all/existing/suggested/pending。", `{"name":"list_tasks","parameters":{"category":{"type":"string"},"status":{"type":"string","enum":["all","existing","suggested","pending"]}}}`, - func(state *ScheduleState, args map[string]any) string { - return ListTasks(state, argsStringPtr(args, "category"), argsStringPtr(args, "status")) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.ListTasks(state, schedule.ArgsStringPtr(args, "category"), schedule.ArgsStringPtr(args, "status")) }, ) r.Register("get_task_info", "查询单个任务详细信息,包括类别、状态、占用时段、嵌入关系。", `{"name":"get_task_info","parameters":{"task_id":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - taskID, ok := argsInt(args, "task_id") + func(state *schedule.ScheduleState, args map[string]any) string { + taskID, ok := schedule.ArgsInt(args, "task_id") if !ok { return "查询失败:缺少必填参数 task_id。" } - return GetTaskInfo(state, taskID) + return schedule.GetTaskInfo(state, taskID) }, ) @@ -196,120 +201,142 @@ func NewDefaultRegistryWithDeps(deps DefaultRegistryDeps) *ToolRegistry { r.Register("place", "将一个待安排任务预排到指定位置。自动检测可嵌入宿主。task_id/day/slot_start 必填。", `{"name":"place","parameters":{"task_id":{"type":"int","required":true},"day":{"type":"int","required":true},"slot_start":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - taskID, ok := argsInt(args, "task_id") + func(state *schedule.ScheduleState, args map[string]any) string { + taskID, ok := schedule.ArgsInt(args, "task_id") if !ok { return "放置失败:缺少必填参数 task_id。" } - day, ok := argsInt(args, "day") + day, ok := schedule.ArgsInt(args, "day") if !ok { return "放置失败:缺少必填参数 day。" } - slotStart, ok := argsInt(args, "slot_start") + slotStart, ok := schedule.ArgsInt(args, "slot_start") if !ok { return "放置失败:缺少必填参数 slot_start。" } - return Place(state, taskID, day, slotStart) + return schedule.Place(state, taskID, day, slotStart) }, ) r.Register("move", "将一个已预排任务(仅 suggested)移动到新位置。existing 属于已安排事实层,不参与 move。task_id/new_day/new_slot_start 必填。", `{"name":"move","parameters":{"task_id":{"type":"int","required":true},"new_day":{"type":"int","required":true},"new_slot_start":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - taskID, ok := argsInt(args, "task_id") + func(state *schedule.ScheduleState, args map[string]any) string { + taskID, ok := schedule.ArgsInt(args, "task_id") if !ok { return "移动失败:缺少必填参数 task_id。" } - newDay, ok := argsInt(args, "new_day") + newDay, ok := schedule.ArgsInt(args, "new_day") if !ok { return "移动失败:缺少必填参数 new_day。" } - newSlotStart, ok := argsInt(args, "new_slot_start") + newSlotStart, ok := schedule.ArgsInt(args, "new_slot_start") if !ok { return "移动失败:缺少必填参数 new_slot_start。" } - return Move(state, taskID, newDay, newSlotStart) + return schedule.Move(state, taskID, newDay, newSlotStart) }, ) r.Register("swap", "交换两个已落位任务的位置。两个任务必须时长相同。task_a/task_b 必填。", `{"name":"swap","parameters":{"task_a":{"type":"int","required":true},"task_b":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - taskA, ok := argsInt(args, "task_a") + func(state *schedule.ScheduleState, args map[string]any) string { + taskA, ok := schedule.ArgsInt(args, "task_a") if !ok { return "交换失败:缺少必填参数 task_a。" } - taskB, ok := argsInt(args, "task_b") + taskB, ok := schedule.ArgsInt(args, "task_b") if !ok { return "交换失败:缺少必填参数 task_b。" } - return Swap(state, taskA, taskB) + return schedule.Swap(state, taskA, taskB) }, ) r.Register("batch_move", "原子性批量移动多个任务(仅 suggested,最多2条),全部成功才生效。若含 existing/pending 或任一冲突将整批失败回滚。", `{"name":"batch_move","parameters":{"moves":{"type":"array","required":true,"items":{"task_id":"int","new_day":"int","new_slot_start":"int"}}}}`, - func(state *ScheduleState, args map[string]any) string { - moves, err := argsMoveList(args) + func(state *schedule.ScheduleState, args map[string]any) string { + moves, err := schedule.ArgsMoveList(args) if err != nil { return fmt.Sprintf("批量移动失败:%s", err.Error()) } - return BatchMove(state, moves) + return schedule.BatchMove(state, moves) }, ) r.Register("queue_apply_head_move", "将当前队首任务移动到指定位置并自动出队。仅作用于 current,不接受 task_id。new_day/new_slot_start 必填。", `{"name":"queue_apply_head_move","parameters":{"new_day":{"type":"int","required":true},"new_slot_start":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - return QueueApplyHeadMove(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueueApplyHeadMove(state, args) }, ) r.Register("queue_skip_head", "跳过当前队首任务(不改日程),将其标记为 skipped 并继续后续队列。", `{"name":"queue_skip_head","parameters":{"reason":{"type":"string"}}}`, - func(state *ScheduleState, args map[string]any) string { - return QueueSkipHead(state, args) + func(state *schedule.ScheduleState, args map[string]any) string { + return schedule.QueueSkipHead(state, args) }, ) r.Register("min_context_switch", "在指定任务集合内重排 suggested 任务,尽量让同类任务连续以减少上下文切换。仅在用户明确允许打乱顺序时使用。task_ids 必填(兼容 task_id)。", `{"name":"min_context_switch","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"}}}`, - func(state *ScheduleState, args map[string]any) string { - taskIDs, err := parseMinContextSwitchTaskIDs(args) + func(state *schedule.ScheduleState, args map[string]any) string { + taskIDs, err := schedule.ParseMinContextSwitchTaskIDs(args) if err != nil { return fmt.Sprintf("减少上下文切换失败:%s。", err.Error()) } - return MinContextSwitch(state, taskIDs) + return schedule.MinContextSwitch(state, taskIDs) }, ) r.Register("spread_even", "在给定任务集合内做均匀化铺开:先按筛选条件收集候选坑位,再规划并原子落地。task_ids 必填(兼容 task_id)。", `{"name":"spread_even","parameters":{"task_ids":{"type":"array","required":true,"items":{"type":"int"}},"task_id":{"type":"int"},"limit":{"type":"int"},"allow_embed":{"type":"bool"},"day":{"type":"int"},"day_start":{"type":"int"},"day_end":{"type":"int"},"day_scope":{"type":"string","enum":["all","workday","weekend"]},"day_of_week":{"type":"array","items":{"type":"int"}},"week":{"type":"int"},"week_filter":{"type":"array","items":{"type":"int"}},"week_from":{"type":"int"},"week_to":{"type":"int"},"slot_type":{"type":"string"},"slot_types":{"type":"array","items":{"type":"string"}},"exclude_sections":{"type":"array","items":{"type":"int"}},"after_section":{"type":"int"},"before_section":{"type":"int"}}}`, - func(state *ScheduleState, args map[string]any) string { - taskIDs, err := parseSpreadEvenTaskIDs(args) + func(state *schedule.ScheduleState, args map[string]any) string { + taskIDs, err := schedule.ParseSpreadEvenTaskIDs(args) if err != nil { return fmt.Sprintf("均匀化调整失败:%s。", err.Error()) } - return SpreadEven(state, taskIDs, args) + return schedule.SpreadEven(state, taskIDs, args) }, ) r.Register("unplace", "将一个已落位任务移除,恢复为待安排状态。会自动清理嵌入关系。task_id 必填。", `{"name":"unplace","parameters":{"task_id":{"type":"int","required":true}}}`, - func(state *ScheduleState, args map[string]any) string { - taskID, ok := argsInt(args, "task_id") + func(state *schedule.ScheduleState, args map[string]any) string { + taskID, ok := schedule.ArgsInt(args, "task_id") if !ok { return "移除失败:缺少必填参数 task_id。" } - return Unplace(state, taskID) + return schedule.Unplace(state, taskID) + }, + ) + + // --- Web 搜索读工具 --- + // 1. provider 为 nil 时 handler 返回"暂未启用"的 observation,不会阻断主流程; + // 2. 两个工具均为读操作,走 action=continue + tool_call 模式。 + webSearchHandler := web.NewSearchToolHandler(deps.WebSearchProvider) + webFetchHandler := web.NewFetchToolHandler(web.NewFetcher()) + + r.Register("web_search", + "Web 搜索:根据 query 返回结构化检索结果(标题/摘要/URL/来源域名/时间)。query 必填。", + `{"name":"web_search","parameters":{"query":{"type":"string","required":true},"top_k":{"type":"int"},"domain_allow":{"type":"array","items":{"type":"string"}},"recency_days":{"type":"int"}}}`, + func(state *schedule.ScheduleState, args map[string]any) string { + return webSearchHandler.Handle(args) + }, + ) + + r.Register("web_fetch", + "抓取指定 URL 的正文内容并做最小 HTML 清洗。url 必填。", + `{"name":"web_fetch","parameters":{"url":{"type":"string","required":true},"max_chars":{"type":"int"}}}`, + func(state *schedule.ScheduleState, args map[string]any) string { + return webFetchHandler.Handle(args) }, ) diff --git a/backend/newAgent/tools/arg_guard.go b/backend/newAgent/tools/schedule/arg_guard.go similarity index 98% rename from backend/newAgent/tools/arg_guard.go rename to backend/newAgent/tools/schedule/arg_guard.go index 1b06967..fa9c65d 100644 --- a/backend/newAgent/tools/arg_guard.go +++ b/backend/newAgent/tools/schedule/arg_guard.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "fmt" diff --git a/backend/newAgent/tools/args.go b/backend/newAgent/tools/schedule/args.go similarity index 80% rename from backend/newAgent/tools/args.go rename to backend/newAgent/tools/schedule/args.go index 3054cc2..22d99c3 100644 --- a/backend/newAgent/tools/args.go +++ b/backend/newAgent/tools/schedule/args.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import "fmt" @@ -7,7 +7,7 @@ import "fmt" // JSON 反序列化后数字默认为 float64,字符串为 string,需要类型断言。 // argsInt 从 map 中提取 int 值。支持 float64(JSON 反序列化的默认类型)。 -func argsInt(args map[string]any, key string) (int, bool) { +func ArgsInt(args map[string]any, key string) (int, bool) { v, ok := args[key] if !ok { return 0, false @@ -22,7 +22,7 @@ func argsInt(args map[string]any, key string) (int, bool) { } // argsString 从 map 中提取 string 值。 -func argsString(args map[string]any, key string) (string, bool) { +func ArgsString(args map[string]any, key string) (string, bool) { v, ok := args[key] if !ok { return "", false @@ -32,8 +32,8 @@ func argsString(args map[string]any, key string) (string, bool) { } // argsIntPtr 从 map 中提取可选 int 值,不存在返回 nil。 -func argsIntPtr(args map[string]any, key string) *int { - v, ok := argsInt(args, key) +func ArgsIntPtr(args map[string]any, key string) *int { + v, ok := ArgsInt(args, key) if !ok { return nil } @@ -41,8 +41,8 @@ func argsIntPtr(args map[string]any, key string) *int { } // argsStringPtr 从 map 中提取可选 string 值,不存在返回 nil。 -func argsStringPtr(args map[string]any, key string) *string { - v, ok := argsString(args, key) +func ArgsStringPtr(args map[string]any, key string) *string { + v, ok := ArgsString(args, key) if !ok { return nil } @@ -50,7 +50,7 @@ func argsStringPtr(args map[string]any, key string) *string { } // argsIntSlice 从 map 中提取 int 数组,支持 []any / []int / []float64。 -func argsIntSlice(args map[string]any, key string) ([]int, bool) { +func ArgsIntSlice(args map[string]any, key string) ([]int, bool) { v, ok := args[key] if !ok { return nil, false @@ -88,7 +88,7 @@ func argsIntSlice(args map[string]any, key string) ([]int, bool) { } // argsMoveList 从 map 中提取 batch_move 的 moves 数组。 -func argsMoveList(args map[string]any) ([]MoveRequest, error) { +func ArgsMoveList(args map[string]any) ([]MoveRequest, error) { v, ok := args["moves"] if !ok { return nil, fmt.Errorf("缺少 moves 参数") @@ -103,15 +103,15 @@ func argsMoveList(args map[string]any) ([]MoveRequest, error) { if !ok { return nil, fmt.Errorf("moves[%d] 不是有效对象", i) } - taskID, ok := argsInt(m, "task_id") + taskID, ok := ArgsInt(m, "task_id") if !ok { return nil, fmt.Errorf("moves[%d].task_id 缺失或无效", i) } - newDay, ok := argsInt(m, "new_day") + newDay, ok := ArgsInt(m, "new_day") if !ok { return nil, fmt.Errorf("moves[%d].new_day 缺失或无效", i) } - newSlotStart, ok := argsInt(m, "new_slot_start") + newSlotStart, ok := ArgsInt(m, "new_slot_start") if !ok { return nil, fmt.Errorf("moves[%d].new_slot_start 缺失或无效", i) } diff --git a/backend/newAgent/tools/compound_tools.go b/backend/newAgent/tools/schedule/compound_tools.go similarity index 98% rename from backend/newAgent/tools/compound_tools.go rename to backend/newAgent/tools/schedule/compound_tools.go index b7e2c55..df769bb 100644 --- a/backend/newAgent/tools/compound_tools.go +++ b/backend/newAgent/tools/schedule/compound_tools.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "encoding/json" @@ -305,19 +305,19 @@ func SpreadEven(state *ScheduleState, taskIDs []int, args map[string]any) string return strings.TrimSpace(sb.String()) } -func parseMinContextSwitchTaskIDs(args map[string]any) ([]int, error) { - return parseCompositeTaskIDs(args) +func ParseMinContextSwitchTaskIDs(args map[string]any) ([]int, error) { + return ParseCompositeTaskIDs(args) } -func parseSpreadEvenTaskIDs(args map[string]any) ([]int, error) { - return parseCompositeTaskIDs(args) +func ParseSpreadEvenTaskIDs(args map[string]any) ([]int, error) { + return ParseCompositeTaskIDs(args) } -func parseCompositeTaskIDs(args map[string]any) ([]int, error) { - if ids, ok := argsIntSlice(args, "task_ids"); ok && len(ids) > 0 { +func ParseCompositeTaskIDs(args map[string]any) ([]int, error) { + if ids, ok := ArgsIntSlice(args, "task_ids"); ok && len(ids) > 0 { return ids, nil } - if id, ok := argsInt(args, "task_id"); ok { + if id, ok := ArgsInt(args, "task_id"); ok { return []int{id}, nil } return nil, fmt.Errorf("缺少必填参数 task_ids(兼容单值 task_id)") diff --git a/backend/newAgent/tools/queue_tools.go b/backend/newAgent/tools/schedule/queue_tools.go similarity index 98% rename from backend/newAgent/tools/queue_tools.go rename to backend/newAgent/tools/schedule/queue_tools.go index aaa9fd8..73a5fa2 100644 --- a/backend/newAgent/tools/queue_tools.go +++ b/backend/newAgent/tools/schedule/queue_tools.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "encoding/json" @@ -117,7 +117,7 @@ func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string { }, "queue_apply_head_move") } - newDay, ok := argsInt(args, "new_day") + newDay, ok := ArgsInt(args, "new_day") if !ok { return mustJSON(queueApplyHeadMoveResult{ Tool: "queue_apply_head_move", @@ -130,7 +130,7 @@ func QueueApplyHeadMove(state *ScheduleState, args map[string]any) string { Result: "缺少必填参数 new_day。", }, "queue_apply_head_move") } - newSlotStart, ok := argsInt(args, "new_slot_start") + newSlotStart, ok := ArgsInt(args, "new_slot_start") if !ok { return mustJSON(queueApplyHeadMoveResult{ Tool: "queue_apply_head_move", @@ -190,7 +190,7 @@ func QueueSkipHead(state *ScheduleState, args map[string]any) string { } reason := "" - if raw, ok := argsString(args, "reason"); ok { + if raw, ok := ArgsString(args, "reason"); ok { reason = strings.TrimSpace(raw) } markCurrentTaskSkipped(state) diff --git a/backend/newAgent/tools/read_filter_tools.go b/backend/newAgent/tools/schedule/read_filter_tools.go similarity index 99% rename from backend/newAgent/tools/read_filter_tools.go rename to backend/newAgent/tools/schedule/read_filter_tools.go index 9ff98b1..65f3ca5 100644 --- a/backend/newAgent/tools/read_filter_tools.go +++ b/backend/newAgent/tools/schedule/read_filter_tools.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "encoding/json" @@ -850,7 +850,7 @@ func inferWeekBounds(state *ScheduleState) (int, int) { // readIntAny 按别名顺序读取 int 参数。 func readIntAny(args map[string]any, keys ...string) (int, bool) { for _, key := range keys { - value, ok := argsInt(args, key) + value, ok := ArgsInt(args, key) if ok { return value, true } @@ -861,7 +861,7 @@ func readIntAny(args map[string]any, keys ...string) (int, bool) { // readStringAny 按别名顺序读取 string 参数。 func readStringAny(args map[string]any, keys ...string) string { for _, key := range keys { - if value, ok := argsString(args, key); ok { + if value, ok := ArgsString(args, key); ok { return value } } @@ -894,7 +894,7 @@ func readBoolAnyWithDefault(args map[string]any, defaultValue bool, keys ...stri // readIntSliceAny 按别名顺序读取 int 列表参数。 func readIntSliceAny(args map[string]any, keys ...string) []int { for _, key := range keys { - if values, ok := argsIntSlice(args, key); ok { + if values, ok := ArgsIntSlice(args, key); ok { return values } } diff --git a/backend/newAgent/tools/read_helpers.go b/backend/newAgent/tools/schedule/read_helpers.go similarity index 99% rename from backend/newAgent/tools/read_helpers.go rename to backend/newAgent/tools/schedule/read_helpers.go index 54e956d..899f534 100644 --- a/backend/newAgent/tools/read_helpers.go +++ b/backend/newAgent/tools/schedule/read_helpers.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "fmt" diff --git a/backend/newAgent/tools/read_tools.go b/backend/newAgent/tools/schedule/read_tools.go similarity index 99% rename from backend/newAgent/tools/read_tools.go rename to backend/newAgent/tools/schedule/read_tools.go index 8b059ec..136bbc5 100644 --- a/backend/newAgent/tools/read_tools.go +++ b/backend/newAgent/tools/schedule/read_tools.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "fmt" diff --git a/backend/newAgent/tools/runtime_queue.go b/backend/newAgent/tools/schedule/runtime_queue.go similarity index 99% rename from backend/newAgent/tools/runtime_queue.go rename to backend/newAgent/tools/schedule/runtime_queue.go index 2c8c64f..20ab211 100644 --- a/backend/newAgent/tools/runtime_queue.go +++ b/backend/newAgent/tools/schedule/runtime_queue.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule // TaskProcessingQueue 表示 execute 阶段的“逐项处理队列”运行态。 // diff --git a/backend/newAgent/tools/state.go b/backend/newAgent/tools/schedule/state.go similarity index 99% rename from backend/newAgent/tools/state.go rename to backend/newAgent/tools/schedule/state.go index e9058f1..77a596a 100644 --- a/backend/newAgent/tools/state.go +++ b/backend/newAgent/tools/schedule/state.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule // DayMapping maps a day_index to a real (week, day_of_week) coordinate. type DayMapping struct { diff --git a/backend/newAgent/tools/status.go b/backend/newAgent/tools/schedule/status.go similarity index 99% rename from backend/newAgent/tools/status.go rename to backend/newAgent/tools/schedule/status.go index 78c1736..7b30e5a 100644 --- a/backend/newAgent/tools/status.go +++ b/backend/newAgent/tools/schedule/status.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import "slices" diff --git a/backend/newAgent/tools/write_helpers.go b/backend/newAgent/tools/schedule/write_helpers.go similarity index 99% rename from backend/newAgent/tools/write_helpers.go rename to backend/newAgent/tools/schedule/write_helpers.go index 40a2e17..2911529 100644 --- a/backend/newAgent/tools/write_helpers.go +++ b/backend/newAgent/tools/schedule/write_helpers.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "fmt" diff --git a/backend/newAgent/tools/write_tools.go b/backend/newAgent/tools/schedule/write_tools.go similarity index 99% rename from backend/newAgent/tools/write_tools.go rename to backend/newAgent/tools/schedule/write_tools.go index 674861b..fd89f62 100644 --- a/backend/newAgent/tools/write_tools.go +++ b/backend/newAgent/tools/schedule/write_tools.go @@ -1,4 +1,4 @@ -package newagenttools +package schedule import ( "fmt" diff --git a/backend/newAgent/tools/web/fetcher.go b/backend/newAgent/tools/web/fetcher.go new file mode 100644 index 0000000..4e080bc --- /dev/null +++ b/backend/newAgent/tools/web/fetcher.go @@ -0,0 +1,175 @@ +package web + +import ( + "context" + "fmt" + "io" + "net/http" + "regexp" + "strings" + "time" +) + +// Fetcher 抓取指定 URL 正文并做最小 HTML 清洗。 +// +// 职责: +// 1. 发起 HTTP GET 请求并读取响应体; +// 2. 剥离 HTML 标签,保留纯文本内容; +// 3. 按 MaxChars 截断,避免超长正文占用模型上下文。 +// +// 不负责: +// 1. 不负责 JS 渲染(无法处理 SPA 页面); +// 2. 不负责反爬绕过(遇到 403 直接返回错误); +// 3. 不负责正文提取算法优化(仅做粗粒度标签剥离)。 +type Fetcher struct { + // Client 带超时的 HTTP 客户端,由调用方注入。 + Client *http.Client + + // MaxChars 正文最大字符数。超出时截断并标记 truncated=true。0 使用默认值 4000。 + MaxChars int +} + +// NewFetcher 创建默认 Fetcher。 +// +// 1. 超时默认 10 秒,足够覆盖大多数静态页面; +// 2. MaxChars 默认 4000 字符,约占 1000~2000 token,不会挤占过多上下文。 +func NewFetcher() *Fetcher { + return &Fetcher{ + Client: &http.Client{ + Timeout: 10 * time.Second, + }, + MaxChars: 4000, + } +} + +// FetchResult 抓取结果。 +type FetchResult struct { + // Title 页面标题(从