package room import ( "context" "fmt" "log" "os" "path/filepath" "strings" "time" "github.com/sdaduanbilei/agent-team/internal/agent" "github.com/sdaduanbilei/agent-team/internal/llm" "github.com/sdaduanbilei/agent-team/internal/prompt" "github.com/sdaduanbilei/agent-team/internal/skill" "github.com/sdaduanbilei/agent-team/internal/store" ) func Load(roomDir string, agentsDir string, skillsDir string, opts ...LoadOption) (*Room, error) { var lo loadOptions for _, opt := range opts { opt(&lo) } data, err := os.ReadFile(filepath.Join(roomDir, "room.md")) if err != nil { return nil, err } cfg, err := parseRoomConfig(data) if err != nil { return nil, err } r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent), Mode: "plan", Status: StatusPending} projectRoot := filepath.Dir(agentsDir) if data, err := os.ReadFile(filepath.Join(projectRoot, "SYSTEM.md")); err == nil { r.systemRules = string(data) } // 从 DB 预加载所有 agent 配置 var dbConfigs []store.AgentConfig if lo.store != nil { dbConfigs, _ = lo.store.GetRoomAgentConfigs(cfg.Name) } dbConfigMap := make(map[string]map[string]string) // agent_name -> file_type -> content for _, c := range dbConfigs { if dbConfigMap[c.AgentName] == nil { dbConfigMap[c.AgentName] = make(map[string]string) } dbConfigMap[c.AgentName][c.FileType] = c.Content } if cfg.Master != "" { var knowledgeDir string if cfg.Team != "" { kd := filepath.Join(agentsDir, cfg.Team, "knowledge") if info, err := os.Stat(kd); err == nil && info.IsDir() { knowledgeDir = kd } } agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master) r.master, err = agent.Load(agentPath) if err != nil { return nil, fmt.Errorf("load master %s: %w", cfg.Master, err) } r.master.KnowledgeDir = knowledgeDir // DB 配置覆盖 if files, ok := dbConfigMap[cfg.Master]; ok { if soul, ok := files["soul"]; ok { r.master.Soul = soul r.master.UseDBConfig = true } if agentDoc, ok := files["agent"]; ok { r.master.AgentDoc = agentDoc r.master.UseDBConfig = true } } for _, name := range cfg.Members { a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name)) if err != nil { return nil, fmt.Errorf("load member %s: %w", name, err) } a.KnowledgeDir = knowledgeDir // DB 配置覆盖 if files, ok := dbConfigMap[name]; ok { if soul, ok := files["soul"]; ok { a.Soul = soul a.UseDBConfig = true } if agentDoc, ok := files["agent"]; ok { a.AgentDoc = agentDoc a.UseDBConfig = true } } r.members[name] = a } } // 加载 TEAM.md 项目模板:优先从 DB,降级到文件系统 teamMDBody := "" if teamDoc, ok := dbConfigMap["__team__"]["team_doc"]; ok && teamDoc != "" { teamMDBody = teamDoc } else if cfg.Team != "" { teamMDPath := filepath.Join(agentsDir, cfg.Team, "TEAM.md") if teamData, err := os.ReadFile(teamMDPath); err == nil { teamMDBody = string(teamData) } } if teamMDBody != "" { body := teamMDBody if strings.HasPrefix(body, "---") { parts := strings.SplitN(body, "---", 3) if len(parts) >= 3 { body = parts[2] } } if pt := parseProjectTemplate(body); pt != nil { r.projectTemplate = pt log.Printf("[room %s] 已加载项目模板,包含 %d 个文件定义", cfg.Name, len(pt.Files)) } if wf := extractTeamWorkflow(body); wf != "" { r.teamWorkflow = wf log.Printf("[room %s] 已加载团队工作流描述 (%d 字符)", cfg.Name, len(wf)) } } r.skillMeta, _ = skill.Discover(skillsDir) // 初始化 prompt 模板引擎 r.Prompt = prompt.New() // 加载 room 级模板覆盖(如果存在) roomPromptDir := filepath.Join(roomDir, "prompts") r.Prompt.LoadOverrides(roomPromptDir) return r, nil } func (r *Room) emit(e Event) { e.RoomID = r.Config.Name if r.Broadcast != nil { r.Broadcast(e) } if r.Store != nil && !e.NoStore { gid := &r.currentGroupID switch e.Type { case EvtAgentMessage: if !e.Streaming && e.Content != "" && e.Role != "tool_use" && !strings.HasPrefix(e.Content, "正在处理:") && !strings.HasPrefix(e.Content, "正在处理: ") { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: e.Agent, Role: e.Role, Content: e.Content, GroupID: gid, }) } case EvtArtifact: r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: e.Agent, Role: "artifact", Content: e.Title, Filename: e.Filename, Title: e.Title, GroupID: gid, }) case EvtTaskAssign: r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: e.From, Role: "task_assign", Content: e.Task, Title: e.To, GroupID: gid, }) } } } func (r *Room) emitUsage(agentName string, usage llm.Usage) { if usage.TotalTokens > 0 { r.emit(Event{ Type: EvtTokenUsage, Agent: agentName, PromptTokens: usage.PromptTokens, CompletionTokens: usage.CompletionTokens, TotalTokens: usage.TotalTokens, }) if r.Store != nil { r.Store.InsertTokenUsage(r.Config.Name, agentName, usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens) } } } func (r *Room) setMode(mode string) { r.Mode = mode r.emit(Event{Type: EvtModeChange, Mode: mode}) } func (r *Room) setStatus(s Status, activeAgent, action string) { r.Status = s r.ActiveAgent = activeAgent r.emit(Event{Type: EvtRoomStatus, Status: s, ActiveAgent: activeAgent, Action: action}) } func (r *Room) Stop() { r.cancelMu.Lock() defer r.cancelMu.Unlock() if r.cancelFunc != nil { r.cancelFunc() r.cancelFunc = nil } r.setStatus(StatusPending, "", "") } // AppendHistory persists a message to today's history file. func (r *Room) AppendHistory(role, agentName, content string) { dir := filepath.Join(r.Dir, "history") os.MkdirAll(dir, 0755) filename := filepath.Join(dir, time.Now().Format("2006-01-02")+".md") line := fmt.Sprintf("\n**[%s] %s** (%s)\n\n%s\n", time.Now().Format("15:04:05"), agentName, role, content) f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() f.WriteString(line) } // contextMaxTokens 是模型上下文窗口限制(留 8K 给输出) const contextMaxTokens = 120000 // estimateTokens 估算消息列表的 token 数(中文约 1.5 token/字,英文约 0.75 token/word) func estimateTokens(msgs []llm.Message) int { total := 0 for _, m := range msgs { // 粗略估算:每个字符约 1 token(中英混合取均值) total += len([]rune(m.Content)) } return total } // compressMessages 当 token 数超限时,保留 system prompt + 最近的消息,中间部分压缩为摘要 func compressMessages(msgs []llm.Message, maxTokens int) []llm.Message { if len(msgs) <= 3 { return msgs } est := estimateTokens(msgs) if est <= maxTokens { return msgs } log.Printf("[context] 当前 %d tokens (估算), 超过 %d 限制, 压缩历史", est, maxTokens) // 保留: system(第0条) + 最近的消息 system := msgs[0] middle := msgs[1:] // 从后往前保留消息,直到 token 数在限制内 // 留 system 的 token + 安全余量 sysTokens := len([]rune(system.Content)) budget := maxTokens - sysTokens - 2000 // 留 2K 余量 var kept []llm.Message keptTokens := 0 for i := len(middle) - 1; i >= 0; i-- { msgTokens := len([]rune(middle[i].Content)) if keptTokens+msgTokens > budget { break } kept = append([]llm.Message{middle[i]}, kept...) keptTokens += msgTokens } // 被截掉的部分生成摘要提示 droppedCount := len(middle) - len(kept) if droppedCount > 0 { summary := llm.NewMsg("user", fmt.Sprintf("[系统提示] 之前有 %d 条对话消息因上下文长度限制已被压缩。请基于当前可见的消息继续工作。", droppedCount)) // NOTE: compressMessages 是无状态函数,无法访问 r.Prompt,保留 fmt 拼接 result := make([]llm.Message, 0, 2+len(kept)) result = append(result, system, summary) result = append(result, kept...) log.Printf("[context] 压缩完成: 保留 %d 条, 丢弃 %d 条, 估算 %d tokens", len(kept), droppedCount, estimateTokens(result)) return result } return msgs } // Handle processes a user message through master orchestration. func (r *Room) Handle(ctx context.Context, userMsg string) error { return r.HandleUserMessage(ctx, "user", userMsg) } // HandleUserMessage 处理用户消息。 func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error { if r.master == nil { return fmt.Errorf("room has no master agent configured") } r.AppendHistory("user", userName, userMsg) if r.Store != nil { id, _ := r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: userName, Role: "user", Content: userMsg, }) r.currentGroupID = id } ctx, cancel := context.WithCancel(ctx) r.cancelMu.Lock() r.cancelFunc = cancel r.cancelMu.Unlock() defer func() { r.cancelMu.Lock() r.cancelFunc = nil r.cancelMu.Unlock() }() // 检测用户直接 @master:去掉前缀,作为普通对话处理 if r.master != nil { masterName := r.master.Config.Name prefix := "@" + masterName trimmed := strings.TrimSpace(userMsg) if strings.HasPrefix(trimmed, prefix) { question := strings.TrimSpace(strings.TrimPrefix(trimmed, prefix)) if question != "" { // 清除 lastActiveMember,确保消息发给 master 而非成员 r.lastActiveMember = "" userMsg = question } } } // 检测用户直接 @agent(成员) userAssignments := parseUserMentions(userMsg, r.members) if len(userAssignments) > 0 { // 区分对话和任务分配:短消息/问候视为对话 if len(userAssignments) == 1 { for name, task := range userAssignments { if isConversational(task) { r.lastActiveMember = name return r.handleMemberConversation(ctx, userName, task) } } } return r.handleDirectAssign(ctx, userAssignments) } // Build 模式下执行暂存任务 if r.Mode == "build" && len(r.pendingAssignments) > 0 { log.Printf("[room %s] build 模式,执行 %d 个暂存任务", r.Config.Name, len(r.pendingAssignments)) assignments := r.pendingAssignments r.pendingAssignments = nil r.pendingPlanReply = "" skillXML := skill.ToXML(r.skillMeta) board := &SharedBoard{} r.setStatus(StatusWorking, "", "") r.runMembersParallel(ctx, assignments, board, skillXML) // 只在写作阶段(前置材料全部完成后)触发审读 if r.allStaticFilesDone() { r.runChallengeRound(ctx, board, skillXML) } r.setStatus(StatusPending, "", "") return nil } // Build 模式下成员对话续接 if r.Mode == "build" && r.lastActiveMember != "" { return r.handleMemberConversation(ctx, userName, userMsg) } r.setStatus(StatusThinking, "", "") // 构建 system prompt teamXML := r.buildTeamXML() skillXML := skill.ToXML(r.skillMeta) skillSummary := r.buildSkillSummary() var userXML string if r.User != nil { userXML = r.User.BuildUserXML() } extraContext := userXML + "\n\n" + teamXML + "\n\n" + skillSummary if r.systemRules != "" { extraContext = r.systemRules + "\n\n" + extraContext } if r.teamWorkflow != "" { extraContext = extraContext + "\n\n\n" + r.teamWorkflow + "\n" } if projectCtx := r.buildProjectContext(r.master.Config.Name); projectCtx != "" { extraContext = extraContext + "\n\n" + projectCtx } if wsCtx := r.buildWorkspaceContext(); wsCtx != "" { extraContext = extraContext + "\n\n" + wsCtx } systemPrompt := r.master.BuildSystemPrompt(extraContext) modeInfo := fmt.Sprintf("\n\n当前用户:%s\n当前模式:%s", userName, r.Mode) if r.Mode == "build" { modeInfo += "\n\n" + r.Prompt.R("mode_build_rule") } else { modeInfo += "\n\n" + r.Prompt.R("mode_plan_rule") } sysMsg := llm.NewMsg("system", systemPrompt+modeInfo) r.historyMu.Lock() if len(r.masterHistory) == 0 { r.masterHistory = []llm.Message{sysMsg} } else { r.masterHistory[0] = sysMsg } r.masterHistory = append(r.masterHistory, llm.NewMsg("user", userMsg)) // token 感知的历史压缩(替代简单的消息数截断) r.masterHistory = compressMessages(r.masterHistory, contextMaxTokens) masterMsgs := make([]llm.Message, len(r.masterHistory)) copy(masterMsgs, r.masterHistory) r.historyMu.Unlock() // Master 规划循环:简化为最多 3 轮(安全上限),每次用户消息通常只跑 1 轮 const maxIterations = 3 for iteration := 0; iteration < maxIterations; iteration++ { log.Printf("[room %s] master iteration %d, sending to LLM...", r.Config.Name, iteration) if r.projectTemplate != nil { if !r.masterCallerDecidedIteration(ctx, &masterMsgs, skillXML, iteration) { break } } else { if !r.masterLegacyIteration(ctx, &masterMsgs, skillXML, iteration) { break } } } r.setStatus(StatusPending, "", "") go r.updateMasterMemory(context.Background(), userMsg, masterMsgs) return nil } // masterCallerDecidedIteration 执行一次 Caller-Decided 路径的 master 迭代。 // 返回 true 表示继续循环,false 表示 break。 func (r *Room) masterCallerDecidedIteration(ctx context.Context, masterMsgs *[]llm.Message, skillXML string, iteration int) bool { if iteration > 0 { r.setStatus(StatusThinking, r.master.Config.Name, "正在规划...") } // 调用前压缩 context *masterMsgs = compressMessages(*masterMsgs, contextMaxTokens) // CHAT CALL: 纯规划+分配 var masterReply strings.Builder _, usage, err := r.master.ChatWithUsage(ctx, *masterMsgs, func(token string) { masterReply.WriteString(token) r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true}) }) if err != nil { log.Printf("[room %s] master chat error: %v", r.Config.Name, err) return false } r.emitUsage(r.master.Config.Name, usage) reply := masterReply.String() log.Printf("[room %s] master chat reply (%d chars): %.100s...", r.Config.Name, len(reply), reply) // 安全网:如果 master 的聊天回复中混入了文档正文,剥离并替换显示 persistContent := reply if r.projectTemplate != nil && isDocument(reply) { stripped := r.stripDocuments(reply) if stripped != "" && stripped != reply { persistContent = stripped r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: stripped, Streaming: false, Action: "replace"}) log.Printf("[room %s] master 聊天中混入文档内容,已剥离", r.Config.Name) } else { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false}) } } else { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false}) } // 存为 text part if r.Store != nil { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master", Content: persistContent, PartType: "text", GroupID: &r.currentGroupID, }) } assistantMsg := llm.NewMsg("assistant", reply) *masterMsgs = append(*masterMsgs, assistantMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, assistantMsg) r.historyMu.Unlock() r.AppendHistory("master", r.master.Config.Name, reply) // Plan 模式下:master 只允许聊天,禁止一切文件操作和任务分配 if r.Mode != "build" { // 检查是否有 @分配 → 提醒用户切换模式 allMentions := parseAssignments(reply) hasAssignment := false for name := range allMentions { if _, isMember := r.members[name]; isMember { hasAssignment = true break } } if hasAssignment { r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: r.Prompt.R("plan_mode_blocked")}) } return false // plan 模式下每次只回复一条,不循环 } // 增量编辑(仅 build 模式) if editFile, edited := r.applyDocumentEdit(reply); edited { editTitle := strings.TrimSuffix(editFile, ".md") r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: editFile, Title: editTitle}) if r.Store != nil { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master", Content: editTitle, Filename: editFile, PartType: "document", GroupID: &r.currentGroupID, }) } } // 安全网:如果 master 在聊天中直接输出了文档内容,自动拦截保存 if isDocument(reply) && r.allStaticFilesDone() { if dynDir, dynOwner, _ := r.getDynamicFileInfo(); dynDir != "" && dynOwner == r.master.Config.Name { chapterFilename := r.extractChapterFilename(reply, dynDir) content := strings.TrimSpace(reply) if !strings.HasPrefix(content, "# ") { content = "# " + strings.TrimSuffix(chapterFilename, ".md") + "\n\n" + content } r.saveWorkspace(chapterFilename, content) docName := strings.TrimSuffix(chapterFilename, ".md") r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: chapterFilename, Title: docName}) if r.Store != nil { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master", Content: docName, Filename: chapterFilename, PartType: "document", GroupID: &r.currentGroupID, }) } log.Printf("[room %s] 拦截 master 聊天中的章节内容,保存到 %s", r.Config.Name, chapterFilename) } } // 解析 @ 分配 allMentions := parseAssignments(reply) assignments := make(map[string]string) for name, task := range allMentions { if _, isMember := r.members[name]; isMember { assignments[name] = task } } // Phase 轻量校验:只在分配写手(phase:2 任务)时检查前置材料是否就绪 if len(assignments) > 0 && r.projectTemplate != nil { blocked := r.validatePhaseAssignments(assignments) if len(blocked) > 0 { var blockedList strings.Builder for name, reason := range blocked { blockedList.WriteString(fmt.Sprintf("- @%s: %s\n", name, reason)) delete(assignments, name) } blockedContent := r.Prompt.Render("phase_blocked", map[string]string{ "BlockedList": blockedList.String(), }) r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: blockedContent}) log.Printf("[room %s] phase 校验阻止了 %d 个任务分配", r.Config.Name, len(blocked)) // 注入反馈让 master 知道被阻止了 phaseBlockMsg := llm.NewMsg("user", blockedContent) *masterMsgs = append(*masterMsgs, phaseBlockMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, phaseBlockMsg) r.historyMu.Unlock() } } if len(assignments) > 0 && r.Mode != "build" { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: r.Prompt.R("plan_mode_blocked")}) return false } // 执行成员任务 if len(assignments) > 0 && r.Mode == "build" { board := &SharedBoard{} results := r.runMembersParallel(ctx, assignments, board, skillXML) // 只在写作阶段(前置材料全部完成后)触发审读 if r.allStaticFilesDone() { r.runChallengeRound(ctx, board, skillXML) } r.setStatus(StatusThinking, r.master.Config.Name, "正在审阅成员结果...") var resultsStr strings.Builder for memberName, result := range results { resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result)) } feedbackMsg := r.Prompt.Render("feedback_results", map[string]string{ "Results": resultsStr.String(), "BoardContext": board.ToContext(), "WorkspaceContext": r.buildWorkspaceContext(), "WorkflowStep": r.buildWorkflowStep(), }) feedbackLLMMsg := llm.NewMsg("user", feedbackMsg) *masterMsgs = append(*masterMsgs, feedbackLLMMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, feedbackLLMMsg) r.historyMu.Unlock() r.updateTasks(*masterMsgs) } // FILE UPDATE CALLS: master 提到要更新已存在的文件 if r.Mode == "build" { updateFiles := r.parseMasterUpdateIntent(reply) if len(updateFiles) > 0 { for _, file := range updateFiles { r.masterFileUpdateCall(ctx, masterMsgs, file, reply) } } } // FILE CALLS: master 负责的文件 pendingFiles := r.findPendingMasterFiles() if len(pendingFiles) > 0 && r.Mode == "build" { for _, file := range pendingFiles { r.masterFileCall(ctx, masterMsgs, file) } } else if r.Mode == "build" && r.allStaticFilesDone() { // 所有静态文件完成,检查是否有动态章节需要写 if dynDir, dynOwner, _ := r.getDynamicFileInfo(); dynDir != "" && dynOwner == r.master.Config.Name { // 用 master 的聊天回复作为章节规划提示 r.masterChapterFileCall(ctx, masterMsgs, dynDir, reply) } } // 每次用户消息最多执行一轮分配 + 一次 file call,然后停下等用户 return false } // masterLegacyIteration 执行一次旧路径的 master 迭代(无项目模板)。 func (r *Room) masterLegacyIteration(ctx context.Context, masterMsgs *[]llm.Message, skillXML string, iteration int) bool { if iteration > 0 { r.setStatus(StatusWorking, r.master.Config.Name, "正在编写文档...") } // 调用前压缩 context *masterMsgs = compressMessages(*masterMsgs, contextMaxTokens) var masterReply strings.Builder _, usage, err := r.master.ChatWithUsage(ctx, *masterMsgs, func(token string) { masterReply.WriteString(token) r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: token, Streaming: true}) }) if err != nil { log.Printf("[room %s] master chat error: %v", r.Config.Name, err) return false } r.emitUsage(r.master.Config.Name, usage) reply := masterReply.String() log.Printf("[room %s] master reply (%d chars): %.100s...", r.Config.Name, len(reply), reply) // Plan 模式下:只聊天,禁止一切后续操作 if r.Mode != "build" { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false}) if r.Store != nil { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master", Content: reply, PartType: "text", GroupID: &r.currentGroupID, }) } assistantMsg := llm.NewMsg("assistant", reply) *masterMsgs = append(*masterMsgs, assistantMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, assistantMsg) r.historyMu.Unlock() r.AppendHistory("master", r.master.Config.Name, reply) // 检查是否尝试了分配或文档 allMentions := parseAssignments(reply) hasAssignment := false for name := range allMentions { if _, isMember := r.members[name]; isMember { hasAssignment = true break } } if hasAssignment { r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: r.Prompt.R("plan_mode_blocked")}) } return false } var savedDocTitles []string persistContent := reply // Build 模式下执行文件产出 { if editFile, edited := r.applyDocumentEdit(reply); edited { persistContent = fmt.Sprintf("已更新《%s》", strings.TrimSuffix(editFile, ".md")) savedDocTitles = append(savedDocTitles, strings.TrimSuffix(editFile, ".md")) } else if docs := splitDocuments(reply); len(docs) > 0 { for _, doc := range docs { title := extractTitle(doc) filename := titleToFilename(title, r.master.Config.Name) r.saveWorkspace(filename, doc) r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: filename, Title: title}) savedDocTitles = append(savedDocTitles, title) } } } if len(savedDocTitles) > 0 { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: persistContent, Streaming: false, Action: "replace"}) } else { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: "", Streaming: false}) } if r.Store != nil && persistContent != "" { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: r.master.Config.Name, Role: "master", Content: persistContent, PartType: "text", GroupID: &r.currentGroupID, }) } assistantMsg := llm.NewMsg("assistant", reply) *masterMsgs = append(*masterMsgs, assistantMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, assistantMsg) r.historyMu.Unlock() r.AppendHistory("master", r.master.Config.Name, reply) allMentions := parseAssignments(reply) assignments := make(map[string]string) for name, task := range allMentions { if _, isMember := r.members[name]; isMember { assignments[name] = task } } if len(assignments) == 0 { if len(savedDocTitles) > 0 && r.Mode == "build" { continueMsg := r.Prompt.R("continue_legacy") continueLLMMsg := llm.NewMsg("user", continueMsg) *masterMsgs = append(*masterMsgs, continueLLMMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, continueLLMMsg) r.historyMu.Unlock() return true // continue } return false // break } if r.Mode != "build" { r.emit(Event{Type: EvtAgentMessage, Agent: r.master.Config.Name, Role: "master", Content: r.Prompt.R("plan_mode_blocked")}) return false } board := &SharedBoard{} results := r.runMembersParallel(ctx, assignments, board, skillXML) // 只在写作阶段(前置材料全部完成后)触发审读 if r.allStaticFilesDone() { r.runChallengeRound(ctx, board, skillXML) } r.setStatus(StatusThinking, r.master.Config.Name, "正在审阅成员结果...") var resultsStr strings.Builder for memberName, result := range results { resultsStr.WriteString(fmt.Sprintf("[%s] %s\n", memberName, result)) } var wsFilesList string if wsFiles := r.listWorkspaceFiles(); len(wsFiles) > 0 { wsFilesList = "\n\n当前产出物文件:\n" for _, f := range wsFiles { wsFilesList += "- " + f + "\n" } } feedbackMsg := r.Prompt.Render("feedback_legacy", map[string]string{ "Results": resultsStr.String(), "WorkspaceFiles": wsFilesList, }) feedbackLLMMsg := llm.NewMsg("user", feedbackMsg) *masterMsgs = append(*masterMsgs, feedbackLLMMsg) r.historyMu.Lock() r.masterHistory = append(r.masterHistory, feedbackLLMMsg) r.historyMu.Unlock() r.updateTasks(*masterMsgs) return true // continue } // handleMemberConversation 处理用户与成员的对话续接 func (r *Room) handleMemberConversation(ctx context.Context, userName, userMsg string) error { memberName := r.lastActiveMember member, ok := r.members[memberName] if !ok { return fmt.Errorf("member %s not found", memberName) } log.Printf("[room %s] 用户与 %s 对话", r.Config.Name, memberName) r.setStatus(StatusWorking, member.Config.Name, "") if r.memberConvos == nil { r.memberConvos = make(map[string][]llm.Message) } // 如果该成员没有现有对话上下文,初始化系统提示 if len(r.memberConvos[memberName]) == 0 { extraCtx := r.buildTeamXML() if r.systemRules != "" { extraCtx = r.systemRules + "\n\n" + extraCtx } if wsCtx := r.buildWorkspaceContext(); wsCtx != "" { extraCtx += "\n\n" + wsCtx } systemPrompt := member.BuildSystemPrompt(extraCtx) r.memberConvos[memberName] = []llm.Message{ llm.NewMsg("system", systemPrompt+"\n\n"+r.Prompt.R("member_conversation")), } } r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("user", userMsg)) var memberReply strings.Builder _, usage, err := member.ChatWithUsage(ctx, r.memberConvos[memberName], func(token string) { memberReply.WriteString(token) r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: token, Streaming: true}) }) if err != nil { r.setStatus(StatusPending, "", "") return err } r.emitUsage(memberName, usage) result := memberReply.String() // 结束流式 r.emit(Event{Type: EvtAgentMessage, Agent: memberName, Role: "member", Content: "", Streaming: false}) r.memberConvos[memberName] = append(r.memberConvos[memberName], llm.NewMsg("assistant", result)) r.AppendHistory("member", memberName, result) // 存储到数据库 if r.Store != nil && strings.TrimSpace(result) != "" { r.Store.InsertMessage(&store.Message{ RoomID: r.Config.Name, Agent: memberName, Role: "member", Content: result, PartType: "text", GroupID: &r.currentGroupID, }) } _, routed := r.saveAgentOutput(memberName, result, "") if routed { r.lastActiveMember = "" } else if !isDocument(result) { // lastActiveMember 保持不变,用户可继续对话 } else { r.lastActiveMember = "" } r.setStatus(StatusPending, "", "") return nil } // handleDirectAssign 处理用户直接 @agent 指派的任务 func (r *Room) handleDirectAssign(ctx context.Context, assignments map[string]string) error { // Plan 模式下阻止任务执行 if r.Mode != "build" { r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: r.Prompt.R("plan_mode_blocked")}) r.setStatus(StatusPending, "", "") return nil } skillXML := skill.ToXML(r.skillMeta) board := &SharedBoard{} r.runMembersParallel(ctx, assignments, board, skillXML) // 只在写作阶段(前置材料全部完成后)触发审读 if r.allStaticFilesDone() { r.runChallengeRound(ctx, board, skillXML) } r.setStatus(StatusPending, "", "") return nil }