Compare commits

..

2 Commits

Author SHA1 Message Date
scorpio
122ab6ef3e 0306 2026-03-06 10:07:16 +08:00
scorpio
b37e66b9a5 Add TEAM.md with metadata 2026-03-05 19:58:14 +08:00
24 changed files with 969 additions and 597 deletions

View File

@ -9,7 +9,10 @@
"Bash(npm install:*)", "Bash(npm install:*)",
"Bash(node:*)", "Bash(node:*)",
"Bash(npm run:*)", "Bash(npm run:*)",
"Bash(curl:*)" "Bash(curl:*)",
"Bash(cat:*)",
"Bash(grep:*)",
"Bash(echo DEEPSEEK_API_KEY=$DEEPSEEK_API_KEY:*)"
] ]
} }
} }

38
agents/legal-team/TEAM.md Normal file
View File

@ -0,0 +1,38 @@
---
name: 法律咨询团队
description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
author: Agent Team
version: 1.0.0
repo_url: https://gitea.catter.cn/agent-teams/legal-team.git
agents:
- 法律总监
- 合同律师
- 合规专员
skills:
- 合同审查
- 法律知识库
---
# 法律咨询团队
专业法律咨询团队,提供全方位的法律服务支持。
## 团队成员
- **法律总监**:团队负责人,协调各项法律事务,提供战略级法律建议
- **合同律师**:专注合同审查、起草和风险评估
- **合规专员**:负责合规检查、风险识别和合规建议
## 核心能力
- 合同审查与风险评估
- 法律咨询与建议
- 合规检查与指导
- 法律文档起草
## 使用场景
- 企业合同审查
- 法律风险评估
- 合规体系建设
- 法律咨询问答

View File

@ -0,0 +1,16 @@
## 2026-03-05 — 你好
ASSIGN:å<>ˆå<CB86>Œå¾å¸ˆˆ†æž<C3A6>用户"第五季"çš„å·¥ä½œé£Žæ ¼åŒæ²Ÿé€šå<C5A1><C3A5>好,整ç<C2B4>†æˆ<C3A6>简æ´<C3A6>è¦<C3A8>ç¹ï¼Œä¸ºå<C2BA>Žç»­æ³•å¾å¨è¯¢æ<C2A2><C3A6>ä¾å<E280BA>考框架ã€
ASSIGN:å<>ˆè§„专å˜:评估用户简ä»ä¸­å<C2AD>¯èƒ½æ¶‰å<E280B0>Šçš„æ³•å¾å<E280B9>ˆè§„风险ç¹ï¼ˆå¦æ²Ÿé€šæ¹å¼<C3A5>ã€<C3A3>æ•°æ<C2B0>®ä½¿ç”¨ç­‰ï¼‰ï¼Œæ<C592><C3A6>出åˆ<C3A5>步注æ„<C3A6>äºé¡¹ã€
## 2026-03-05 — æˆä¸€ä¸ªä¸­é—´äººï¼Œè¯´å<C2B4>¯ä»¥ç»™æˆä»ç»<C3A7>一个è¶
基于本次å¨è¯¢ï¼Œæ€»ç»“关键è¦<EFBFBD>ç¹å¦ä¸ï¼š
- **æ<>ƒå±žæ ¸æŸ¥æ˜¯åŸºçŸ³**:投资任何实体项ç®å‰<C3A5>,**å¿…é¡»**é¦å…ˆå®¡æŸ¥å¹¶ç¡®è®¤æ ¸å¿ƒèµ„产(å¦åœŸåœ°ã€<C3A3>场地)的完整ã€<C3A3>å<EFBFBD>ˆæ³•æ<E280A2>ƒå±žé“¾æ<C2BE>¡å<C2A1>Šä½¿ç”¨è®¸å<C2B8>¯ï¼Œæ<C592>ƒå±žä¸<C3A4>清是根本性风险ã€
- **中间人模å¼<C3A5>风险高**:通过无明确授æ<CB86>ƒçš„“中间人â€<C3A2>è¿è¡ŒæŠ•资,会导致法å¾å…³ç³»æ¨¡ç³Šã€<C3A3>资é‡å¤±æŽ§ï¼Œä¸”æž<C3A6>易与项ç®å®žé™…æ¹è„±èŠï¼Œåº”争å<E280B0>与æ<C5BD>ƒå±žæ¹å»ºç«ç´æŽ¥å<C2A5>ˆå<CB86>Œå…³ç³»ã€
- **å¼å¸¸é«˜åžæŠ¥æ˜¯å<C2AF>±é™©ä¿¡å<C2A1>·**:远高于å¸åœºå¹³å<C2B3>‡æ°´å¹³çš„åžæŠ¥æ‰¿è¯ºï¼ˆå¦å¹´åŒ30%)通常伴éš<C3A9>æž<C3A6>高的è¿<C3A8>约风险æˆé¡¹ç®æœ¬èº«ä¸<C3A4>å<EFBFBD>¯è¡Œï¼Œéœ€æ·±ç©¶å…¶å•†ä¸šå<C5A1>ˆç<CB86>†æ€§ä¸Žæ³•å¾å<E280B9>ˆè§„性ã€
- **å<><C3A5>议性质必须明确**:“投资å<E2809E><C3A5>è®®â€<C3A2>的法å¾å®šæ€§ï¼ˆå€Ÿè´·ã€<C3A3>å<EFBFBD>ˆä¼™æˆå§”æ‰˜ï¼‰ç´æŽ¥å†³å®šæ¨çš„æ<E2809E>ƒåˆ©ã€<C3A3>风险与责任,必须在æ<C2A8>¡æ¬¾ä¸­æ¸…晰界定,é<C592>¿å…<C3A5>å<EFBFBD>Žç»­äº‰è®®ã€
- **å<>ˆè§„红线ä¸<C3A4>å<EFBFBD>¯è§¦ç¢°**:涉å<E280B0>Šæ”¿åºœèµ„æº<C3A6>æˆèµ„产的项ç®ï¼Œè¥è¿<C3A8>作模å¼<C3A5>ä¾<C3A4>èµâ€œå…³ç³»â€<C3A2>而é<C592>žå…¬å¼€ç¨åº<C3A5>,æž<C3A6>易涉å<E280B0>Šå•†ä¸šè´¿èµã€<C3A3>è¿<C3A8>è§„èž<C3A8>资等åˆäºé£Žé™©ï¼Œå¿…é¡»å<C2BB>šæŒ<C3A6>å<EFBFBD>ˆæ³•é€<C3A9>明路径ã€

View File

