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

215 lines
6.2 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 (
"fmt"
"regexp"
"strconv"
"strings"
"sync"
"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"
"github.com/sdaduanbilei/agent-team/internal/user"
)
type RoomType string
const (
TypeProject RoomType = "project"
)
type Status string
const (
StatusPending Status = "pending"
StatusThinking Status = "thinking"
StatusWorking Status = "working"
)
type Config struct {
Name string `yaml:"name"`
Type RoomType `yaml:"type"`
Master string `yaml:"master"` // agent name
Members []string `yaml:"members"` // agent names
Color string `yaml:"color"` // avatar color
Team string `yaml:"team"` // installed team name
}
type Room struct {
Config Config
Dir string
master *agent.Agent
members map[string]*agent.Agent
skillMeta []skill.Meta
User *user.User
Status Status
ActiveAgent string // for working status display
Broadcast func(Event) // set by api layer
// master 的会话历史,保持多轮对话上下文
masterHistory []llm.Message
historyMu sync.Mutex
Mode string // "plan" | "build"
pendingAssignments map[string]string // plan 模式下暂存的待执行任务
pendingPlanReply string // master 规划原文,用于生成计划文档
// Build 模式下成员对话跟踪
memberConvos map[string][]llm.Message // 成员名 -> 多轮对话历史
memberArtifacts map[string]string // 成员名 -> 已产出的文档文件名(用于后续覆盖更新)
lastActiveMember string // 最后一个发出提问的成员
planFilename string // 当前任务计划文件名
systemRules string // SYSTEM.md 全局规则
projectTemplate *ProjectTemplate // 项目模板(可为 nil
Store *store.Store
currentGroupID int64 // 当前用户消息的 group_id
cancelFunc func()
cancelMu sync.Mutex
}
type EventType string
const (
EvtAgentMessage EventType = "agent_message"
EvtTaskAssign EventType = "task_assign"
EvtReview EventType = "review"
EvtRoomStatus EventType = "room_status"
EvtTasksUpdate EventType = "tasks_update"
EvtWorkspaceFile EventType = "workspace_file"
EvtModeChange EventType = "mode_change"
EvtArtifact EventType = "artifact"
EvtTaskDone EventType = "task_done"
EvtScheduleRun EventType = "schedule_run"
EvtTokenUsage EventType = "token_usage"
EvtFileRead EventType = "file_read"
)
type Event struct {
Type EventType `json:"type"`
RoomID string `json:"room_id"`
Agent string `json:"agent,omitempty"`
Role string `json:"role,omitempty"` // master | member | challenge
Content string `json:"content,omitempty"`
Streaming bool `json:"streaming,omitempty"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Task string `json:"task,omitempty"`
Feedback string `json:"feedback,omitempty"`
Status Status `json:"status,omitempty"`
ActiveAgent string `json:"active_agent,omitempty"`
Action string `json:"action,omitempty"`
Filename string `json:"filename,omitempty"`
Mode string `json:"mode,omitempty"`
Title string `json:"title,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
NoStore bool `json:"-"` // 跳过 emit 中的自动 DB 存储(调用方已显式存储)
}
// ProjectFile 项目模板中的单个文件
type ProjectFile struct {
Path string // 如 "创作需求书.md"
Owner string // 负责的 agent 名
Phase int // 阶段编号
IsDir bool // 是否为目录
Dynamic bool // ... 标记,可动态扩展
}
// ProjectTemplate 从 TEAM.md 解析的项目模板
type ProjectTemplate struct {
Files []ProjectFile
}
// parseProjectTemplate 从 TEAM.md body 中提取 project-template 代码块并解析
func parseProjectTemplate(body string) *ProjectTemplate {
re := regexp.MustCompile("(?s)```project-template\\s*\\n(.+?)```")
match := re.FindStringSubmatch(body)
if match == nil {
return nil
}
block := match[1]
lineRe := regexp.MustCompile(`[├└─│\s]+(.+?)\.md\s+@(\S+)\s*(?:phase:(\d+))?`)
dirRe := regexp.MustCompile(`[├└─│\s]+(.+?)/\s*$`)
dynamicRe := regexp.MustCompile(`[├└─│\s]+\.\.\.\s+@(\S+)\s*(?:phase:(\d+))?`)
var files []ProjectFile
for _, line := range strings.Split(block, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "workspace/") {
continue
}
if m := dynamicRe.FindStringSubmatch(line); m != nil {
phase := 0
if m[2] != "" {
phase, _ = strconv.Atoi(m[2])
}
files = append(files, ProjectFile{
Path: "...", Owner: m[1], Phase: phase, Dynamic: true,
})
continue
}
if m := dirRe.FindStringSubmatch(line); m != nil {
files = append(files, ProjectFile{
Path: m[1] + "/", IsDir: true,
})
continue
}
if m := lineRe.FindStringSubmatch(line); m != nil {
phase := 0
if m[3] != "" {
phase, _ = strconv.Atoi(m[3])
}
files = append(files, ProjectFile{
Path: strings.TrimSpace(m[1]) + ".md", Owner: m[2], Phase: phase,
})
}
}
if len(files) == 0 {
return nil
}
return &ProjectTemplate{Files: files}
}
type BoardEntry struct {
Author string
Content string
Type string // "draft" | "challenge"
}
type SharedBoard struct {
mu sync.RWMutex
entries []BoardEntry
}
func (b *SharedBoard) Add(author, content, typ string) {
b.mu.Lock()
defer b.mu.Unlock()
b.entries = append(b.entries, BoardEntry{Author: author, Content: content, Type: typ})
}
func (b *SharedBoard) ToContext() string {
b.mu.RLock()
defer b.mu.RUnlock()
if len(b.entries) == 0 {
return ""
}
var sb strings.Builder
sb.WriteString("<team_board>\n")
for _, e := range b.entries {
fmt.Fprintf(&sb, " <entry type=\"%s\" author=\"%s\">\n%s\n </entry>\n", e.Type, e.Author, e.Content)
}
sb.WriteString("</team_board>")
return sb.String()
}