sdaduanbilei 8cb4e041c8 fix
2026-03-09 17:38:43 +08:00

245 lines
7.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/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()
}