@ -62,6 +62,7 @@ func (s *Server) routes() {
g := s.e.Group("/api") g := s.e.Group("/api")
g.GET("/rooms", s.listRooms) g.GET("/rooms", s.listRooms)
g.POST("/rooms", s.createRoom) g.POST("/rooms", s.createRoom)
g.PUT("/rooms/:id/team", s.setRoomTeam)
g.GET("/agents", s.listAgents) g.GET("/agents", s.listAgents)
g.GET("/agents/:name/files/:file", s.readAgentFile) g.GET("/agents/:name/files/:file", s.readAgentFile)
g.PUT("/agents/:name/files/:file", s.writeAgentFile) g.PUT("/agents/:name/files/:file", s.writeAgentFile)
@ -196,10 +197,13 @@ func (s *Server) listRooms(c echo.Context) error {
Type string `json:"type"` Type string `json:"type"`
Status room.Status `json:"status"` Status room.Status `json:"status"`
Master string `json:"master"` Master string `json:"master"`
Members []string `json:"members"`
Color string `json:"color"`
Team string `json:"team"`
} }
var list []roomInfo var list []roomInfo
for id, r := range s.rooms { for id, r := range s.rooms {
list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master}) list = append(list, roomInfo{ID: id, Name: r.Config.Name, Type: string(r.Config.Type), Status: r.Status, Master: r.Config.Master, Members: r.Config.Members, Color: r.Config.Color, Team: r.Config.Team})
} }
return c.JSON(200, list) return c.JSON(200, list)
} }
@ -209,16 +213,14 @@ func (s *Server) createRoom(c echo.Context) error {
if err := c.Bind(&cfg); err != nil { if err := c.Bind(&cfg); err != nil {
return err return err
} }
if cfg.Type == "" {
cfg.Type = room.TypeDept
}
dir := filepath.Join(s.roomsDir, cfg.Name) dir := filepath.Join(s.roomsDir, cfg.Name)
os.MkdirAll(filepath.Join(dir, "workspace"), 0755) os.MkdirAll(filepath.Join(dir, "workspace"), 0755)
os.MkdirAll(filepath.Join(dir, "history"), 0755) os.MkdirAll(filepath.Join(dir, "history"), 0755)
content := "---\nname: " + cfg.Name + "\ntype: " + string(cfg.Type) + "\nmaster: " + cfg.Master + "\nmembers:\n" s.writeRoomFile(dir, cfg)
for _, m := range cfg.Members {
content += " - " + m + "\n"
}
content += "---\n"
os.WriteFile(filepath.Join(dir, "room.md"), []byte(content), 0644)
r, err := room.Load(dir, s.agentsDir, s.skillsDir) r, err := room.Load(dir, s.agentsDir, s.skillsDir)
if err != nil { if err != nil {
@ -231,6 +233,76 @@ func (s *Server) createRoom(c echo.Context) error {
return c.JSON(201, map[string]string{"id": cfg.Name}) return c.JSON(201, map[string]string{"id": cfg.Name})
} }
func (s *Server) writeRoomFile(dir string, cfg room.Config) {
content := "---\nname: " + cfg.Name + "\ntype: " + string(cfg.Type) + "\nmaster: " + cfg.Master + "\ncolor: " + cfg.Color + "\nteam: " + cfg.Team + "\nmembers:\n"
for _, m := range cfg.Members {
content += " - " + m + "\n"
}
content += "---\n"
os.WriteFile(filepath.Join(dir, "room.md"), []byte(content), 0644)
}
func (s *Server) setRoomTeam(c echo.Context) error {
id := c.Param("id")
var body struct {
Team string `json:"team"`
}
if err := c.Bind(&body); err != nil {
return err
}
// Load team agents
teamDir := filepath.Join(s.teamsDir, body.Team)
team, err := loadTeam(filepath.Join(teamDir, "team.yaml"))
if err != nil {
return c.JSON(404, map[string]string{"error": "team not found"})
}
s.mu.Lock()
defer s.mu.Unlock()
r := s.rooms[id]
if r == nil {
return c.JSON(404, map[string]string{"error": "room not found"})
}
// Find master and members from team agents by reading AGENT.md
master := ""
var members []string
for _, agentName := range team.Agents {
agentMD, err := os.ReadFile(filepath.Join(s.agentsDir, body.Team, agentName, "AGENT.md"))
if err == nil && strings.Contains(string(agentMD), "role: master") {
master = agentName
} else {
members = append(members, agentName)
}
}
if master == "" && len(team.Agents) > 0 {
master = team.Agents[0]
members = team.Agents[1:]
}
cfg := r.Config
cfg.Team = body.Team
cfg.Master = master
cfg.Members = members
s.writeRoomFile(r.Dir, cfg)
newRoom, err := room.Load(r.Dir, s.agentsDir, s.skillsDir)
if err != nil {
return c.JSON(500, map[string]string{"error": err.Error()})
}
newRoom.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
newRoom.User = s.user
s.rooms[id] = newRoom
return c.JSON(200, map[string]interface{}{
"team": body.Team,
"master": master,
"members": members,
})
}
func (s *Server) listAgents(c echo.Context) error { func (s *Server) listAgents(c echo.Context) error {
entries, _ := os.ReadDir(s.agentsDir) entries, _ := os.ReadDir(s.agentsDir)
type agentInfo struct { type agentInfo struct {
@ -393,7 +465,6 @@ func (s *Server) getWorkspaceFile(c echo.Context) error {
func (s *Server) getMessages(c echo.Context) error { func (s *Server) getMessages(c echo.Context) error {
id := c.Param("id") id := c.Param("id")
// Read today's history file and parse into messages
historyFile := filepath.Join(s.roomsDir, id, "history", time.Now().Format("2006-01-02")+".md") historyFile := filepath.Join(s.roomsDir, id, "history", time.Now().Format("2006-01-02")+".md")
data, err := os.ReadFile(historyFile) data, err := os.ReadFile(historyFile)
if err != nil { if err != nil {
@ -406,30 +477,40 @@ func (s *Server) getMessages(c echo.Context) error {
Content string `json:"content"` Content string `json:"content"`
} }
var msgs []msg var msgs []msg
lines := strings.Split(string(data), "\n## ") // Format: "\n**[HH:MM:SS] agentName** (role)\n\ncontent\n"
for i, block := range lines { blocks := strings.Split(string(data), "\n**[")
for i, block := range blocks {
if block == "" { if block == "" {
continue continue
} }
// Parse "**[HH:MM:SS] agentName** (role)\n\ncontent" // block starts with "HH:MM:SS] agentName** (role)\n\ncontent"
parts := strings.SplitN(block, "\n\n", 2) headerEnd := strings.Index(block, "\n\n")
if len(parts) < 2 { if headerEnd < 0 {
continue continue
} }
header := parts[0] header := block[:headerEnd]
content := strings.TrimSpace(parts[1]) content := strings.TrimSpace(block[headerEnd+2:])
// Extract role and agent from header like "**[15:04:05] agentName** (role)" // header: "HH:MM:SS] agentName** (role)"
var agentName, role string bracketEnd := strings.Index(header, "] ")
fmt.Sscanf(header, "**[%*s] %s** (%s)", &agentName, &role) if bracketEnd < 0 {
agentName = strings.TrimSuffix(agentName, "**") continue
role = strings.TrimSuffix(role, ")") }
rest := header[bracketEnd+2:] // "agentName** (role)"
starIdx := strings.Index(rest, "**")
if starIdx < 0 {
continue
}
agentName := rest[:starIdx]
roleStr := strings.TrimSpace(rest[starIdx+2:]) // " (role)"
roleStr = strings.TrimPrefix(roleStr, "(")
roleStr = strings.TrimSuffix(roleStr, ")")
if agentName == "" { if agentName == "" {
agentName = "unknown" agentName = "unknown"
} }
if role == "" { if roleStr == "" {
role = "member" roleStr = "member"
} }
msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: role, Content: content}) msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: roleStr, Content: content})
} }
return c.JSON(200, msgs) return c.JSON(200, msgs)
} }
@ -529,6 +610,7 @@ func (s *Server) listTeams(c echo.Context) error {
continue continue
} }
teams = append(teams, map[string]interface{}{ teams = append(teams, map[string]interface{}{
"id": e.Name(),
"name": team.Name, "name": team.Name,
"description": team.Description, "description": team.Description,
"author": team.Author, "author": team.Author,
@ -650,22 +732,36 @@ func loadTeam(path string) (*hub.Team, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Simple YAML parse
team := &hub.Team{} team := &hub.Team{}
lines := strings.Split(string(data), "\n") lines := strings.Split(string(data), "\n")
var currentField string
for _, line := range lines { for _, line := range lines {
if strings.HasPrefix(line, "name: ") { if strings.HasPrefix(line, "name: ") {
team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: ")) team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: "))
currentField = ""
} else if strings.HasPrefix(line, "description: ") { } else if strings.HasPrefix(line, "description: ") {
team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: ")) team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: "))
currentField = ""
} else if strings.HasPrefix(line, "author: ") { } else if strings.HasPrefix(line, "author: ") {
team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: ")) team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: "))
currentField = ""
} else if strings.HasPrefix(line, "repo_url: ") { } else if strings.HasPrefix(line, "repo_url: ") {
team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: ")) team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: "))
currentField = ""
} else if strings.HasPrefix(line, "installed_at: ") { } else if strings.HasPrefix(line, "installed_at: ") {
team.InstalledAt = strings.TrimSpace(strings.TrimPrefix(line, "installed_at: ")) team.InstalledAt = strings.TrimSpace(strings.TrimPrefix(line, "installed_at: "))
currentField = ""
} else if strings.HasPrefix(line, "agents:") {
currentField = "agents"
} else if strings.HasPrefix(line, "skills:") {
currentField = "skills"
} else if strings.HasPrefix(line, " - ") { } else if strings.HasPrefix(line, " - ") {
team.Agents = append(team.Agents, strings.TrimSpace(strings.TrimPrefix(line, " - "))) item := strings.TrimSpace(strings.TrimPrefix(line, " - "))
if currentField == "agents" {
team.Agents = append(team.Agents, item)
} else if currentField == "skills" {
team.Skills = append(team.Skills, item)
}
} }
} }
return team, nil return team, nil

View File

@ -8,6 +8,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"gopkg.in/yaml.v3"
) )
type Team struct { type Team struct {
@ -108,10 +110,14 @@ func installFromDir(tmp, teamName, repoURL, agentsDir, skillsDir, teamsDir strin
// Create knowledge dir // Create knowledge dir
os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755) os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755)
// Read TEAM.md for metadata
teamMeta := readTeamMeta(tmp)
// Create team record // Create team record
team := &Team{ team := &Team{
Name: teamName, Name: teamMeta.Name,
Description: "团队描述", Description: teamMeta.Description,
Author: teamMeta.Author,
RepoURL: repoURL, RepoURL: repoURL,
Agents: installedAgents, Agents: installedAgents,
Skills: installedSkills, Skills: installedSkills,
@ -238,3 +244,30 @@ func copyDir(src, dst string) error {
} }
return nil return nil
} }
func readTeamMeta(tmp string) Team {
team := Team{}
teamFile := filepath.Join(tmp, "TEAM.md")
data, err := os.ReadFile(teamFile)
if err != nil {
return Team{Name: extractRepoName(tmp)}
}
content := string(data)
if strings.HasPrefix(content, "---") {
parts := strings.SplitN(content, "---", 3)
if len(parts) >= 3 {
yamlContent := parts[1]
yaml.Unmarshal([]byte(yamlContent), &team)
}
}
if team.Name == "" {
team.Name = extractRepoName(tmp)
}
if team.Description == "" {
team.Description = "团队描述"
}
return team
}

View File

