Files
smartmate/backend/services/agent/sv/schedule_rpc_provider.go
Losita d7184b776b 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 迁移面
2026-05-05 16:00:57 +08:00

331 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}