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/skill" "github.com/sdaduanbilei/agent-team/internal/store" ) func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) { 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) } 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 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 r.members[name] = a } } if cfg.Team != "" { teamMDPath := filepath.Join(agentsDir, cfg.Team, "TEAM.md") if teamData, err := os.ReadFile(teamMDPath); err == nil { body := string(teamData) 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)) } } } r.skillMeta, _ = skill.Discover(skillsDir) 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)) 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() }() // 检测用户直接 @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) 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 projectCtx := r.buildProjectContext(r.master.Config.Name); projectCtx != "" { extraContext = extraContext + "\n\n" + projectCtx } systemPrompt := r.master.BuildSystemPrompt(extraContext) sysMsg := llm.NewMsg("system", systemPrompt+fmt.Sprintf("\n\n当前用户:%s\n当前模式:%s", userName, r.Mode)) 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 规划循环 executedMembers := make(map[string]int) // 成员名 → 已执行的 phase,防止同 phase 重复分配 for iteration := 0; iteration < 12; 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, executedMembers) { 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, executedMembers map[string]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) 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: 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) // 增量编辑 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 r.Mode == "build" && isDocument(reply) && r.allStaticFilesDone() { if dynDir, dynOwner, _ := r.getDynamicFileInfo(); dynDir != "" && dynOwner == r.master.Config.Name { chapterFilename := r.extractChapterFilename(reply, dynDir) fullPath := dynDir + "/" + chapterFilename content := strings.TrimSpace(reply) if !strings.HasPrefix(content, "# ") { content = "# " + strings.TrimSuffix(chapterFilename, ".md") + "\n\n" + content } r.saveWorkspace(fullPath, content) docName := strings.TrimSuffix(chapterFilename, ".md") r.emit(Event{Type: EvtArtifact, Agent: r.master.Config.Name, Filename: fullPath, 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: fullPath, PartType: "document", GroupID: &r.currentGroupID, }) } log.Printf("[room %s] 拦截 master 聊天中的章节内容,保存到 %s", r.Config.Name, fullPath) } } // 解析 @ 分配 allMentions := parseAssignments(reply) assignments := make(map[string]string) for name, task := range allMentions { if _, isMember := r.members[name]; isMember { assignments[name] = task } } // 去重:过滤本轮循环中已执行过同 phase 任务的成员 for name := range assignments { if prevPhase, done := executedMembers[name]; done { targetFile := r.findMemberTargetFile(name) if targetFile != nil && targetFile.Phase == prevPhase { log.Printf("[room %s] 跳过重复分配: %s(phase %d 已执行)", r.Config.Name, name, prevPhase) delete(assignments, name) } } } // Phase 强制校验:阻止跨 phase 分配任务 if len(assignments) > 0 && r.projectTemplate != nil { blocked := r.validatePhaseAssignments(assignments) if len(blocked) > 0 { var blockedMsg strings.Builder blockedMsg.WriteString("[系统] 以下任务被阻止,因为前置阶段尚未完成:\n") for name, reason := range blocked { blockedMsg.WriteString(fmt.Sprintf("- @%s: %s\n", name, reason)) delete(assignments, name) } blockedMsg.WriteString("\n请先完成当前阶段的工作,再推进下一阶段。") r.emit(Event{Type: EvtAgentMessage, Agent: "system", Role: "master", Content: blockedMsg.String()}) log.Printf("[room %s] phase 校验阻止了 %d 个任务分配", r.Config.Name, len(blocked)) // 注入反馈让 master 知道被阻止了 phaseBlockMsg := llm.NewMsg("user", blockedMsg.String()) *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: "当前是 Plan 模式,无法执行任务。请切换到 Build 模式后发送消息开始执行。"}) return false } // 执行成员任务 if len(assignments) > 0 && r.Mode == "build" { // 标记本轮已执行的成员及其 phase for name := range assignments { if tf := r.findMemberTargetFile(name); tf != nil { executedMembers[name] = tf.Phase } else { executedMembers[name] = 0 } } board := &SharedBoard{} results := r.runMembersParallel(ctx, assignments, board, skillXML) 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 := "Team results:\n" + resultsStr.String() if boardCtx := board.ToContext(); boardCtx != "" { feedbackMsg += "\n\nTeam board:\n" + boardCtx } if wsCtx := r.buildWorkspaceContext(); wsCtx != "" { feedbackMsg += "\n\n" + wsCtx } if stepCtx := r.buildWorkflowStep(); stepCtx != "" { feedbackMsg = stepCtx + "\n\n" + feedbackMsg } feedbackMsg += "\n\n请审查成员结果,然后用 @成员名 分配下一步任务。不要在回复中输出完整文档正文,你负责的文件由系统自动发起调用。\n注意:简短回复,不要重复你上一条消息的内容。" // 提醒更新 TodoList if r.hasTodoList() { feedbackMsg += "\n\n如果有任务完成,请用 <<>> 格式更新 TodoList,标记已完成的项目。" } 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 CALLS: master 负责的文件 pendingFiles := r.findPendingMasterFiles() chapterWritten := false 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) chapterWritten = true } } // Plan 模式下不循环,每次只回复一条等用户 if r.Mode != "build" { return false } // 章节写完后暂停,等用户确认再继续写下一章 if chapterWritten { return false // break,等用户说"继续" } // 有成员任务执行过:feedbackMsg 已注入(含 workflow step + 下一步指令),直接 continue // 不再注入 continueMsg,避免 master 收到重复的"请分配任务"提示 if len(assignments) > 0 { return true // continue,master 根据 feedbackMsg 决定下一步 } // 只有 file call(无成员任务):注入 continue 提示 if len(pendingFiles) > 0 { if stepCtx := r.buildWorkflowStep(); stepCtx != "" { continueMsg := stepCtx + "\n\n文档已完成。请继续推进下一阶段。\n注意:简短回复,不要重复之前说过的内容。" 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 } // 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) var savedDocTitles []string persistContent := reply 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 := "文档已保存到 workspace。请根据工作流程,用 @成员名 分配下一步任务。不要重复输出文档内容。" 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: "当前是 Plan 模式,无法执行任务。请切换到 Build 模式后发送消息开始执行。"}) return false } board := &SharedBoard{} results := r.runMembersParallel(ctx, assignments, board, skillXML) 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 := "Team results:\n" + resultsStr.String() if wsFiles := r.listWorkspaceFiles(); len(wsFiles) > 0 { feedbackMsg += "\n\n当前产出物文件:\n" for _, f := range wsFiles { feedbackMsg += "- " + f + "\n" } } feedbackMsg += "\n\n请审查结果并决定下一步行动。" 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.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 { skillXML := skill.ToXML(r.skillMeta) board := &SharedBoard{} r.runMembersParallel(ctx, assignments, board, skillXML) r.runChallengeRound(ctx, board, skillXML) r.setStatus(StatusPending, "", "") return nil }