@ -36,6 +36,8 @@ type Config struct {
Type RoomType `yaml:"type"` Type RoomType `yaml:"type"`
Master string `yaml:"master"` // agent name Master string `yaml:"master"` // agent name
Members []string `yaml:"members"` // agent names Members []string `yaml:"members"` // agent names
Color string `yaml:"color"` // avatar color
Team string `yaml:"team"` // installed team name
} }
type Room struct { type Room struct {
@ -90,17 +92,20 @@ func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent)} r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent)}
r.master, err = agent.Load(filepath.Join(agentsDir, cfg.Master)) if cfg.Master != "" {
agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master)
r.master, err = agent.Load(agentPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("load master %s: %w", cfg.Master, err) return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
} }
for _, name := range cfg.Members { for _, name := range cfg.Members {
a, err := agent.Load(filepath.Join(agentsDir, name)) a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name))
if err != nil { if err != nil {
return nil, fmt.Errorf("load member %s: %w", name, err) return nil, fmt.Errorf("load member %s: %w", name, err)
} }
r.members[name] = a r.members[name] = a
} }
}
r.skillMeta, _ = skill.Discover(skillsDir) r.skillMeta, _ = skill.Discover(skillsDir)
return r, nil return r, nil
@ -140,6 +145,9 @@ func (r *Room) Handle(ctx context.Context, userMsg string) error {
// HandleUserMessage processes a user message with a specific user name. // HandleUserMessage processes a user message with a specific user name.
func (r *Room) HandleUserMessage(ctx context.Context, userName, userMsg string) error { 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) r.AppendHistory("user", userName, userMsg)
r.setStatus(StatusThinking, "", "") r.setStatus(StatusThinking, "", "")
@ -319,3 +327,14 @@ func parseRoomConfig(data []byte) (Config, error) {
} }
return cfg, yaml.Unmarshal(parts[1], &cfg) 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)
}

BIN
main Executable file

Binary file not shown.

72
progress.md Normal file
View File

@ -0,0 +1,72 @@
# Project: Agent Team
Last updated: 2026-03-05
## Pinned (关键约束)
- 一切皆 MD — agent 配置、soul、memory、tasks、history 全部是 Markdown 文件
- 使用前必须设置环境变量 `DEEPSEEK_API_KEY` 或其他 LLM provider 的 API key
## Decisions (决策)
- 2026-03-05使用 Go + Echo + go-openai 作为后端技术栈
- 2026-03-05使用 React 19 + TypeScript + Tailwind v4 + Zustand 作为前端技术栈
- 2026-03-05采用纯文件系统存储无数据库
## Constraints (约束)
- 暂无
## TODO
### In Progress
- 暂无
### Pending
- 暂无
## Done
- 2026-03-05支持从 TEAM.md 读取团队元信息
- 修改 internal/hub/hub.go添加 readTeamMeta 函数
- 安装团队时从 TEAM.md 解析 name/description/author
- 代码变更:`files changed: 1`, `insertions: +29`, `deletions: -4`
- Evidence: internal/hub/hub.go:129-175
- 2026-03-05配置精选团队为营销团队和律师团队
- 代码变更:`files changed: 1`, `insertions: +20`, `deletions: -51`
- Evidence: web/src/components/MarketPage.tsx:18-37
- 2026-03-05创建营销团队 (marketing-team) 并提交到 Gitea
- 参考 legal-team 律师团队结构
- 包含 3 个 Agent营销总监(master)、市场分析师(member)、内容策划师(member)
- 提交地址https://gitea.catter.cn/agent-teams/marketing-team.git
- Evidence: agents/marketing-team/
- 2026-03-05修复后端 loadTeam 函数解析 skills 错误添加到 agents 的问题
- 代码变更:`files changed: 1`, `insertions: +16`, `deletions: -2`
- Evidence: internal/api/server.go:648-672
- 2026-03-05修复精选团队和我的团队 grid 风格不一致
- 代码变更:`files changed: 1`, `insertions: +26`, `deletions: -8`
- Evidence: web/src/components/MarketPage.tsx:388-432
- 2026-03-05替换 MDEditor 为简洁的 GitHub 风格 Markdown 编辑器
- 代码变更:`files changed: 4`, `insertions: +68`, `deletions: -45`
- Evidence: web/src/components/MarkdownEditor.tsx (new), TeamDetail.tsx, UserSettings.tsx, Onboarding.tsx
- 2026-03-05精选团队查看详情改为打开 GitHub 链接,我的团队查看详情打开本地详情
- 代码变更:`files changed: 2`, `insertions: +30`, `deletions: -15`
- Evidence: MarketPage.tsx, TeamDetail.tsx
- 2026-03-05精选团队添加"立即雇佣"按钮
- 代码变更:`files changed: 1`, `insertions: +25`, `deletions: -10`
- Evidence: MarketPage.tsx
- 2026-03-05侧边栏布局改为 Discord 风格 - 聊天按钮在上,工具栏(市场/主题/设置)在下
- 代码变更:`files changed: 1`, `insertions: +80`, `deletions: -20`
- Evidence: App.tsx
- 2026-03-05顶部 Logo 改为 Sparkles AI 图标,移除背景色
- 代码变更:`files changed: 1`, `insertions: +5`, `deletions: -3`
- Evidence: App.tsx
- 2026-03-05实施 Discord 风格三栏布局 - 左侧群列表 + 中间成员列表 + 聊天区域 + 右侧工作区
- 新建 MemberList.tsx、RightSidebar.tsx
- 修改 ChatView.tsx 移除内置右侧栏
- 代码变更:`files changed: 5`, `insertions: +350`, `deletions: -200`
- Evidence: App.tsx, MemberList.tsx (new), RightSidebar.tsx (new), ChatView.tsx
- 2026-03-05完成群聊创建和实时对话功能
- 支持创建新群聊WebSocket 实时消息推送
- 后端房间管理优化,支持任务和工作区文件同步
- 代码变更:`files changed: 4`, `insertions: +200`, `deletions: -50`
- Evidence: internal/room/room.go, internal/api/server.go, web/src/store.ts, web/src/types.ts
## Experiments
- 暂无
## Notes
- 2026-03-05 启动前必须设置环境变量 `export DEEPSEEK_API_KEY=your_key_here` 或其他 provider (OPENAI_API_KEY, KIMI_API_KEY, OLLAMA_HOST)

View File

@ -1,7 +1,7 @@
name: legal-team name: 法律咨询团队
description: 团队描述 description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
author: author: Agent Team
repo_url: https://gitea.catter.cn/agent-teams/legal-team.git repo_url: https://gitea.catter.cn/agent-teams/legal-team
agents: agents:
- 合同律师 - 合同律师
- 合规专员 - 合规专员

View File

@ -0,0 +1,9 @@
name: 营销团队
description: 营销团队,包含市场分析、竞品研究和内容策划
author: Agent Team
repo_url: https://gitea.catter.cn/agent-teams/marketing-team
agents:
[]
skills:
[]
installed_at: 2026-03-05

View File

