Version: 0.9.75.dev.260505

后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
This commit is contained in:
Losita
2026-05-05 16:00:57 +08:00
parent e1819c5653
commit d7184b776b
174 changed files with 2189 additions and 1236 deletions

View File

@@ -0,0 +1,330 @@
package sv
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/model"
agentconv "github.com/LoveLosita/smartflow/backend/services/agent/conv"
scheduletool "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
schedulecontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/schedule"
taskclasscontracts "github.com/LoveLosita/smartflow/backend/shared/contracts/taskclass"
)
const scheduleProviderRPCTimeout = 6 * time.Second
// ScheduleAgentRPCClient 描述 agent schedule provider 读取 schedule 服务所需的最小能力。
//
// 职责边界:
// 1. 只读取按周原始日程槽位事实;
// 2. 不暴露 schedule DAO、缓存或写入状态机
// 3. 返回 JSON 契约后由 provider 复用既有 LoadScheduleState 建模逻辑。
type ScheduleAgentRPCClient interface {
GetAgentWeekSchedule(ctx context.Context, req schedulecontracts.AgentScheduleWeekRequest) (json.RawMessage, error)
}
// TaskClassAgentReadRPCClient 描述 agent schedule provider 读取 task-class 服务所需的最小能力。
type TaskClassAgentReadRPCClient interface {
GetAgentTaskClasses(ctx context.Context, req taskclasscontracts.AgentTaskClassesRequest) (json.RawMessage, error)
}
// TaskClassAgentRPCClient 聚合 agent 当前依赖的 task-class RPC 写入与读取能力。
type TaskClassAgentRPCClient interface {
TaskClassUpsertRPCClient
TaskClassAgentReadRPCClient
}
// ScheduleRPCProvider 通过 schedule/task-class zrpc 构建 agent ScheduleState。
//
// 职责边界:
// 1. 只替换 agent schedule provider 的 DAO 读取路径;
// 2. 窗口推导、extra category 与 ScheduleState 建模继续复用 agent/conv 老逻辑;
// 3. 不负责持久化 Diff不改变 confirm/apply 链路。
type ScheduleRPCProvider struct {
scheduleClient ScheduleAgentRPCClient
taskClassClient TaskClassAgentReadRPCClient
}
func NewScheduleRPCProvider(scheduleClient ScheduleAgentRPCClient, taskClassClient TaskClassAgentReadRPCClient) *ScheduleRPCProvider {
return &ScheduleRPCProvider{
scheduleClient: scheduleClient,
taskClassClient: taskClassClient,
}
}
func (p *ScheduleRPCProvider) LoadScheduleState(ctx context.Context, userID int) (*scheduletool.ScheduleState, error) {
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID, nil)
if err != nil {
return nil, err
}
return p.loadScheduleStateWithTaskClasses(ctx, userID, taskClasses, true)
}
func (p *ScheduleRPCProvider) LoadScheduleStateForTaskClasses(ctx context.Context, userID int, taskClassIDs []int) (*scheduletool.ScheduleState, error) {
if len(taskClassIDs) == 0 {
return p.LoadScheduleState(ctx, userID)
}
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID, taskClassIDs)
if err != nil {
return nil, err
}
return p.loadScheduleStateWithTaskClasses(ctx, userID, taskClasses, false)
}
func (p *ScheduleRPCProvider) LoadTaskClassMetas(ctx context.Context, userID int, taskClassIDs []int) ([]scheduletool.TaskClassMeta, error) {
if len(taskClassIDs) == 0 {
return nil, nil
}
taskClasses, err := p.loadCompleteTaskClasses(ctx, userID, taskClassIDs)
if err != nil {
return nil, err
}
return agentconv.TaskClassesToScheduleMetas(taskClasses), nil
}
func (p *ScheduleRPCProvider) loadScheduleStateWithTaskClasses(ctx context.Context, userID int, taskClasses []model.TaskClass, allowCurrentWeekFallback bool) (*scheduletool.ScheduleState, error) {
windowDays, weeks := agentconv.BuildWindowFromTaskClasses(taskClasses)
if len(windowDays) == 0 {
if !allowCurrentWeekFallback {
return nil, fmt.Errorf("任务类缺少有效时间窗:请补充 start_date/end_date 后再进行智能编排")
}
var err error
windowDays, weeks, err = agentconv.BuildCurrentWeekWindow()
if err != nil {
return nil, err
}
}
allSchedules := make([]model.Schedule, 0)
for _, week := range weeks {
weekSchedules, err := p.loadWeekSchedules(ctx, userID, week)
if err != nil {
return nil, fmt.Errorf("通过 schedule RPC 加载用户周日程失败 week=%d: %w", week, err)
}
allSchedules = append(allSchedules, weekSchedules...)
}
extraItemCategories := agentconv.BuildExtraItemCategories(allSchedules, taskClasses)
return agentconv.LoadScheduleState(allSchedules, taskClasses, extraItemCategories, windowDays), nil
}
func (p *ScheduleRPCProvider) loadCompleteTaskClasses(ctx context.Context, userID int, taskClassIDs []int) ([]model.TaskClass, error) {
if p == nil || p.taskClassClient == nil {
return nil, errors.New("task-class rpc reader is nil")
}
callCtx, cancel := context.WithTimeout(ctx, scheduleProviderRPCTimeout)
defer cancel()
raw, err := p.taskClassClient.GetAgentTaskClasses(callCtx, taskclasscontracts.AgentTaskClassesRequest{
UserID: userID,
TaskClassIDs: append([]int(nil), taskClassIDs...),
})
if err != nil {
return nil, err
}
var resp taskclasscontracts.AgentTaskClassesResponse
if len(raw) > 0 && string(raw) != "null" {
if err := json.Unmarshal(raw, &resp); err != nil {
return nil, err
}
}
taskClasses := make([]model.TaskClass, 0, len(resp.TaskClasses))
for _, item := range resp.TaskClasses {
taskClass, err := agentTaskClassToModel(item)
if err != nil {
return nil, err
}
taskClasses = append(taskClasses, taskClass)
}
return taskClasses, nil
}
func (p *ScheduleRPCProvider) loadWeekSchedules(ctx context.Context, userID int, week int) ([]model.Schedule, error) {
if p == nil || p.scheduleClient == nil {
return nil, errors.New("schedule rpc reader is nil")
}
callCtx, cancel := context.WithTimeout(ctx, scheduleProviderRPCTimeout)
defer cancel()
raw, err := p.scheduleClient.GetAgentWeekSchedule(callCtx, schedulecontracts.AgentScheduleWeekRequest{
UserID: userID,
Week: week,
})
if err != nil {
return nil, err
}
var resp schedulecontracts.AgentScheduleWeekResponse
if len(raw) > 0 && string(raw) != "null" {
if err := json.Unmarshal(raw, &resp); err != nil {
return nil, err
}
}
schedules := make([]model.Schedule, 0, len(resp.Schedules))
for _, item := range resp.Schedules {
schedules = append(schedules, agentScheduleSlotToModel(item))
}
return schedules, nil
}
func agentTaskClassToModel(in taskclasscontracts.AgentTaskClass) (model.TaskClass, error) {
startDate, err := parseAgentDate(in.StartDate)
if err != nil {
return model.TaskClass{}, err
}
endDate, err := parseAgentDate(in.EndDate)
if err != nil {
return model.TaskClass{}, err
}
items := make([]model.TaskClassItem, 0, len(in.Items))
for _, item := range in.Items {
content := item.Content
items = append(items, model.TaskClassItem{
ID: item.ID,
CategoryID: cloneIntPtr(item.CategoryID),
Order: cloneIntPtr(item.Order),
Content: &content,
EmbeddedTime: taskClassContractTargetTimeToModel(item.EmbeddedTime),
Status: cloneIntPtr(item.Status),
})
}
return model.TaskClass{
ID: in.ID,
UserID: intPtrOrNil(in.UserID),
Name: stringPtrOrNil(in.Name),
Mode: stringPtrOrNil(in.Mode),
StartDate: startDate,
EndDate: endDate,
SubjectType: stringPtrOrNil(in.SubjectType),
DifficultyLevel: stringPtrOrNil(in.DifficultyLevel),
CognitiveIntensity: stringPtrOrNil(in.CognitiveIntensity),
TotalSlots: intPtrOrNil(in.TotalSlots),
AllowFillerCourse: boolPtr(in.AllowFillerCourse),
Strategy: stringPtrOrNil(in.Strategy),
ExcludedSlots: model.IntSlice(append([]int(nil), in.ExcludedSlots...)),
ExcludedDaysOfWeek: model.IntSlice(append([]int(nil), in.ExcludedDaysOfWeek...)),
Items: items,
}, nil
}
func agentScheduleSlotToModel(in schedulecontracts.AgentScheduleSlot) model.Schedule {
return model.Schedule{
ID: in.ID,
EventID: in.EventID,
UserID: in.UserID,
Week: in.Week,
DayOfWeek: in.DayOfWeek,
Section: in.Section,
EmbeddedTaskID: cloneIntPtr(in.EmbeddedTaskID),
Status: in.Status,
Event: agentScheduleEventToModel(in.Event),
EmbeddedTask: agentScheduleTaskItemToModel(in.EmbeddedTask),
}
}
func agentScheduleEventToModel(in *schedulecontracts.AgentScheduleEvent) *model.ScheduleEvent {
if in == nil {
return nil
}
return &model.ScheduleEvent{
ID: in.ID,
UserID: in.UserID,
Name: in.Name,
Location: cloneStringPtr(in.Location),
Type: in.Type,
RelID: cloneIntPtr(in.RelID),
TaskSourceType: in.TaskSourceType,
CanBeEmbedded: in.CanBeEmbedded,
StartTime: in.StartTime,
EndTime: in.EndTime,
}
}
func agentScheduleTaskItemToModel(in *schedulecontracts.AgentScheduleTaskItem) *model.TaskClassItem {
if in == nil {
return nil
}
content := in.Content
return &model.TaskClassItem{
ID: in.ID,
CategoryID: cloneIntPtr(in.CategoryID),
Order: cloneIntPtr(in.Order),
Content: &content,
EmbeddedTime: scheduleContractTargetTimeToModel(in.EmbeddedTime),
Status: cloneIntPtr(in.Status),
}
}
func parseAgentDate(value string) (*time.Time, error) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil, nil
}
parsed, err := time.ParseInLocation("2006-01-02", trimmed, time.Local)
if err != nil {
return nil, err
}
return &parsed, nil
}
func taskClassContractTargetTimeToModel(value *taskclasscontracts.TargetTime) *model.TargetTime {
if value == nil {
return nil
}
return &model.TargetTime{
Week: value.Week,
DayOfWeek: value.DayOfWeek,
SectionFrom: value.SectionFrom,
SectionTo: value.SectionTo,
}
}
func scheduleContractTargetTimeToModel(value *schedulecontracts.AgentScheduleTargetTime) *model.TargetTime {
if value == nil {
return nil
}
return &model.TargetTime{
Week: value.Week,
DayOfWeek: value.DayOfWeek,
SectionFrom: value.SectionFrom,
SectionTo: value.SectionTo,
}
}
func stringPtrOrNil(value string) *string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return nil
}
return &trimmed
}
func intPtrOrNil(value int) *int {
if value == 0 {
return nil
}
return &value
}
func boolPtr(value bool) *bool {
return &value
}
func cloneIntPtr(value *int) *int {
if value == nil {
return nil
}
copied := *value
return &copied
}
func cloneStringPtr(value *string) *string {
if value == nil {
return nil
}
copied := *value
return &copied
}