package room import ( "bytes" "fmt" "os" "path/filepath" "strings" "time" "github.com/sdaduanbilei/agent-team/internal/agent" "gopkg.in/yaml.v3" ) // extractTitle 从文档内容中提取标题 func extractTitle(content string) string { for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "# ") { return strings.TrimPrefix(line, "# ") } } if idx := strings.Index(content, "《"); idx != -1 { if end := strings.Index(content[idx:], "》"); end != -1 { return content[idx+len("《") : idx+end] } } for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if strings.HasPrefix(line, "## ") { return strings.TrimPrefix(line, "## ") } } return "" } // titleToFilename 从文档标题生成文件名,如 "主角小传:林远" → "主角小传-林远.md" func titleToFilename(title, agentName string) string { if title == "" { return fmt.Sprintf("%s-%s.md", agentName, time.Now().Format("20060102-150405")) } name := strings.NewReplacer( ":", "-", ":", "-", "/", "-", "\\", "-", " ", "-", " ", "-", "*", "", "?", "", "\"", "", "<", "", ">", "", "|", "", ).Replace(title) for strings.Contains(name, "--") { name = strings.ReplaceAll(name, "--", "-") } name = strings.Trim(name, "-") if name == "" { return fmt.Sprintf("%s-%s.md", agentName, time.Now().Format("20060102-150405")) } return name + ".md" } func min(a, b int) int { if a < b { return a } return b } // parseAssignments 解析任务分配指令。 // 支持多行任务描述:从 @成员名 开始,到下一个 @成员名 或文本结束为止。 func parseAssignments(text string) map[string]string { result := make(map[string]string) lines := strings.Split(text, "\n") var currentName string var currentTask strings.Builder flush := func() { if currentName != "" { task := strings.TrimSpace(currentTask.String()) if task != "" { result[currentName] = task } } currentName = "" currentTask.Reset() } for _, line := range lines { trimmed := strings.TrimSpace(line) atIdx := strings.Index(trimmed, "@") if atIdx >= 0 { rest := trimmed[atIdx+1:] if idx := strings.IndexAny(rest, " \t"); idx > 0 { name := strings.TrimSpace(rest[:idx]) task := strings.TrimSpace(rest[idx+1:]) flush() currentName = name if task != "" { currentTask.WriteString(task) } continue } name := strings.TrimSpace(rest) if name != "" { flush() currentName = name continue } } if strings.HasPrefix(trimmed, "ASSIGN:") { parts := strings.SplitN(strings.TrimPrefix(trimmed, "ASSIGN:"), ":", 2) if len(parts) == 2 { flush() result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) } continue } if currentName != "" && trimmed != "" { if currentTask.Len() > 0 { currentTask.WriteString("\n") } currentTask.WriteString(trimmed) } } flush() return result } // parseUserMentions 从用户消息中提取 @agent 指派。 func parseUserMentions(text string, validMembers map[string]*agent.Agent) map[string]string { assignments := make(map[string]string) for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "@") { continue } rest := strings.TrimPrefix(line, "@") idx := strings.IndexAny(rest, " \t") if idx <= 0 { continue } name := strings.TrimSpace(rest[:idx]) task := strings.TrimSpace(rest[idx+1:]) if _, ok := validMembers[name]; ok && task != "" { assignments[name] = task } } return assignments } // isConversational 判断用户 @成员 后的文本是对话还是任务分配 func isConversational(text string) bool { text = strings.TrimSpace(text) runeLen := len([]rune(text)) // 非常短的消息视为对话 if runeLen < 10 { return true } // 问候/简短回复 lower := strings.ToLower(text) greetings := []string{"你好", "嗨", "hi", "hello", "hey", "谢谢", "感谢", "好的", "ok", "好", "嗯", "对", "是的", "不是", "怎么样", "什么"} for _, g := range greetings { if strings.HasPrefix(lower, g) { return true } } // 以问号结尾的短消息视为提问(对话) if runeLen < 30 && (strings.HasSuffix(text, "?") || strings.HasSuffix(text, "?")) { return true } return false } func parseRoomConfig(data []byte) (Config, error) { var cfg Config if !bytes.HasPrefix(data, []byte("---")) { return cfg, fmt.Errorf("missing frontmatter") } parts := bytes.SplitN(data, []byte("---"), 3) if len(parts) < 3 { return cfg, fmt.Errorf("invalid frontmatter") } return cfg, yaml.Unmarshal(parts[1], &cfg) } // resolveAgentPath finds agent dir: prefers agentsDir/team/name, falls back to agentsDir/name func resolveAgentPath(agentsDir, team, name string) string { if team != "" { p := filepath.Join(agentsDir, team, name) if _, err := os.Stat(p); err == nil { return p } } return filepath.Join(agentsDir, name) }