@ -1,9 +1,14 @@
# 我的简介 # 我的简介
## 我是谁 ## 我是谁
(介绍你的身份、职业背景) 我是[
## 工作风格 ## 工作风格
- 喜欢的沟通方式 - 喜欢[沟通方式]
- 重视的点 - 重视[重视的点]
- 不喜欢的内容 - 不喜欢[不喜欢的]
## 期望的回复方式
- 用 bullet point 列出要点
- 重要的决定给出 pros 和 cons
- 提供数据支持

View File

@ -1,8 +1,11 @@
--- ---
name: 用户 name: 第五季
description: description: 产品经理,擅长需求分析
provider: deepseek provider: deepseek
model: deepseek-chat model: deepseek-chat
api_key_env: DEEPSEEK_API_KEY api_key_env: DEEPSEEK_API_KEY
avatar_color: "#5865F2" avatar_color: '#5865F2'
preferences:
language: ""
tone: ""
--- ---

View File

@ -1,20 +1,29 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { MessageSquare, ShoppingCart, Hash, Settings } from 'lucide-react' import { ShoppingCart, Sparkles, Settings, Plus } from 'lucide-react'
import { useStore } from './store' import { useStore } from './store'
import { RoomSidebar } from './components/RoomSidebar'
import { ChatView } from './components/ChatView' import { ChatView } from './components/ChatView'
import { MarketPage } from './components/MarketPage' import { MarketPage } from './components/MarketPage'
import { UserSettings } from './components/UserSettings' import { UserSettings } from './components/UserSettings'
import { ThemeToggle } from './components/ThemeToggle' import { ThemeToggle } from './components/ThemeToggle'
import { Onboarding } from './components/Onboarding' import { Onboarding } from './components/Onboarding'
import { MemberList } from './components/MemberList'
import { RightSidebar } from './components/RightSidebar'
const API = '/api'
const COLORS = ['#5865F2', '#57F287', '#FEE75C', '#EB459E', '#ED4245', '#3BA55C', '#FAA61A', '#00B0F4']
const randomColor = () => COLORS[Math.floor(Math.random() * COLORS.length)]
export default function App() { export default function App() {
const { page, setPage, onboardingCompleted, setOnboardingCompleted, fetchUser } = useStore() const { page, setPage, onboardingCompleted, setOnboardingCompleted, fetchUser, fetchRooms, rooms, activeRoomId, setActiveRoom } = useStore()
const [showOnboarding, setShowOnboarding] = useState(onboardingCompleted ? false : true) const [showOnboarding, setShowOnboarding] = useState(onboardingCompleted ? false : true)
const [creating, setCreating] = useState(false)
const [newRoomName, setNewRoomName] = useState('')
useEffect(() => { useEffect(() => {
fetchUser() fetchUser()
}, [fetchUser]) fetchRooms()
}, [fetchUser, fetchRooms])
const handleOnboardingComplete = () => { const handleOnboardingComplete = () => {
setShowOnboarding(false) setShowOnboarding(false)
@ -22,10 +31,17 @@ export default function App() {
fetchUser() fetchUser()
} }
const navItems = [ const createRoom = async () => {
{ id: 'chat', icon: MessageSquare, label: '群聊' }, if (!newRoomName.trim()) return
{ id: 'market', icon: ShoppingCart, label: '市场' }, const color = randomColor()
] as const await fetch(`${API}/rooms`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newRoomName.trim(), type: 'dept', color, master: '', members: [] })
})
setCreating(false)
setNewRoomName('')
useStore.getState().fetchRooms()
}
return ( return (
<> <>
@ -37,25 +53,59 @@ export default function App() {
{/* Left sidebar - Navigation */} {/* Left sidebar - Navigation */}
<div className="flex flex-col w-[72px] bg-[var(--sidebar-bg)] items-center py-3 gap-2"> <div className="flex flex-col w-[72px] bg-[var(--sidebar-bg)] items-center py-3 gap-2">
{/* Logo */} {/* Logo */}
<div className="w-12 h-12 bg-[var(--accent)] rounded-xl flex items-center justify-center mb-2"> <div className="w-12 h-12 rounded-xl flex items-center justify-center mb-2">
<Hash className="w-6 h-6 text-white" /> <Sparkles className="w-7 h-7 text-[var(--accent)]" />
</div> </div>
{/* Nav buttons */} {/* Room list */}
<div className="flex flex-col gap-1 w-full px-3"> <div className="flex flex-col gap-1 w-full px-3 flex-1 overflow-y-auto scrollbar-thin">
{navItems.map(item => ( {rooms.map(r => {
<NavBtn const isActive = activeRoomId === r.id
key={item.id} const initial = r.name[0]?.toUpperCase() || '?'
icon={<item.icon className="w-5 h-5" />} const color = r.color || '#5865F2'
label={item.label} return (
active={page === item.id} <button
onClick={() => setPage(item.id as typeof page)} key={r.id}
/> onClick={() => { setPage('chat'); setActiveRoom(r.id) }}
))} title={r.name}
className={`
w-full h-10 rounded-lg flex items-center justify-center transition-all duration-200 relative text-white text-sm font-bold
${isActive ? 'ring-2 ring-white/40' : 'hover:rounded-xl'}
`}
style={{ backgroundColor: color }}
>
{initial}
{r.status !== 'pending' && (
<span className={`absolute top-0.5 right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[var(--sidebar-bg)] ${r.status === 'thinking' ? 'bg-yellow-400' : 'bg-green-400 animate-pulse'}`} />
)}
</button>
)
})}
{/* Add room button */}
<button
onClick={() => setCreating(true)}
title="创建群聊"
className="w-full h-10 rounded-lg flex items-center justify-center text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] transition-all duration-200"
>
<Plus className="w-5 h-5" />
</button>
</div> </div>
{/* Divider */} {/* Bottom buttons */}
<div className="w-8 h-[2px] bg-[var(--border)] rounded-full my-1" /> <div className="mt-auto flex flex-col gap-1 w-full px-3">
{/* Market */}
<button
onClick={() => setPage('market')}
className={`
w-9 h-9 rounded-lg flex items-center justify-center transition-colors duration-200
text-[var(--text-secondary)] hover:text-[var(--text-primary)]
${page === 'market' ? 'bg-[var(--accent)] text-white' : 'hover:bg-[var(--bg-hover)]'}
`}
title="市场"
>
<ShoppingCart className="w-5 h-5" />
</button>
{/* Theme toggle */} {/* Theme toggle */}
<ThemeToggle /> <ThemeToggle />
@ -73,39 +123,50 @@ export default function App() {
<Settings className="w-5 h-5" /> <Settings className="w-5 h-5" />
</button> </button>
</div> </div>
</div>
{/* Create room modal */}
{creating && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={() => setCreating(false)}>
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 w-80 shadow-xl" onClick={e => e.stopPropagation()}>
<h3 className="font-semibold mb-3"></h3>
<input
autoFocus
className="w-full bg-[var(--bg-tertiary)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)] mb-3"
placeholder="群聊名称"
value={newRoomName}
onChange={e => setNewRoomName(e.target.value)}
onKeyDown={e => e.key === 'Enter' && createRoom()}
/>
<div className="flex gap-2">
<button
onClick={createRoom}
className="flex-1 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white rounded-lg py-2 text-sm font-medium"
>
</button>
<button
onClick={() => setCreating(false)}
className="flex-1 bg-[var(--bg-hover)] hover:bg-[var(--bg-active)] rounded-lg py-2 text-sm"
>
</button>
</div>
</div>
</div>
)}
{/* Main content */} {/* Main content */}
{page === 'chat' && <RoomSidebar />} {page === 'chat' && (
{page === 'chat' && <ChatView />} <>
<MemberList />
<ChatView />
<RightSidebar />
</>
)}
{page === 'market' && <MarketPage />} {page === 'market' && <MarketPage />}
{page === 'settings' && <UserSettings />} {page === 'settings' && <UserSettings />}
</div> </div>
</> </>
) )
} }
function NavBtn({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) {
return (
<button
onClick={onClick}
title={label}
className={`
w-full h-12 rounded-xl flex items-center justify-center transition-all duration-200 group relative
${active
? 'bg-[var(--accent)] text-white'
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
}
`}
>
{icon}
{/* Active indicator */}
{active && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-white rounded-r-full" />
)}
{/* Hover tooltip */}
<div className="absolute left-full ml-4 px-2 py-1 bg-[var(--bg-tertiary)] text-[var(--text-primary)] text-sm rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50 shadow-lg">
{label}
</div>
</button>
)
}

View File

