Version: 0.9.15.dev.260412

后端:
1. 排程工具从 tools/ 根目录拆分为 tools/schedule 独立子包
- 12 个排程工具文件等价迁入 tools/schedule/,tools/ 根目录仅保留 registry.go 作为统一注册入口
- 所有依赖方(conv / model / node / prompt / service)import 统一切到 schedule 子包
2. Web 搜索工具链落地(tools/web 子包)
- 新增 web_search(结构化检索)与 web_fetch(正文抓取)两个读工具,支持博查 API / mock 降级
- 启动流程按配置选择 provider,未识别类型自动降级为 mock,不阻断主流程
- 执行提示补齐 web 工具使用约束与返回值示例
- config.example.yaml 补齐 websearch 配置段
前端:无
仓库:无
This commit is contained in:
Losita
2026-04-12 19:02:54 +08:00
parent bf1f1defa5
commit 070d4c3459
34 changed files with 1033 additions and 205 deletions

View File

@@ -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)

View File

@@ -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. 转换失败的 slotday_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)
}

View File

@@ -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),
}

View File

@@ -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)