后端: 1.把最后一块拼图:schedule_refine也搬迁到了agent2,此时agent已经完全解耦。但是它没融入新架构,Codex只尝试把它调整了一部分,回退了一些错误的更改,保持着现在的可运行状态。下次继续改。 2.agent目录先保留,直到refine彻底融入新架构。 3.改善Codex主导的新史山结构:node文件夹里面大量文件,转而改成了module.go+module_tool.go的双文件格局,极大提升架构整洁度和代码可读性。 前端: 1.新开了日历界面,正在保持往前推进。做了很多更改,感觉越来越好了。
319 lines
15 KiB
Go
319 lines
15 KiB
Go
package agentsvc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"strings"
|
||
"time"
|
||
|
||
agentmodel "github.com/LoveLosita/smartflow/backend/agent2/model"
|
||
agentshared "github.com/LoveLosita/smartflow/backend/agent2/shared"
|
||
"github.com/LoveLosita/smartflow/backend/model"
|
||
"github.com/LoveLosita/smartflow/backend/respond"
|
||
)
|
||
|
||
// saveSchedulePlanPreview 鎶婃帓绋嬬粨鏋滀互缁撴瀯鍖?JSON 蹇収鍐欏叆 Redis銆?
|
||
//
|
||
// 鑱岃矗杈圭晫锛?
|
||
// 1. 璐熻矗鎶?finalState 涓殑 summary + candidate_plans 鏀舵暃涓虹紦瀛?DTO锛?
|
||
// 2. 璐熻矗浠モ€滃け璐ヤ笉闃绘柇鑱婂ぉ涓婚摼璺€濈殑绛栫暐鎵ц鍐欏叆锛?
|
||
// 3. 涓嶈礋璐?SSE 杩斿洖鍗忚锛屼笉璐熻矗鏁版嵁搴撹惤搴撱€?
|
||
func (s *AgentService) saveSchedulePlanPreview(ctx context.Context, userID int, chatID string, finalState *agentmodel.SchedulePlanState) {
|
||
// 1. 鍩虹鍓嶇疆鏍¢獙锛歴tate 涓虹┖鏃剁洿鎺ヨ繑鍥烇紝閬垮厤鍐欏叆鍗婃垚鍝佸揩鐓с€?
|
||
if s == nil || finalState == nil {
|
||
return
|
||
}
|
||
normalizedChatID := strings.TrimSpace(chatID)
|
||
if normalizedChatID == "" {
|
||
return
|
||
}
|
||
|
||
// 2. 缁勮缂撳瓨蹇収锛?
|
||
// 2.1 summary 浼樺厛鍙?final summary锛岀┖鍊兼椂浣跨敤缁熶竴鍏滃簳鏂囨锛?
|
||
// 2.2 candidate_plans 鍋氬垏鐗囨嫹璐濓紝閬垮厤鍚庣画寮曠敤鍏变韩瀵艰嚧鎰忓瑕嗙洊锛?
|
||
// 2.3 generated_at 鐢ㄤ簬鍓嶇鍒ゆ柇鈥滃綋鍓嶉瑙堢殑鏂伴矞搴︹€濄€?
|
||
summary := strings.TrimSpace(finalState.FinalSummary)
|
||
if summary == "" {
|
||
summary = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
preview := &model.SchedulePlanPreviewCache{
|
||
UserID: userID,
|
||
ConversationID: normalizedChatID,
|
||
TraceID: strings.TrimSpace(finalState.TraceID),
|
||
Summary: summary,
|
||
CandidatePlans: cloneWeekSchedules(finalState.CandidatePlans),
|
||
TaskClassIDs: append([]int(nil), finalState.TaskClassIDs...),
|
||
HybridEntries: cloneHybridEntries(finalState.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(finalState.AllocatedItems),
|
||
GeneratedAt: time.Now(),
|
||
}
|
||
|
||
// 3. 璋冪敤鐩殑锛氬厛鍐?Redis 棰勮锛屼繚璇佸墠绔煡璇㈡帴鍙h兘蹇€熻鍙栫粨鏋勫寲缁撴灉銆?
|
||
// 3.1 Redis 鏄€滃揩璺緞鈥濓紱澶辫触鍙褰曟棩蹇楋紝涓嶄腑鏂富閾捐矾锛?
|
||
// 3.2 澶辫触鍏滃簳鐢卞悗缁?MySQL 蹇収鎵挎帴銆?
|
||
if s.cacheDAO != nil {
|
||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); err != nil {
|
||
log.Printf("鍐欏叆鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
|
||
// 4. 璋冪敤鐩殑锛氬悓姝ュ啓 MySQL 鐘舵€佸揩鐓э紝淇濊瘉 Redis 澶辨晥鍚庝粛鍙繛缁井璋冦€?
|
||
// 4.1 杩欓噷閲囩敤鈥滃悓姝ュ啓搴撯€濊€屼笉鏄?outbox锛氬洜涓轰笅涓€杞井璋冭寮哄疄鏃惰鍙栵紱
|
||
// 4.2 蹇収鍐欏叆澶辫触鍙墦鏃ュ織锛屼笉闃绘柇鏈疆鐢ㄦ埛鍥炲锛岄伩鍏嶄綋楠屾姈鍔紱
|
||
// 4.3 revision 鑷鐢?DAO 鐨?upsert 鍐茬獊鏇存柊璐熻矗銆?
|
||
if s.repo != nil {
|
||
snapshot := buildSchedulePlanSnapshotFromState(userID, normalizedChatID, finalState)
|
||
if err := s.repo.UpsertScheduleStateSnapshot(ctx, snapshot); err != nil {
|
||
log.Printf("鍐欏叆鎺掔▼鐘舵€佸揩鐓уけ璐?chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// saveSchedulePlanPreviewAgent2 鎶?agent2 鐨?schedule_plan 缁撴灉鍐欏叆 Redis 棰勮涓?MySQL 蹇収銆?
|
||
//
|
||
// 鑱岃矗杈圭晫锛?
|
||
// 1. 璐熻矗鎵挎帴鈥滄柊 agent2 棣栨鎺掔▼閾捐矾鈥濈殑鏈€缁堢姸鎬侊紱
|
||
// 2. 璐熻矗娌跨敤鐜版湁棰勮缂撳瓨/鐘舵€佸揩鐓у崗璁紝淇濊瘉鏌ヨ鎺ュ彛涓?refine 璇诲彇閫昏緫涓嶉渶瑕佽窡鐫€閲嶅啓锛?
|
||
// 3. 涓嶈礋璐?refine 鐘舵€佽浆鎹紝refine 浠嶇户缁蛋鏃ч摼璺殑 saveSchedulePlanPreview銆?
|
||
func (s *AgentService) saveSchedulePlanPreviewAgent2(ctx context.Context, userID int, chatID string, finalState *agentmodel.SchedulePlanState) {
|
||
// 1. 鍩虹鍓嶇疆鏍¢獙锛歴tate 涓虹┖鏃剁洿鎺ヨ繑鍥烇紝閬垮厤鍐欏叆鍗婃垚鍝佸揩鐓с€?
|
||
if s == nil || finalState == nil {
|
||
return
|
||
}
|
||
normalizedChatID := strings.TrimSpace(chatID)
|
||
if normalizedChatID == "" {
|
||
return
|
||
}
|
||
|
||
// 2. 缁勮缂撳瓨蹇収銆?
|
||
// 2.1 summary 浼樺厛鍙?final summary锛岀┖鍊兼椂浣跨敤缁熶竴鍏滃簳鏂囨锛?
|
||
// 2.2 candidate_plans / hybrid_entries / allocated_items 缁熶竴娣辨嫹璐濓紝閬垮厤缂撳瓨涓?graph state 鍏辩敤搴曞眰鍒囩墖锛?
|
||
// 2.3 generated_at 鐢ㄤ簬鍓嶇鍒ゆ柇鈥滃綋鍓嶉瑙堟槸鍚︿负鏈€鏂版柟妗堚€濄€?
|
||
summary := strings.TrimSpace(finalState.FinalSummary)
|
||
if summary == "" {
|
||
summary = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
preview := &model.SchedulePlanPreviewCache{
|
||
UserID: userID,
|
||
ConversationID: normalizedChatID,
|
||
TraceID: strings.TrimSpace(finalState.TraceID),
|
||
Summary: summary,
|
||
CandidatePlans: cloneWeekSchedules(finalState.CandidatePlans),
|
||
TaskClassIDs: append([]int(nil), finalState.TaskClassIDs...),
|
||
HybridEntries: cloneHybridEntries(finalState.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(finalState.AllocatedItems),
|
||
GeneratedAt: time.Now(),
|
||
}
|
||
|
||
// 3. 鍏堝啓 Redis 棰勮锛屼繚璇佸墠绔煡璇㈡帴鍙h兘绔嬪嵆璇诲彇缁撴瀯鍖栫粨鏋溿€?
|
||
// 3.1 Redis 鏄€滃揩璺緞鈥濓紱
|
||
// 3.2 澶辫触鍙褰曟棩蹇楋紝涓嶄腑鏂亰澶╀富閾捐矾銆?
|
||
if s.cacheDAO != nil {
|
||
if err := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, preview); err != nil {
|
||
log.Printf("鍐欏叆鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
|
||
// 4. 鍚屾鍐?MySQL 蹇収锛屼繚璇?Redis 澶辨晥鍚庝粛鑳芥仮澶嶉瑙堜笌杩炵画寰皟涓婁笅鏂囥€?
|
||
// 4.1 杩欓噷缁х画淇濇寔鈥滃悓姝ュ啓搴撯€濈瓥鐣ワ紝鍥犱负涓嬩竴杞井璋冨蹇収璇诲彇鏄己瀹炴椂渚濊禆锛?
|
||
// 4.2 鍐欏簱澶辫触鍙墦鏃ュ織锛屼笉闃绘柇鏈疆缁欑敤鎴风殑鏂囨湰鍥炲銆?
|
||
if s.repo != nil {
|
||
snapshot := buildSchedulePlanSnapshotFromAgent2State(userID, normalizedChatID, finalState)
|
||
if err := s.repo.UpsertScheduleStateSnapshot(ctx, snapshot); err != nil {
|
||
log.Printf("鍐欏叆鎺掔▼鐘舵€佸揩鐓уけ璐?chat_id=%s: %v", normalizedChatID, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// GetSchedulePlanPreview 鎸?conversation_id 璇诲彇缁撴瀯鍖栨帓绋嬮瑙堛€?
|
||
//
|
||
// 鑱岃矗杈圭晫锛?
|
||
// 1. 璐熻矗鍙傛暟褰掍竴鍖栥€佺紦瀛樿鍙栦笌浼氳瘽褰掑睘鏍¢獙锛?
|
||
// 2. 璐熻矗鎶婄紦瀛?DTO 杞垚 API 鍝嶅簲 DTO锛?
|
||
// 3. 涓嶈礋璐hЕ鍙戞帓绋嬶紝涓嶈礋璐hˉ绠楃紦瀛樸€?
|
||
func (s *AgentService) GetSchedulePlanPreview(ctx context.Context, userID int, chatID string) (*model.GetSchedulePlanPreviewResponse, error) {
|
||
// 1. 鍙傛暟鏍¢獙锛歝onversation_id 涓虹┖鐩存帴杩斿洖鍙傛暟閿欒锛岄伩鍏嶆棤鏁?Redis 璇锋眰銆?
|
||
normalizedChatID := strings.TrimSpace(chatID)
|
||
if normalizedChatID == "" {
|
||
return nil, respond.MissingParam
|
||
}
|
||
if s == nil {
|
||
return nil, errors.New("agent service is not initialized")
|
||
}
|
||
|
||
// 2. 鏌ヨ缂撳瓨骞舵牎楠屽綊灞烇細
|
||
// 2.1 缂撳瓨鏈懡涓細缁熶竴杩斿洖鈥滈瑙堜笉瀛樺湪/宸茶繃鏈熲€濓紱
|
||
// 2.2 鍛戒腑浣?user_id 涓嶄竴鑷达細鎸夋湭鍛戒腑澶勭悊锛岄伩鍏嶆硠闇蹭粬浜轰細璇濅俊鎭紱
|
||
// 2.3 澶辫触鍏滃簳锛氱紦瀛樿寮傚父鐩存帴涓婃姏锛岀敱 API 灞傜粺涓€閿欒澶勭悊銆?
|
||
if s.cacheDAO != nil {
|
||
preview, err := s.cacheDAO.GetSchedulePlanPreviewFromCache(ctx, userID, normalizedChatID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if preview != nil {
|
||
if preview.UserID > 0 && preview.UserID != userID {
|
||
return nil, respond.SchedulePlanPreviewNotFound
|
||
}
|
||
plans := cloneWeekSchedules(preview.CandidatePlans)
|
||
if plans == nil {
|
||
plans = make([]model.UserWeekSchedule, 0)
|
||
}
|
||
return &model.GetSchedulePlanPreviewResponse{
|
||
ConversationID: normalizedChatID,
|
||
TraceID: strings.TrimSpace(preview.TraceID),
|
||
Summary: strings.TrimSpace(preview.Summary),
|
||
CandidatePlans: plans,
|
||
GeneratedAt: preview.GeneratedAt,
|
||
}, nil
|
||
}
|
||
}
|
||
|
||
// 3. Redis 鏈懡涓椂鍥炶惤 MySQL 蹇収锛?
|
||
// 3.1 璇诲彇鎴愬姛鍚庣洿鎺ヨ繑鍥烇紝閬垮厤鐢ㄦ埛鐪嬪埌鈥滈瑙堜笉瀛樺湪鈥濈殑鍋囬槾鎬э紱
|
||
// 3.2 鑻ユ湰娆″懡涓?DB 涓旂紦瀛樺彲鐢紝鍒欓『鎵嬪洖濉?Redis锛屾彁鍗囧悗缁懡涓巼锛?
|
||
// 3.3 DB 涔熸湭鍛戒腑鏃跺啀杩斿洖 not found銆?
|
||
if s.repo != nil {
|
||
snapshot, err := s.repo.GetScheduleStateSnapshot(ctx, userID, normalizedChatID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if snapshot != nil {
|
||
response := snapshotToSchedulePlanPreviewResponse(snapshot)
|
||
if s.cacheDAO != nil {
|
||
cachePreview := snapshotToSchedulePlanPreviewCache(snapshot)
|
||
if setErr := s.cacheDAO.SetSchedulePlanPreviewToCache(ctx, userID, normalizedChatID, cachePreview); setErr != nil {
|
||
log.Printf("鍥炲~鎺掔▼棰勮缂撳瓨澶辫触 chat_id=%s: %v", normalizedChatID, setErr)
|
||
}
|
||
}
|
||
return response, nil
|
||
}
|
||
}
|
||
return nil, respond.SchedulePlanPreviewNotFound
|
||
}
|
||
|
||
// cloneWeekSchedules 瀵瑰懆瑙嗗浘鎺掔▼缁撴灉鍋氭繁鎷疯礉锛岄伩鍏嶅垏鐗囧紩鐢ㄥ叡浜€?
|
||
func cloneWeekSchedules(src []model.UserWeekSchedule) []model.UserWeekSchedule {
|
||
return agentshared.CloneWeekSchedules(src)
|
||
}
|
||
|
||
// cloneHybridEntries 娣辨嫹璐濇贩鍚堟潯鐩垏鐗囷紝閬垮厤缂撳瓨/鐘舵€佷箣闂寸浉浜掓薄鏌撱€?
|
||
func cloneHybridEntries(src []model.HybridScheduleEntry) []model.HybridScheduleEntry {
|
||
return agentshared.CloneHybridEntries(src)
|
||
}
|
||
|
||
// cloneTaskClassItems 娣辨嫹璐濅换鍔″潡鍒囩墖锛堝寘鍚寚閽堝瓧娈碉級锛岄伩鍏嶈法璇锋眰寮曠敤鍏变韩銆?
|
||
func cloneTaskClassItems(src []model.TaskClassItem) []model.TaskClassItem {
|
||
return agentshared.CloneTaskClassItems(src)
|
||
}
|
||
|
||
// buildSchedulePlanSnapshotFromState 鎶?graph 杩愯缁撴灉鏄犲皠鎴愬彲鎸佷箙鍖栧揩鐓?DTO銆?
|
||
//
|
||
// 鑱岃矗杈圭晫锛?
|
||
// 1. 璐熻矗瀛楁鏄犲皠涓庢繁鎷疯礉锛岄伩鍏嶈法灞傚叡浜彲鍙樺垏鐗囷紱
|
||
// 2. 璐熻矗琛ラ綈 state_version 榛樿鍊硷紱
|
||
// 3. 涓嶈礋璐f暟鎹簱鍐欏叆锛堝啓鍏ョ敱 DAO 鎵挎媴锛夈€?
|
||
func buildSchedulePlanSnapshotFromState(userID int, conversationID string, st *agentmodel.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||
if st == nil {
|
||
return nil
|
||
}
|
||
return &model.SchedulePlanStateSnapshot{
|
||
UserID: userID,
|
||
ConversationID: conversationID,
|
||
StateVersion: model.SchedulePlanStateVersionV1,
|
||
TaskClassIDs: append([]int(nil), st.TaskClassIDs...),
|
||
Constraints: append([]string(nil), st.Constraints...),
|
||
HybridEntries: cloneHybridEntries(st.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(st.AllocatedItems),
|
||
CandidatePlans: cloneWeekSchedules(st.CandidatePlans),
|
||
UserIntent: strings.TrimSpace(st.UserIntent),
|
||
Strategy: strings.TrimSpace(st.Strategy),
|
||
AdjustmentScope: strings.TrimSpace(st.AdjustmentScope),
|
||
RestartRequested: st.RestartRequested,
|
||
FinalSummary: strings.TrimSpace(st.FinalSummary),
|
||
Completed: st.Completed,
|
||
TraceID: strings.TrimSpace(st.TraceID),
|
||
}
|
||
}
|
||
|
||
// buildSchedulePlanSnapshotFromAgent2State 鎶?agent2 鐨勬帓绋嬬姸鎬佹槧灏勬垚鍙寔涔呭寲蹇収 DTO銆?
|
||
//
|
||
// 璋冪敤鐩殑锛?
|
||
// 1. 杩欒疆鍙縼绉?schedule_plan锛屼笉鍔?refine锛?
|
||
// 2. 鍥犳 preview/蹇収鍗忚缁х画澶嶇敤鑰佺粨鏋勶紝浣嗚琛ヤ竴涓€渁gent2 state -> snapshot DTO鈥濈殑鏄犲皠灞傦紱
|
||
// 3. 杩欐牱鍙互鍋氬埌锛氳鍒掑垱寤洪摼璺垏鍒?agent2锛岃€?refine / 棰勮鏌ヨ閾捐矾鏆傛椂鏃犻渶澶ф敼銆?
|
||
func buildSchedulePlanSnapshotFromAgent2State(userID int, conversationID string, st *agentmodel.SchedulePlanState) *model.SchedulePlanStateSnapshot {
|
||
if st == nil {
|
||
return nil
|
||
}
|
||
return &model.SchedulePlanStateSnapshot{
|
||
UserID: userID,
|
||
ConversationID: conversationID,
|
||
StateVersion: model.SchedulePlanStateVersionV1,
|
||
TaskClassIDs: append([]int(nil), st.TaskClassIDs...),
|
||
Constraints: append([]string(nil), st.Constraints...),
|
||
HybridEntries: cloneHybridEntries(st.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(st.AllocatedItems),
|
||
CandidatePlans: cloneWeekSchedules(st.CandidatePlans),
|
||
UserIntent: strings.TrimSpace(st.UserIntent),
|
||
Strategy: strings.TrimSpace(st.Strategy),
|
||
AdjustmentScope: strings.TrimSpace(st.AdjustmentScope),
|
||
RestartRequested: st.RestartRequested,
|
||
FinalSummary: strings.TrimSpace(st.FinalSummary),
|
||
Completed: st.Completed,
|
||
TraceID: strings.TrimSpace(st.TraceID),
|
||
}
|
||
}
|
||
|
||
// snapshotToSchedulePlanPreviewCache 鎶?MySQL 蹇収杞崲涓?Redis 棰勮缂撳瓨缁撴瀯銆?
|
||
func snapshotToSchedulePlanPreviewCache(snapshot *model.SchedulePlanStateSnapshot) *model.SchedulePlanPreviewCache {
|
||
if snapshot == nil {
|
||
return nil
|
||
}
|
||
summary := strings.TrimSpace(snapshot.FinalSummary)
|
||
if summary == "" {
|
||
summary = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
generatedAt := snapshot.UpdatedAt
|
||
if generatedAt.IsZero() {
|
||
generatedAt = time.Now()
|
||
}
|
||
return &model.SchedulePlanPreviewCache{
|
||
UserID: snapshot.UserID,
|
||
ConversationID: snapshot.ConversationID,
|
||
TraceID: strings.TrimSpace(snapshot.TraceID),
|
||
Summary: summary,
|
||
CandidatePlans: cloneWeekSchedules(snapshot.CandidatePlans),
|
||
TaskClassIDs: append([]int(nil), snapshot.TaskClassIDs...),
|
||
HybridEntries: cloneHybridEntries(snapshot.HybridEntries),
|
||
AllocatedItems: cloneTaskClassItems(snapshot.AllocatedItems),
|
||
GeneratedAt: generatedAt,
|
||
}
|
||
}
|
||
|
||
// snapshotToSchedulePlanPreviewResponse 鎶?MySQL 蹇収杞崲涓烘煡璇㈡帴鍙e搷搴斻€?
|
||
func snapshotToSchedulePlanPreviewResponse(snapshot *model.SchedulePlanStateSnapshot) *model.GetSchedulePlanPreviewResponse {
|
||
if snapshot == nil {
|
||
return nil
|
||
}
|
||
plans := cloneWeekSchedules(snapshot.CandidatePlans)
|
||
if plans == nil {
|
||
plans = make([]model.UserWeekSchedule, 0)
|
||
}
|
||
summary := strings.TrimSpace(snapshot.FinalSummary)
|
||
if summary == "" {
|
||
summary = "排程流程已完成,但未生成结果摘要。"
|
||
}
|
||
generatedAt := snapshot.UpdatedAt
|
||
if generatedAt.IsZero() {
|
||
generatedAt = time.Now()
|
||
}
|
||
return &model.GetSchedulePlanPreviewResponse{
|
||
ConversationID: snapshot.ConversationID,
|
||
TraceID: strings.TrimSpace(snapshot.TraceID),
|
||
Summary: summary,
|
||
CandidatePlans: plans,
|
||
GeneratedAt: generatedAt,
|
||
}
|
||
}
|