@ -3,38 +3,23 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm' import remarkGfm from 'remark-gfm'
import { import {
Hash, Hash,
Users, Crown,
ListTodo,
FileText,
X,
Plus, Plus,
Smile, Smile,
Send, Send,
Crown,
} from 'lucide-react' } from 'lucide-react'
import { useStore } from '../store' import { useStore } from '../store'
import type { Message } from '../types' import type { Message } from '../types'
const API = '/api'
export function ChatView() { export function ChatView() {
const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore() const { activeRoomId, rooms, messages, sendMessage, user } = useStore()
const [input, setInput] = useState('') const [input, setInput] = useState('')
const [drawer, setDrawer] = useState<null | 'members' | 'tasks' | 'files'>(null)
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const room = rooms.find(r => r.id === activeRoomId) const room = rooms.find(r => r.id === activeRoomId)
const msgs: Message[] = activeRoomId ? (messages[activeRoomId] || []) : [] const msgs: Message[] = activeRoomId ? (messages[activeRoomId] || []) : []
const tasksMd = activeRoomId ? (tasks[activeRoomId] || '') : ''
const files = activeRoomId ? (workspace[activeRoomId] || []) : []
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs]) useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs, room?.status])
const openFile = async (filename: string) => {
const d = await fetch(`${API}/rooms/${activeRoomId}/workspace/${filename}`).then(r => r.json())
setPreviewFile({ name: filename, content: d.content || '' })
}
if (!room) { if (!room) {
return ( return (
@ -45,16 +30,10 @@ export function ChatView() {
) )
} }
const statusLabel = room.status === 'working' && room.activeAgent const statusLabel = room.status === 'working' && room.activeAgent
? `${room.activeAgent} ${room.action || ''}` ? `${room.activeAgent} ${room.action || ''}`
: room.status === 'thinking' ? '思考中...' : room.status === 'working' ? '工作中...' : '空闲' : room.status === 'thinking' ? '思考中...' : room.status === 'working' ? '工作中...' : '空闲'
const drawerButtons = [
{ id: 'members', icon: Users, label: '成员' },
{ id: 'tasks', icon: ListTodo, label: '任务' },
{ id: 'files', icon: FileText, label: '产物' },
] as const
return ( return (
<div className="flex flex-1 overflow-hidden bg-[var(--bg-primary)]"> <div className="flex flex-1 overflow-hidden bg-[var(--bg-primary)]">
{/* Main chat area */} {/* Main chat area */}
@ -94,7 +73,11 @@ export function ChatView() {
<p className="text-xs mt-1"></p> <p className="text-xs mt-1"></p>
</div> </div>
)} )}
{msgs.map(msg => <MessageBubble key={msg.id} msg={msg} />)} {msgs.map(msg => <MessageBubble key={msg.id} msg={msg} userName={user?.name} />)}
{/* Thinking indicator */}
{room.status === 'thinking' && msgs[msgs.length - 1]?.role === 'user' && (
<ThinkingBubble agent={room.master} />
)}
<div ref={bottomRef} /> <div ref={bottomRef} />
</div> </div>
@ -134,124 +117,14 @@ export function ChatView() {
</div> </div>
</div> </div>
</div> </div>
{/* Right panel */}
<div className="w-72 border-l border-[var(--border)] bg-[var(--bg-secondary)] flex flex-col overflow-hidden">
{/* Panel tabs */}
<div className="flex border-b border-[var(--border)]">
{drawerButtons.map(btn => (
<button
key={btn.id}
onClick={() => setDrawer(drawer === btn.id ? null : btn.id)}
className={`
flex-1 py-3 text-xs font-medium flex items-center justify-center gap-1.5 transition-colors border-b-2
${drawer === btn.id
? 'border-[var(--accent)] text-[var(--accent)]'
: 'border-transparent text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
}
`}
>
<btn.icon className="w-4 h-4" />
{btn.label}
</button>
))}
</div>
{/* Panel content */}
<div className="flex-1 overflow-y-auto p-4 scrollbar-thin">
{/* Members */}
{(drawer === 'members' || drawer === null) && (
<section className={drawer !== 'members' && drawer !== null ? 'hidden' : ''}>
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
<Users className="w-3 h-3" />
{1 + msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).length}
</h3>
<MemberItem
name={room.master}
role="master"
status={room.status === 'thinking' ? 'thinking' : 'idle'}
/>
{msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => (
<MemberItem
key={name}
name={name}
role="member"
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'idle'}
/>
))}
</section>
)}
{/* Tasks */}
{(drawer === 'tasks' || drawer === null) && tasksMd && (
<section className={`mt-4 ${drawer !== 'tasks' && drawer !== null ? 'hidden' : ''}`}>
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
<ListTodo className="w-3 h-3" />
</h3>
<div className="text-sm prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
</div>
</section>
)}
{/* Files */}
{(drawer === 'files' || drawer === null) && files.length > 0 && (
<section className={`mt-4 ${drawer !== 'files' && drawer !== null ? 'hidden' : ''}`}>
<h3 className="text-xs font-semibold text-[var(--text-muted)] uppercase mb-3 flex items-center gap-1">
<FileText className="w-3 h-3" />
</h3>
{files.map(f => (
<div
key={f}
onClick={() => openFile(f)}
className="flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-hover)] cursor-pointer py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors"
>
<FileText className="w-4 h-4" />
<span className="truncate">{f}</span>
</div>
))}
</section>
)}
</div>
</div>
{/* File preview modal */}
{previewFile && (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
onClick={() => setPreviewFile(null)}
>
<div
className="bg-[var(--bg-secondary)] rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col shadow-2xl"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
<div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-[var(--accent)]" />
<span className="font-semibold">{previewFile.name}</span>
</div>
<button
onClick={() => setPreviewFile(null)}
className="w-8 h-8 rounded flex items-center justify-center text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="overflow-y-auto p-4 prose dark:prose-invert max-w-none text-sm">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
</div>
</div>
</div>
)}
</div> </div>
) )
} }
function MessageBubble({ msg }: { msg: Message }) { function MessageBubble({ msg, userName }: { msg: Message; userName?: string }) {
const isUser = msg.role === 'user' const isUser = msg.role === 'user'
const isMaster = msg.role === 'master' const isMaster = msg.role === 'master'
const agentName = msg.agent || 'Unknown'
const avatarColors = [ const avatarColors = [
'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500', 'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500',
@ -259,24 +132,22 @@ function MessageBubble({ msg }: { msg: Message }) {
'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500', 'bg-cyan-500', 'bg-sky-500', 'bg-blue-500', 'bg-indigo-500',
'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500', 'bg-rose-500' 'bg-violet-500', 'bg-purple-500', 'bg-fuchsia-500', 'bg-pink-500', 'bg-rose-500'
] ]
const colorIndex = msg.agent.charCodeAt(0) % avatarColors.length const colorIndex = agentName.charCodeAt(0) % avatarColors.length
const displayName = isUser ? (userName || agentName) : agentName
return ( return (
<div className={`flex gap-3 group ${isUser ? 'flex-row-reverse' : ''}`}> <div className={`flex gap-3 group ${isUser ? 'flex-row-reverse' : ''}`}>
{/* Avatar */} {/* Avatar */}
{!isUser && ( <div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 mt-0.5 ${isUser ? 'bg-[var(--accent)]' : avatarColors[colorIndex]}`}>
<div className={`w-10 h-10 rounded-full flex items-center justify-center text-white font-semibold flex-shrink-0 mt-0.5 ${avatarColors[colorIndex]}`}> {isMaster ? <Crown className="w-5 h-5" /> : displayName[0]?.toUpperCase()}
{isMaster ? <Crown className="w-5 h-5" /> : msg.agent[0]?.toUpperCase()}
</div> </div>
)}
{/* Content */} {/* Content */}
<div className={`flex flex-col max-w-[70%] ${isUser ? 'items-end' : 'items-start'}`}> <div className={`flex flex-col max-w-[70%] ${isUser ? 'items-end' : 'items-start'}`}>
{/* Name + timestamp */} {/* Name */}
{!isUser && ( <div className={`flex items-center gap-2 mb-0.5 ${isUser ? 'flex-row-reverse' : ''}`}>
<div className="flex items-center gap-2 mb-0.5"> <span className={`font-medium text-sm ${isMaster ? 'text-[var(--color-warning)]' : isUser ? 'text-[var(--accent)]' : 'text-[var(--text-primary)]'}`}>
<span className={`font-medium text-sm ${isMaster ? 'text-[var(--color-warning)]' : 'text-[var(--text-primary)]'}`}> {displayName}
{msg.agent}
</span> </span>
{isMaster && ( {isMaster && (
<span className="text-[10px] px-1 py-0.5 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded font-medium"> <span className="text-[10px] px-1 py-0.5 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded font-medium">
@ -284,16 +155,15 @@ function MessageBubble({ msg }: { msg: Message }) {
</span> </span>
)} )}
</div> </div>
)}
{/* Message bubble */} {/* Message bubble */}
<div <div
className={` className={`
px-3.5 py-2 rounded-lg text-sm relative group px-3.5 py-2 rounded-lg text-sm relative
${isUser ${isUser
? 'bg-[var(--accent)] text-white rounded-tr-sm' ? 'bg-[var(--accent)] text-white rounded-tr-sm'
: isMaster : isMaster
? 'bg-[var(--bg-tertiary)] border-2 border-[var(--color-warning)]/30 rounded-tl-sm' ? 'bg-[var(--bg-tertiary)] border-l-4 border-[var(--color-warning)] rounded-tl-sm'
: 'bg-[var(--bg-tertiary)] rounded-tl-sm' : 'bg-[var(--bg-tertiary)] rounded-tl-sm'
} }
`} `}
@ -310,37 +180,20 @@ function MessageBubble({ msg }: { msg: Message }) {
) )
} }
function MemberItem({ name, role, status }: { name: string; role: string; status: string }) { function ThinkingBubble({ agent }: { agent: string }) {
const dotClass = status === 'thinking'
? 'bg-[var(--color-warning)] animate-pulse'
: status === 'working'
? 'bg-[var(--color-success)] animate-pulse'
: 'bg-[var(--text-muted)]'
const statusText = status === 'thinking' ? '思考中' : status === 'working' ? '工作中' : '空闲'
return ( return (
<div className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors group"> <div className="flex gap-3">
<div className="relative"> <div className="w-10 h-10 rounded-full bg-[var(--color-warning)]/20 flex items-center justify-center flex-shrink-0 mt-0.5">
<div className={`w-7 h-7 rounded-full bg-[var(--accent)] flex items-center justify-center text-white text-xs font-semibold`}> <Crown className="w-5 h-5 text-[var(--color-warning)]" />
{role === 'master' ? <Crown className="w-3.5 h-3.5" /> : name[0]?.toUpperCase()}
</div> </div>
<div className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[var(--bg-secondary)] ${dotClass}`} /> <div className="flex flex-col items-start">
</div> <span className="font-medium text-sm text-[var(--color-warning)] mb-0.5">{agent || 'Master'}</span>
<div className="flex-1 min-w-0"> <div className="px-3.5 py-2.5 rounded-lg bg-[var(--bg-tertiary)] border-l-4 border-[var(--color-warning)] rounded-tl-sm flex items-center gap-1.5">
<div className="flex items-center gap-1"> <span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '0ms' }} />
{role === 'master' && <Crown className="w-3 h-3 text-[var(--color-warning)]" />} <span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="text-sm font-medium truncate">{name}</span> <span className="w-2 h-2 rounded-full bg-[var(--color-warning)] animate-bounce" style={{ animationDelay: '300ms' }} />
{role === 'master' && (
<span className="text-[10px] px-1 bg-[var(--color-warning)]/20 text-[var(--color-warning)] rounded">
MASTER
</span>
)}
</div> </div>
</div> </div>
<span className="text-xs text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">
{statusText}
</span>
</div> </div>
) )
} }

View File

