245 lines
7.2 KiB
Go
245 lines
7.2 KiB
Go
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/prompt"
|
||
"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)
|
||
teamWorkflow string // TEAM.md 中的流程描述(非 project-template 部分)
|
||
Prompt *prompt.Engine // 提示词模板引擎
|
||
|
||
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"
|
||
EvtFileWorking EventType = "file_working" // file-llm 开始生成文件
|
||
EvtFileDone EventType = "file_done" // file-llm 文件生成完成
|
||
)
|
||
|
||
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
|
||
}
|
||
|
||
// extractTeamWorkflow 提取 TEAM.md 中除 project-template 代码块外的流程描述
|
||
func extractTeamWorkflow(body string) string {
|
||
re := regexp.MustCompile("(?s)```project-template\\s*\\n.+?```")
|
||
cleaned := re.ReplaceAllString(body, "")
|
||
cleaned = strings.TrimSpace(cleaned)
|
||
if cleaned == "" {
|
||
return ""
|
||
}
|
||
return cleaned
|
||
}
|
||
|
||
// 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}
|
||
}
|
||
|
||
// LoadOption 用于 room.Load 的可选参数
|
||
type LoadOption func(*loadOptions)
|
||
|
||
type loadOptions struct {
|
||
store *store.Store
|
||
}
|
||
|
||
// WithStore 在加载时传入 store,用于从 DB 读取 agent 配置
|
||
func WithStore(s *store.Store) LoadOption {
|
||
return func(o *loadOptions) {
|
||
o.store = s
|
||
}
|
||
}
|
||
|
||
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()
|
||
}
|