Files
smartmate/backend/services/agent/sv/schedule_rpc_provider.go
Losita 3b6fca44a6 Version: 0.9.77.dev.260505
后端:
1.阶段 6 CP4/CP5 目录收口与共享边界纯化
- 将 backend 根目录收口为 services、client、gateway、cmd、shared 五个一级目录
- 收拢 bootstrap、inits、infra/kafka、infra/outbox、conv、respond、pkg、middleware,移除根目录旧实现与空目录
- 将 utils 下沉到 services/userauth/internal/auth,将 logic 下沉到 services/schedule/core/planning
- 将迁移期 runtime 桥接实现统一收拢到 services/runtime/{conv,dao,eventsvc,model},删除 shared/legacy 与未再被 import 的旧 service 实现
- 将 gateway/shared/respond 收口为 HTTP/Gin 错误写回适配,shared/respond 仅保留共享错误语义与状态映射
- 将 HTTP IdempotencyMiddleware 与 RateLimitMiddleware 收口到 gateway/middleware
- 将 GormCachePlugin 下沉到 shared/infra/gormcache,将共享 RateLimiter 下沉到 shared/infra/ratelimit,将 agent token budget 下沉到 services/agent/shared
- 删除 InitEino 兼容壳,收缩 cmd/internal/coreinit 仅保留旧组合壳残留域初始化语义
- 更新微服务迁移计划与桌面 checklist,补齐 CP4/CP5 当前切流点、目录终态与验证结果
- 完成 go test ./...、git diff --check 与最终真实 smoke;health、register/login、task/create+get、schedule/today、task-class/list、memory/items、agent chat/meta/timeline/context-stats 全部 200,SSE 合并结果为 CP5_OK 且 [DONE] 只有 1 个
2026-05-05 23:25:07 +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"
agentconv "github.com/LoveLosita/smartflow/backend/services/agent/conv"
scheduletool "github.com/LoveLosita/smartflow/backend/services/agent/tools/schedule"
"github.com/LoveLosita/smartflow/backend/services/runtime/model"
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
}