@ -0,0 +1,59 @@
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Eye, Edit2 } from 'lucide-react'
interface MarkdownEditorProps {
value: string
onChange: (value: string) => void
height?: number
}
export function MarkdownEditor({ value, onChange, height = 250 }: MarkdownEditorProps) {
const [mode, setMode] = useState<'edit' | 'preview'>('edit')
return (
<div className="border border-[var(--border)] rounded-lg overflow-hidden" style={{ height }}>
<div className="flex items-center gap-1 px-3 py-2 bg-[var(--bg-secondary)] border-b border-[var(--border)]">
<button
onClick={() => setMode('edit')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded text-xs transition-colors ${
mode === 'edit'
? 'bg-[var(--accent)] text-white'
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
}`}
>
<Edit2 className="w-3.5 h-3.5" />
</button>
<button
onClick={() => setMode('preview')}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded text-xs transition-colors ${
mode === 'preview'
? 'bg-[var(--accent)] text-white'
: 'hover:bg-[var(--bg-hover)] text-[var(--text-secondary)]'
}`}
>
<Eye className="w-3.5 h-3.5" />
</button>
</div>
<div className="h-[calc(100%-40px)] overflow-y-auto">
{mode === 'edit' ? (
<textarea
value={value}
onChange={(e) => onChange(e.target.value)}
className="w-full h-full p-4 bg-[var(--bg-tertiary)] text-[var(--text-primary)] outline-none resize-none font-mono text-sm"
style={{ height: height - 40 }}
/>
) : (
<div className="p-4 prose prose-invert prose-sm max-w-none bg-[var(--bg-tertiary)]">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{value || '*无内容*'}
</ReactMarkdown>
</div>
)}
</div>
</div>
)
}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { ShoppingCart, Github, Upload, Users, ArrowRight, Check, AlertCircle, Package, Zap, Star, Globe, Link as LinkIcon } from 'lucide-react' import { ShoppingCart, Github, Upload, Users, ArrowRight, Check, AlertCircle, Package, Zap, Star, Globe, Link as LinkIcon, Loader2 } from 'lucide-react'
import { useStore } from '../store' import { useStore } from '../store'
import { TeamDetail } from './TeamDetail' import { TeamDetail } from './TeamDetail'
@ -18,56 +18,20 @@ interface Team {
const PRESET_TEAMS: Team[] = [ const PRESET_TEAMS: Team[] = [
{ {
name: '法律咨询团队', name: '法律咨询团队',
description: '法律咨询团队,包含合同审查和法律风险评估', description: '法律咨询团队,包含合同审查、法律风险评估和合规管理',
author: 'Agent Team', author: 'Agent Team',
repo_url: 'https://github.com/sdaduanbilei/legal-team', repo_url: 'https://gitea.catter.cn/agent-teams/legal-team',
agents: ['法律总监', '合同律师', '合规专员'], agents: ['法律总监', '合同律师', '合规专员'],
skills: ['合同审查', '法律知识库'], skills: ['合同审查', '法律知识库'],
installed_at: '', installed_at: '',
}, },
{
name: '开发团队',
description: '开发团队,包含代码审查、架构设计、全栈测试',
author: 'Agent Team',
repo_url: 'https://github.com/sdaduanbilei/dev-team',
agents: ['技术主管', '代码审查员', '测试工程师'],
skills: ['代码扫描', '性能分析'],
installed_at: '',
},
{ {
name: '营销团队', name: '营销团队',
description: '营销团队,包含内容创意、广告投放策略制定', description: '营销团队,包含市场分析、竞品研究和内容策划',
author: 'Agent Team', author: 'Agent Team',
repo_url: 'https://github.com/sdaduanbilei/marketing-team', repo_url: 'https://gitea.catter.cn/agent-teams/marketing-team',
agents: ['创意总监', '策略分析师', '文案', '运营'], agents: ['营销总监', '市场分析师', '内容策划师'],
skills: ['数据分析', '创意评估'], skills: ['数据分析', '用户画像'],
installed_at: '',
},
{
name: '医疗咨询',
description: '医疗领域专业咨询团队',
author: 'Expert AI Corp',
repo_url: 'https://github.com/expert-ai/medical-team',
agents: ['医疗顾问', '营养师', '健康指导员'],
skills: ['症状分析', '饮食建议'],
installed_at: '',
},
{
name: '金融投资',
description: '金融分析与投资咨询团队',
author: 'FiTech Solutions',
repo_url: 'https://github.com/fitech/finance-team',
agents: ['财务分析师', '投资顾问', '风险测评'],
skills: ['股市预测', '财报分析'],
installed_at: '',
},
{
name: '心理咨询',
description: '专业心理咨询服务团队',
author: 'MindWell Inc',
repo_url: 'https://github.com/mindwell/psychology-team',
agents: ['心理咨询师', '治疗助理', '危机干预'],
skills: ['情绪评估', '心理疏导'],
installed_at: '', installed_at: '',
}, },
] ]
@ -81,6 +45,7 @@ export function MarketPage() {
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const [teams, setTeams] = useState<Team[]>([]) const [teams, setTeams] = useState<Team[]>([])
const [selectedTeam, setSelectedTeam] = useState<Team | null>(null) const [selectedTeam, setSelectedTeam] = useState<Team | null>(null)
const [installingTeams, setInstallingTeams] = useState<Set<string>>(new Set())
useEffect(() => { useEffect(() => {
if (tab === 'teams') { if (tab === 'teams') {
@ -94,6 +59,25 @@ export function MarketPage() {
setTeams(data || []) setTeams(data || [])
} }
const installTeam = async (team: Team) => {
if (!team.repo_url || installingTeams.has(team.name)) return
setInstallingTeams(prev => new Set(prev).add(team.name))
try {
await fetch(`${API}/hub/install`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: team.repo_url })
})
fetchAgents()
fetchTeams()
} finally {
setInstallingTeams(prev => {
const next = new Set(prev)
next.delete(team.name)
return next
})
}
}
const install = async () => { const install = async () => {
if (!repo.trim()) return if (!repo.trim()) return
setStatus('loading') setStatus('loading')
@ -149,13 +133,6 @@ export function MarketPage() {
fetchTeams() fetchTeams()
} }
const isSelected = (teamName: string) => {
if (selectedTeam) {
return selectedTeam.name === teamName
}
return false
}
return ( return (
<div className="flex-1 overflow-y-auto bg-[var(--bg-primary)]"> <div className="flex-1 overflow-y-auto bg-[var(--bg-primary)]">
<div className="max-w-4xl mx-auto p-6"> <div className="max-w-4xl mx-auto p-6">
@ -209,10 +186,10 @@ export function MarketPage() {
</button> </button>
</div> </div>
{selectedTeam && !isSelected(selectedTeam.name) && ( {selectedTeam && (
<TeamDetail <TeamDetail
team={selectedTeam} team={selectedTeam}
installed={teams.some(t => t.name === selectedTeam.name)} installed={tab === 'teams' || teams.some(t => t.name === selectedTeam.name)}
onBack={() => setSelectedTeam(null)} onBack={() => setSelectedTeam(null)}
onInstalled={() => { onInstalled={() => {
setSelectedTeam(null) setSelectedTeam(null)
@ -237,13 +214,13 @@ export function MarketPage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{PRESET_TEAMS.map(team => { {PRESET_TEAMS.map(team => {
const isInstalled = teams.some(t => t.name === team.name) const isInstalled = teams.some(t => t.name === team.name)
const isInstalling = installingTeams.has(team.name)
return ( return (
<div <div
key={team.name} key={team.name}
className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all cursor-pointer ${ className={`bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all ${
isInstalled ? 'ring-2 ring-[var(--accent)] ring-offset-2' : '' isInstalled ? 'ring-2 ring-[var(--accent)] ring-offset-2' : ''
}`} }`}
onClick={() => setSelectedTeam(team)}
> >
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div> <div>
@ -274,6 +251,7 @@ export function MarketPage() {
{team.skills?.length || 0} Skills {team.skills?.length || 0} Skills
</span> </span>
</div> </div>
<div className="flex items-center gap-2">
{team.repo_url && ( {team.repo_url && (
<a <a
href={team.repo_url} href={team.repo_url}
@ -285,6 +263,26 @@ export function MarketPage() {
</a> </a>
)} )}
{!isInstalled && team.repo_url && (
<button
onClick={() => installTeam(team)}
disabled={isInstalling}
className="ml-auto flex items-center gap-1.5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] disabled:opacity-50 px-3 py-1.5 rounded-lg text-xs font-medium text-white transition-colors"
>
{isInstalling ? (
<>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
...
</>
) : (
<>
<ShoppingCart className="w-3.5 h-3.5" />
</>
)}
</button>
)}
</div>
</div> </div>
) )
})} })}
@ -405,23 +403,25 @@ export function MarketPage() {
<div <div
key={team.name} key={team.name}
onClick={() => setSelectedTeam(team)} onClick={() => setSelectedTeam(team)}
className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 cursor-pointer hover:border-[var(--accent)] transition-colors" className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4 hover:border-[var(--accent)] transition-all cursor-pointer ring-2 ring-[var(--accent)] ring-offset-2"
> >
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-3">
<h3 className="font-semibold">{team.name}</h3> <div>
<ArrowRight className="w-4 h-4 text-[var(--text-muted)]" /> <h3 className="font-semibold mb-1">{team.name}</h3>
<p className="text-xs text-[var(--text-muted)]">{team.description || '暂无描述'}</p>
</div> </div>
<p className="text-sm text-[var(--text-muted)] mb-3 line-clamp-2"> <div className="w-8 h-8 bg-[var(--color-success)] rounded-full flex items-center justify-center text-white">
{team.description || '暂无描述'} <Check className="w-4 h-4" />
</p> </div>
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)]"> </div>
<div className="flex items-center gap-3 text-xs text-[var(--text-secondary)] mb-3">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Users className="w-3.5 h-3.5" /> <Users className="w-3.5 h-3.5" />
{team.agents?.length || 0} agents {team.agents?.length || 0}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Package className="w-3.5 h-3.5" /> <Package className="w-3.5 h-3.5" />
{team.skills?.length || 0} skills {team.skills?.length || 0} Skills
</span> </span>
</div> </div>
</div> </div>

View File

@ -0,0 +1,139 @@
import { useEffect, useState } from 'react'
import { Crown, Users } from 'lucide-react'
import { useStore } from '../store'
const API = '/api'
interface TeamInfo {
id: string
name: string
description: string
}
export function MemberList() {
const { activeRoomId, rooms, setRoomTeam } = useStore()
const [teams, setTeams] = useState<TeamInfo[]>([])
const [loadingTeam, setLoadingTeam] = useState(false)
const room = rooms.find(r => r.id === activeRoomId)
useEffect(() => {
if (room && !room.team) {
fetch(`${API}/teams`).then(r => r.json()).then(data => setTeams(data || []))
}
}, [room?.id, room?.team])
if (!room) {
return (
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col">
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
<span className="text-sm font-semibold text-[var(--text-muted)]"></span>
</div>
<div className="flex-1 flex items-center justify-center text-[var(--text-muted)] text-xs">
</div>
</div>
)
}
if (!room.team) {
return (
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col">
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
<span className="text-sm font-semibold text-[var(--text-muted)]"></span>
</div>
<div className="flex-1 flex flex-col items-center justify-center gap-3 px-4">
<Users className="w-8 h-8 text-[var(--text-muted)]" />
<p className="text-xs text-[var(--text-muted)] text-center"></p>
<div className="w-full flex flex-col gap-1.5">
{teams.map(t => (
<button
key={t.id}
disabled={loadingTeam}
onClick={async () => {
setLoadingTeam(true)
await setRoomTeam(room.id, t.id)
setLoadingTeam(false)
}}
className="w-full text-left px-3 py-2 rounded-lg bg-[var(--bg-hover)] hover:bg-[var(--bg-active)] transition-colors"
>
<div className="text-xs font-medium truncate">{t.name}</div>
{t.description && <div className="text-[10px] text-[var(--text-muted)] truncate mt-0.5">{t.description}</div>}
</button>
))}
{teams.length === 0 && (
<p className="text-[10px] text-[var(--text-muted)] text-center"></p>
)}
</div>
</div>
</div>
)
}
const memberAgents = room.members || []
return (
<div className="w-[200px] bg-[var(--channel-list-bg)] border-r border-[var(--border)] flex flex-col overflow-hidden">
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center justify-between">
<span className="text-sm font-semibold text-[var(--text-muted)]"> {1 + memberAgents.length}</span>
<button
onClick={() => {
fetch(`${API}/teams`).then(r => r.json()).then(data => setTeams(data || []))
}}
title="切换团队"
className="text-[10px] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
>
{room.team}
</button>
</div>
<div className="flex-1 overflow-y-auto py-2 scrollbar-thin">
<div className="px-2">
<MemberItem
name={room.master}
role="master"
status={room.status === 'thinking' ? 'thinking' : 'idle'}
/>
{memberAgents.map(name => (
<MemberItem
key={name}
name={name}
role="member"
status={room.status === 'working' && room.activeAgent === name ? 'working' : 'idle'}
/>
))}
</div>
</div>
</div>
)
}
function MemberItem({ name, role, status }: { name: string; role: string; status: string }) {
const dotClass = status === 'thinking'
? 'bg-[var(--color-warning)] animate-pulse'
: status === 'working'
? 'bg-[var(--color-success)] animate-pulse'
: 'bg-[var(--text-muted)]'
const statusText = status === 'thinking' ? '思考中' : status === 'working' ? '工作中' : '空闲'
return (
<div className="flex items-center gap-2 py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors group mb-0.5">
<div className="relative flex-shrink-0">
<div className="w-8 h-8 rounded-full bg-[var(--accent)] flex items-center justify-center text-white text-xs font-semibold">
{role === 'master' ? <Crown className="w-4 h-4" /> : name[0]?.toUpperCase()}
</div>
<div className={`absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[var(--channel-list-bg)] ${dotClass}`} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1">
{role === 'master' && <Crown className="w-3 h-3 text-[var(--color-warning)] flex-shrink-0" />}
<span className="text-sm font-medium truncate">{name}</span>
</div>
<span className="text-[10px] text-[var(--text-muted)] opacity-0 group-hover:opacity-100 transition-opacity">
{statusText}
</span>
</div>
</div>
)
}

View File

@ -1,6 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { ChevronRight, ChevronLeft, Check, Hash, Sparkles, Key, User } from 'lucide-react' import { ChevronRight, ChevronLeft, Check, Hash, Sparkles, Key, User } from 'lucide-react'
import MDEditor from '@uiw/react-md-editor' import { MarkdownEditor } from './MarkdownEditor'
const API = '/api' const API = '/api'
@ -211,8 +211,8 @@ export function Onboarding({ onComplete }: OnboardingProps) {
<div> <div>
<label className="text-sm font-medium mb-1.5 block"> Markdown</label> <label className="text-sm font-medium mb-1.5 block"> Markdown</label>
<div className="border border-[var(--border)] rounded-lg overflow-hidden bg-[var(--bg-tertiary)]"> <div className="mt-2">
<MDEditor <MarkdownEditor
value={profile || `# 我的简介 value={profile || `# 我的简介
## ##
@ -227,24 +227,8 @@ export function Onboarding({ onComplete }: OnboardingProps) {
- bullet point - bullet point
- pros cons - pros cons
- `} - `}
onChange={v => setProfile(v || '')} onChange={(v: string) => setProfile(v)}
height={250} height={250}
preview="edit"
visibleDragbar={false}
className="mt-2"
style={{
backgroundColor: 'var(--bg-primary)',
}}
textareaProps={{
placeholder: '详细编辑你的个人简介...',
style: {
fontFamily: 'var(--font-mono), monospace',
fontSize: '13px',
backgroundColor: 'var(--bg-primary)',
color: 'var(--text-primary)',
padding: '1rem',
}
}}
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,119 @@
import { useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { ListTodo, FileText, X, ExternalLink } from 'lucide-react'
import { useStore } from '../store'
const API = '/api'
export function RightSidebar() {
const { activeRoomId, tasks, workspace } = useStore()
const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
const tasksMd = activeRoomId ? (tasks[activeRoomId] || '') : ''
const files = activeRoomId ? (workspace[activeRoomId] || []) : []
const openFile = async (filename: string) => {
if (!activeRoomId) return
const d = await fetch(`${API}/rooms/${activeRoomId}/workspace/${filename}`).then(r => r.json())
setPreviewFile({ name: filename, content: d.content || '' })
}
if (!activeRoomId) {
return (
<div className="w-[280px] bg-[var(--bg-secondary)] border-l border-[var(--border)] flex flex-col">
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
<span className="text-sm font-semibold text-[var(--text-muted)]"></span>
</div>
<div className="flex-1 flex items-center justify-center text-[var(--text-muted)] text-xs">
</div>
</div>
)
}
return (
<>
<div className="w-[280px] bg-[var(--bg-secondary)] border-l border-[var(--border)] flex flex-col overflow-hidden">
<div className="h-12 px-4 border-b border-[var(--border)] flex items-center">
<span className="text-sm font-semibold text-[var(--text-muted)]"></span>
</div>
<div className="flex-1 overflow-y-auto scrollbar-thin">
{/* TodoList */}
<section className="border-b border-[var(--border)]">
<div className="px-4 py-3 flex items-center gap-2 text-xs font-semibold text-[var(--text-muted)] uppercase">
<ListTodo className="w-3.5 h-3.5" />
</div>
<div className="px-4 pb-4">
{tasksMd ? (
<div className="text-sm prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{tasksMd}</ReactMarkdown>
</div>
) : (
<p className="text-xs text-[var(--text-muted)]"></p>
)}
</div>
</section>
{/* Modified Files */}
<section>
<div className="px-4 py-3 flex items-center gap-2 text-xs font-semibold text-[var(--text-muted)] uppercase">
<FileText className="w-3.5 h-3.5" />
({files.length})
</div>
<div className="px-4 pb-4">
{files.length > 0 ? (
<div className="space-y-1">
{files.map(f => (
<button
key={f}
onClick={() => openFile(f)}
className="w-full flex items-center gap-2 text-sm text-[var(--accent)] hover:text-[var(--accent-hover)] py-1.5 px-2 rounded hover:bg-[var(--bg-hover)] transition-colors text-left"
>
<FileText className="w-4 h-4 flex-shrink-0" />
<span className="truncate">{f}</span>
<ExternalLink className="w-3 h-3 flex-shrink-0 ml-auto opacity-0 group-hover:opacity-100" />
</button>
))}
</div>
) : (
<p className="text-xs text-[var(--text-muted)]"></p>
)}
</div>
</section>
</div>
</div>
{/* File preview modal */}
{previewFile && (
<div
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4"
onClick={() => setPreviewFile(null)}
>
<div
className="bg-[var(--bg-secondary)] rounded-lg w-full max-w-3xl max-h-[80vh] flex flex-col shadow-2xl"
onClick={e => e.stopPropagation()}
>
<div className="flex items-center justify-between px-4 py-3 border-b border-[var(--border)]">
<div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-[var(--accent)]" />
<span className="font-semibold">{previewFile.name}</span>
</div>
<button
onClick={() => setPreviewFile(null)}
className="w-8 h-8 rounded flex items-center justify-center text-[var(--text-muted)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)] transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="overflow-y-auto p-4 prose dark:prose-invert max-w-none text-sm">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{previewFile.content}</ReactMarkdown>
</div>
</div>
</div>
)}
</>
)
}

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import MDEditor from '@uiw/react-md-editor' import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus, ExternalLink } from 'lucide-react'
import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus } from 'lucide-react' import { MarkdownEditor } from './MarkdownEditor'
const API = '/api' const API = '/api'
@ -170,6 +170,18 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
<h2 className="font-semibold">{team.name}</h2> <h2 className="font-semibold">{team.name}</h2>
</div> </div>
{installed ? ( {installed ? (
<div className="flex items-center gap-2">
{team.repo_url && (
<a
href={team.repo_url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-[var(--text-muted)] hover:text-[var(--accent)] text-sm px-3 py-1.5 rounded-lg hover:bg-[var(--bg-hover)]"
>
<ExternalLink className="w-4 h-4" />
GitHub
</a>
)}
<button <button
onClick={handleDelete} onClick={handleDelete}
disabled={deleting} disabled={deleting}
@ -182,6 +194,7 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
)} )}
</button> </button>
</div>
) : ( ) : (
<button <button
onClick={handleInstall} onClick={handleInstall}
@ -204,20 +217,11 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
{/* Team info */} {/* Team info */}
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4"> <div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-xl p-4">
<p className="text-sm text-[var(--text-secondary)] mb-2">{team.description}</p> <p className="text-sm text-[var(--text-secondary)] mb-2">{team.description}</p>
<div className="flex items-center gap-4 text-xs text-[var(--text-muted)]"> {team.author && (
{team.author && <span>: {team.author}</span>} <div className="text-xs text-[var(--text-muted)]">
{team.repo_url && ( : {team.author}
<a
href={team.repo_url}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--accent)] hover:underline flex items-center gap-1"
>
<Plus className="w-3 h-3" />
</a>
)}
</div> </div>
)}
</div> </div>
{/* Agents */} {/* Agents */}
@ -270,18 +274,16 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
))} ))}
</div> </div>
{/* Editor */} {/* Editor */}
<div className="h-64 border-t border-[var(--border)]"> <div className="border-t border-[var(--border)]">
{loading ? ( {loading ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-64">
<Loader2 className="w-5 h-5 animate-spin text-[var(--accent)]" /> <Loader2 className="w-5 h-5 animate-spin text-[var(--accent)]" />
</div> </div>
) : ( ) : (
<MDEditor <MarkdownEditor
value={agentContent} value={agentContent}
onChange={(v: string | undefined) => setAgentContent(v || '')} onChange={(v: string) => setAgentContent(v)}
height={250} height={250}
preview="edit"
visibleDragbar={false}
/> />
)} )}
</div> </div>
@ -372,13 +374,11 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
<div className="flex-1 flex flex-col border border-[var(--border)] rounded-lg overflow-hidden"> <div className="flex-1 flex flex-col border border-[var(--border)] rounded-lg overflow-hidden">
{selectedKnowledge ? ( {selectedKnowledge ? (
<> <>
<div className="h-80"> <div className="flex-1">
<MDEditor <MarkdownEditor
value={knowledgeContent} value={knowledgeContent}
onChange={(v: string | undefined) => setKnowledgeContent(v || '')} onChange={(v: string) => setKnowledgeContent(v)}
height={300} height={320}
preview="edit"
visibleDragbar={false}
/> />
</div> </div>
<div className="flex justify-end px-4 py-2 bg-[var(--bg-tertiary)]"> <div className="flex justify-end px-4 py-2 bg-[var(--bg-tertiary)]">

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import MDEditor from '@uiw/react-md-editor'
import { User, Key, Save, Loader2 } from 'lucide-react' import { User, Key, Save, Loader2 } from 'lucide-react'
import { MarkdownEditor } from './MarkdownEditor'
const API = '/api' const API = '/api'
@ -142,25 +142,10 @@ export function UserSettings() {
</button> </button>
</div> </div>
<div className="flex-1 overflow-auto p-4"> <div className="flex-1 overflow-auto p-4">
<MDEditor <MarkdownEditor
value={profile} value={profile}
onChange={(v) => setProfile(v || '')} onChange={(v: string) => setProfile(v)}
height={300} height={300}
preview="edit"
visibleDragbar={false}
className="border border-[var(--border)] rounded-lg overflow-hidden"
style={{
backgroundColor: 'var(--bg-primary)',
}}
textareaProps={{
placeholder: '编辑个人资料介绍...',
style: {
fontFamily: 'var(--font-mono), monospace',
fontSize: '13px',
backgroundColor: 'var(--bg-primary)',
color: 'var(--text-primary)',
}
}}
/> />
</div> </div>
</> </>

View File

@ -75,145 +75,6 @@
--channel-list-bg: #2B2D31; --channel-list-bg: #2B2D31;
} }
/* Custom styling for react-md-editor - Complete override */
.w-md-editor {
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
box-shadow: none !important;
}
.w-md-editor * {
box-sizing: border-box;
}
.w-md-editor-toolbar {
border-bottom: 1px solid var(--border) !important;
background: var(--bg-secondary) !important;
padding: 8px 12px !important;
}
.w-md-editor-toolbar button {
color: var(--text-secondary) !important;
}
.w-md-editor-toolbar button:hover {
background: var(--bg-hover) !important;
}
.w-md-editor-toolbar button.active {
color: var(--accent) !important;
}
.w-md-editor-text-pre > code,
.w-md-editor-text-input {
font-family: var(--font-mono), monospace !important;
font-size: 13px !important;
line-height: 1.6 !important;
color: var(--text-primary) !important;
background: transparent !important;
}
.w-md-editor-text-input {
caret-color: var(--text-primary) !important;
padding: 12px !important;
}
.w-md-editor-text-pre {
padding: 12px !important;
background: var(--bg-primary) !important;
margin: 0 !important;
}
.w-md-editor-preview {
padding: 12px !important;
background: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
.w-md-editor-preview h1,
.w-md-editor-preview h2,
.w-md-editor-preview h3,
.w-md-editor-preview h4,
.w-md-editor-preview h5,
.w-md-editor-preview h6 {
color: var(--text-primary) !important;
border-bottom: 1px solid var(--border) !important;
padding-bottom: 0.3em !important;
margin-top: 1.5em !important;
}
.w-md-editor-preview a {
color: var(--accent) !important;
}
.w-md-editor-preview code {
background: var(--bg-tertiary) !important;
padding: 2px 6px !important;
border-radius: 4px !important;
font-family: var(--font-mono), monospace !important;
font-size: 0.9em !important;
}
.w-md-editor-preview pre {
background: var(--bg-tertiary) !important;
padding: 12px !important;
border-radius: 6px !important;
overflow-x: auto !important;
}
.w-md-editor-preview pre code {
background: transparent !important;
padding: 0 !important;
}
.w-md-editor-preview blockquote {
border-left: 4px solid var(--accent) !important;
padding-left: 16px !important;
color: var(--text-secondary) !important;
margin: 1em 0 !important;
}
.w-md-editor-preview ul,
.w-md-editor-preview ol {
padding-left: 24px !important;
}
.w-md-editor-preview table {
border-collapse: collapse !important;
width: 100% !important;
margin: 1em 0 !important;
}
.w-md-editor-preview th,
.w-md-editor-preview td {
border: 1px solid var(--border) !important;
padding: 8px 12px !important;
}
.w-md-editor-preview th {
background: var(--bg-tertiary) !important;
}
.w-md-editor-input {
background: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
.w-md-editor-text,
.w-md-editor-text-pre {
background: var(--bg-primary) !important;
color: var(--text-primary) !important;
}
/* Hide drag bar in certain contexts */
.w-md-editor .w-md-editor-bar {
background: var(--bg-tertiary) !important;
width: 4px !important;
cursor: col-resize !important;
}
body { body {
margin: 0; margin: 0;
font-family: var(--font-sans); font-family: var(--font-sans);

View File

@ -29,6 +29,7 @@ interface AppState {
fetchSkills: () => Promise<void> fetchSkills: () => Promise<void>
sendMessage: (roomId: string, content: string) => void sendMessage: (roomId: string, content: string) => void
connectRoom: (roomId: string) => void connectRoom: (roomId: string) => void
setRoomTeam: (roomId: string, team: string) => Promise<void>
} }
const API = '/api' const API = '/api'
@ -171,5 +172,18 @@ export const useStore = create<AppState>((set, get) => {
set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } })) set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } }))
ws[roomId]?.send(JSON.stringify({ type: 'user_message', content, user_name: userName })) ws[roomId]?.send(JSON.stringify({ type: 'user_message', content, user_name: userName }))
}, },
setRoomTeam: async (roomId, team) => {
const res = await fetch(`${API}/rooms/${roomId}/team`, {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ team })
})
if (res.ok) {
const data = await res.json()
set(s => ({
rooms: s.rooms.map(r => r.id === roomId ? { ...r, team, master: data.master } : r)
}))
}
},
} }
}) })

View File

@ -7,8 +7,11 @@ export interface Room {
type: RoomType type: RoomType
status: RoomStatus status: RoomStatus
master: string master: string
members?: string[]
activeAgent?: string activeAgent?: string
action?: string action?: string
color?: string
team?: string
} }
export interface Message { export interface Message {