scorpio e6e8bd8ce1 feat: phase强制校验、重复分配防护、章节file call、成员对话模式
- 系统级phase强制校验:阻止跨阶段分配任务,前置阶段未完成时自动拦截
- 循环内去重:同phase同成员不会被重复执行,消除master重复分配问题
- 消除重复提示:feedbackMsg和continueMsg不再同时注入
- 动态章节file call:所有静态文件完成后自动触发masterChapterFileCall
- 章节内容安全网拦截:master在聊天中输出文档内容时自动保存到workspace
- 用户@成员对话:区分对话和任务分配,短消息/问候走对话路由而非任务流水线
- handleMemberConversation改进:初始化系统提示、流式输出、DB存储
- 策划编辑AGENT.md:新增前置依赖检查、评分必须引用文档数据来源
- TodoList更新提醒:任务完成后提醒master用编辑指令更新TodoList
- buildWorkflowStep强化:显示阶段依赖关系和后续阶段解锁提示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 22:16:08 +08:00

749 lines
25 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 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] 跳过重复分配: %sphase %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<reminder>如果有任务完成,请用 <<<EDIT TodoList.md>>> 格式更新 TodoList标记已完成的项目。</reminder>"
}
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 // continuemaster 根据 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
}