diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index ab5b449..57a7f02 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -9,7 +9,10 @@
"Bash(npm install:*)",
"Bash(node:*)",
"Bash(npm run:*)",
- "Bash(curl:*)"
+ "Bash(curl:*)",
+ "Bash(cat:*)",
+ "Bash(grep:*)",
+ "Bash(echo DEEPSEEK_API_KEY=$DEEPSEEK_API_KEY:*)"
]
}
}
diff --git a/agents/legal-team/TEAM.md b/agents/legal-team/TEAM.md
index 41de22f..7a74f57 100644
--- a/agents/legal-team/TEAM.md
+++ b/agents/legal-team/TEAM.md
@@ -1,8 +1,38 @@
---
name: 法律咨询团队
-description: 法律咨询团队,包含合同审查、法律风险评估和合规管理
+description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
author: Agent Team
+version: 1.0.0
+repo_url: https://gitea.catter.cn/agent-teams/legal-team.git
+agents:
+ - 法律总监
+ - 合同律师
+ - 合规专员
skills:
- 合同审查
- 法律知识库
----
\ No newline at end of file
+---
+
+# 法律咨询团队
+
+专业法律咨询团队,提供全方位的法律服务支持。
+
+## 团队成员
+
+- **法律总监**:团队负责人,协调各项法律事务,提供战略级法律建议
+- **合同律师**:专注合同审查、起草和风险评估
+- **合规专员**:负责合规检查、风险识别和合规建议
+
+## 核心能力
+
+- 合同审查与风险评估
+- 法律咨询与建议
+- 合规检查与指导
+- 法律文档起草
+
+## 使用场景
+
+- 企业合同审查
+- 法律风险评估
+- 合规体系建设
+- 法律咨询问答
\ No newline at end of file
diff --git a/agents/legal-team/法律总监/memory/2026-03.md b/agents/legal-team/法律总监/memory/2026-03.md
new file mode 100644
index 0000000..7c4348d
--- /dev/null
+++ b/agents/legal-team/法律总监/memory/2026-03.md
@@ -0,0 +1,16 @@
+
+## 2026-03-05 — 你好
+
+ASSIGN:合同律师:分析用户"第五季"的工作风格和沟通偏好,整理成简洁要点,为后续法律咨询提供参考框架。
+
+ASSIGN:合规专员:评估用户简介中可能涉及的法律合规风险点(如沟通方式、数据使用等),提出初步注意事项。
+
+## 2026-03-05 — 我一个中间人,说可以给我介绍一个
+
+基于本次咨询,总结关键要点如下:
+
+- **权属核查是基石**:投资任何实体项目前,**必须**首先审查并确认核心资产(如土地、场地)的完整、合法权属链条及使用许可,权属不清是根本性风险。
+- **中间人模式风险高**:通过无明确授权的“中间人”进行投资,会导致法律关系模糊、资金失控,且极易与项目实际方脱节,应争取与权属方建立直接合同关系。
+- **异常高回报是危险信号**:远高于市场平均水平的回报承诺(如年化30%)通常伴随极高的违约风险或项目本身不可行,需深究其商业合理性与法律合规性。
+- **协议性质必须明确**:“投资协议”的法律定性(借贷、合伙或委托)直接决定您的权利、风险与责任,必须在条款中清晰界定,避免后续争议。
+- **合规红线不可触碰**:涉及政府资源或资产的项目,若运作模式依赖“关系”而非公开程序,极易涉及商业贿赂、违规融资等刑事风险,必须坚持合法透明路径。
diff --git a/internal/api/server.go b/internal/api/server.go
index 2cc2dfe..d65a540 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -62,6 +62,7 @@ func (s *Server) routes() {
g := s.e.Group("/api")
g.GET("/rooms", s.listRooms)
g.POST("/rooms", s.createRoom)
+ g.PUT("/rooms/:id/team", s.setRoomTeam)
g.GET("/agents", s.listAgents)
g.GET("/agents/:name/files/:file", s.readAgentFile)
g.PUT("/agents/:name/files/:file", s.writeAgentFile)
@@ -191,15 +192,18 @@ func (s *Server) listRooms(c echo.Context) error {
s.mu.RLock()
defer s.mu.RUnlock()
type roomInfo struct {
- ID string `json:"id"`
- Name string `json:"name"`
- Type string `json:"type"`
- Status room.Status `json:"status"`
- Master string `json:"master"`
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Type string `json:"type"`
+ Status room.Status `json:"status"`
+ Master string `json:"master"`
+ Members []string `json:"members"`
+ Color string `json:"color"`
+ Team string `json:"team"`
}
var list []roomInfo
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)
}
@@ -209,16 +213,14 @@ func (s *Server) createRoom(c echo.Context) error {
if err := c.Bind(&cfg); err != nil {
return err
}
+ if cfg.Type == "" {
+ cfg.Type = room.TypeDept
+ }
dir := filepath.Join(s.roomsDir, cfg.Name)
os.MkdirAll(filepath.Join(dir, "workspace"), 0755)
os.MkdirAll(filepath.Join(dir, "history"), 0755)
- content := "---\nname: " + cfg.Name + "\ntype: " + string(cfg.Type) + "\nmaster: " + cfg.Master + "\nmembers:\n"
- for _, m := range cfg.Members {
- content += " - " + m + "\n"
- }
- content += "---\n"
- os.WriteFile(filepath.Join(dir, "room.md"), []byte(content), 0644)
+ s.writeRoomFile(dir, cfg)
r, err := room.Load(dir, s.agentsDir, s.skillsDir)
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})
}
+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 {
entries, _ := os.ReadDir(s.agentsDir)
type agentInfo struct {
@@ -393,7 +465,6 @@ func (s *Server) getWorkspaceFile(c echo.Context) error {
func (s *Server) getMessages(c echo.Context) error {
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")
data, err := os.ReadFile(historyFile)
if err != nil {
@@ -406,30 +477,40 @@ func (s *Server) getMessages(c echo.Context) error {
Content string `json:"content"`
}
var msgs []msg
- lines := strings.Split(string(data), "\n## ")
- for i, block := range lines {
+ // Format: "\n**[HH:MM:SS] agentName** (role)\n\ncontent\n"
+ blocks := strings.Split(string(data), "\n**[")
+ for i, block := range blocks {
if block == "" {
continue
}
- // Parse "**[HH:MM:SS] agentName** (role)\n\ncontent"
- parts := strings.SplitN(block, "\n\n", 2)
- if len(parts) < 2 {
+ // block starts with "HH:MM:SS] agentName** (role)\n\ncontent"
+ headerEnd := strings.Index(block, "\n\n")
+ if headerEnd < 0 {
continue
}
- header := parts[0]
- content := strings.TrimSpace(parts[1])
- // Extract role and agent from header like "**[15:04:05] agentName** (role)"
- var agentName, role string
- fmt.Sscanf(header, "**[%*s] %s** (%s)", &agentName, &role)
- agentName = strings.TrimSuffix(agentName, "**")
- role = strings.TrimSuffix(role, ")")
+ header := block[:headerEnd]
+ content := strings.TrimSpace(block[headerEnd+2:])
+ // header: "HH:MM:SS] agentName** (role)"
+ bracketEnd := strings.Index(header, "] ")
+ if bracketEnd < 0 {
+ continue
+ }
+ 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 == "" {
agentName = "unknown"
}
- if role == "" {
- role = "member"
+ if roleStr == "" {
+ 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)
}
@@ -529,6 +610,7 @@ func (s *Server) listTeams(c echo.Context) error {
continue
}
teams = append(teams, map[string]interface{}{
+ "id": e.Name(),
"name": team.Name,
"description": team.Description,
"author": team.Author,
@@ -650,22 +732,36 @@ func loadTeam(path string) (*hub.Team, error) {
if err != nil {
return nil, err
}
- // Simple YAML parse
team := &hub.Team{}
lines := strings.Split(string(data), "\n")
+ var currentField string
for _, line := range lines {
if strings.HasPrefix(line, "name: ") {
team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: "))
+ currentField = ""
} else if strings.HasPrefix(line, "description: ") {
team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: "))
+ currentField = ""
} else if strings.HasPrefix(line, "author: ") {
team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: "))
+ currentField = ""
} else if strings.HasPrefix(line, "repo_url: ") {
team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: "))
+ currentField = ""
} else if strings.HasPrefix(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, " - ") {
- 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
diff --git a/internal/hub/hub.go b/internal/hub/hub.go
index ec7d2b2..1fe81c8 100644
--- a/internal/hub/hub.go
+++ b/internal/hub/hub.go
@@ -8,6 +8,8 @@ import (
"path/filepath"
"strings"
"time"
+
+ "gopkg.in/yaml.v3"
)
type Team struct {
@@ -108,10 +110,14 @@ func installFromDir(tmp, teamName, repoURL, agentsDir, skillsDir, teamsDir strin
// Create knowledge dir
os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755)
+ // Read TEAM.md for metadata
+ teamMeta := readTeamMeta(tmp)
+
// Create team record
team := &Team{
- Name: teamName,
- Description: "团队描述",
+ Name: teamMeta.Name,
+ Description: teamMeta.Description,
+ Author: teamMeta.Author,
RepoURL: repoURL,
Agents: installedAgents,
Skills: installedSkills,
@@ -238,3 +244,30 @@ func copyDir(src, dst string) error {
}
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
+}
diff --git a/internal/room/room.go b/internal/room/room.go
index 4c687af..16d90d1 100644
--- a/internal/room/room.go
+++ b/internal/room/room.go
@@ -36,6 +36,8 @@ type Config struct {
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 {
@@ -90,16 +92,19 @@ func Load(roomDir string, agentsDir string, skillsDir string) (*Room, error) {
r := &Room{Config: cfg, Dir: roomDir, members: make(map[string]*agent.Agent)}
- r.master, err = agent.Load(filepath.Join(agentsDir, cfg.Master))
- if err != nil {
- return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
- }
- for _, name := range cfg.Members {
- a, err := agent.Load(filepath.Join(agentsDir, name))
+ if cfg.Master != "" {
+ agentPath := resolveAgentPath(agentsDir, cfg.Team, cfg.Master)
+ r.master, err = agent.Load(agentPath)
if err != nil {
- return nil, fmt.Errorf("load member %s: %w", name, err)
+ return nil, fmt.Errorf("load master %s: %w", cfg.Master, err)
+ }
+ for _, name := range cfg.Members {
+ a, err := agent.Load(resolveAgentPath(agentsDir, cfg.Team, name))
+ if err != nil {
+ return nil, fmt.Errorf("load member %s: %w", name, err)
+ }
+ r.members[name] = a
}
- r.members[name] = a
}
r.skillMeta, _ = skill.Discover(skillsDir)
@@ -140,6 +145,9 @@ func (r *Room) Handle(ctx context.Context, userMsg string) error {
// HandleUserMessage processes a user message with a specific user name.
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.setStatus(StatusThinking, "", "")
@@ -319,3 +327,14 @@ func parseRoomConfig(data []byte) (Config, error) {
}
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)
+}
diff --git a/main b/main
new file mode 100755
index 0000000..5167757
Binary files /dev/null and b/main differ
diff --git a/progress.md b/progress.md
new file mode 100644
index 0000000..54ac0d9
--- /dev/null
+++ b/progress.md
@@ -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)
diff --git a/teams/legal-team/team.yaml b/teams/legal-team/team.yaml
index c6bd533..4ac299f 100644
--- a/teams/legal-team/team.yaml
+++ b/teams/legal-team/team.yaml
@@ -1,7 +1,7 @@
-name: legal-team
-description: 团队描述
-author:
-repo_url: https://gitea.catter.cn/agent-teams/legal-team.git
+name: 法律咨询团队
+description: 专业法律咨询团队,包含合同审查、法律风险评估和合规建议
+author: Agent Team
+repo_url: https://gitea.catter.cn/agent-teams/legal-team
agents:
- 合同律师
- 合规专员
diff --git a/teams/marketing-team/team.yaml b/teams/marketing-team/team.yaml
new file mode 100644
index 0000000..6c5e5a0
--- /dev/null
+++ b/teams/marketing-team/team.yaml
@@ -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
diff --git a/users/default/PROFILE.md b/users/default/PROFILE.md
index 2e0a400..97e9042 100644
--- a/users/default/PROFILE.md
+++ b/users/default/PROFILE.md
@@ -1,9 +1,14 @@
# 我的简介
## 我是谁
-(介绍你的身份、职业背景)
+我是[
## 工作风格
-- 喜欢的沟通方式
-- 重视的点
-- 不喜欢的内容
+- 喜欢[沟通方式]
+- 重视[重视的点]
+- 不喜欢[不喜欢的]
+
+## 期望的回复方式
+- 用 bullet point 列出要点
+- 重要的决定给出 pros 和 cons
+- 提供数据支持
\ No newline at end of file
diff --git a/users/default/USER.md b/users/default/USER.md
index 44c937c..90341ba 100644
--- a/users/default/USER.md
+++ b/users/default/USER.md
@@ -1,8 +1,11 @@
---
-name: 用户
-description:
+name: 第五季
+description: 产品经理,擅长需求分析
provider: deepseek
model: deepseek-chat
api_key_env: DEEPSEEK_API_KEY
-avatar_color: "#5865F2"
+avatar_color: '#5865F2'
+preferences:
+ language: ""
+ tone: ""
---
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 8046e6e..6c04436 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,20 +1,29 @@
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 { RoomSidebar } from './components/RoomSidebar'
import { ChatView } from './components/ChatView'
import { MarketPage } from './components/MarketPage'
import { UserSettings } from './components/UserSettings'
import { ThemeToggle } from './components/ThemeToggle'
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() {
- 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 [creating, setCreating] = useState(false)
+ const [newRoomName, setNewRoomName] = useState('')
useEffect(() => {
fetchUser()
- }, [fetchUser])
+ fetchRooms()
+ }, [fetchUser, fetchRooms])
const handleOnboardingComplete = () => {
setShowOnboarding(false)
@@ -22,10 +31,17 @@ export default function App() {
fetchUser()
}
- const navItems = [
- { id: 'chat', icon: MessageSquare, label: '群聊' },
- { id: 'market', icon: ShoppingCart, label: '市场' },
- ] as const
+ const createRoom = async () => {
+ if (!newRoomName.trim()) return
+ const color = randomColor()
+ 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 (
<>
@@ -37,75 +53,120 @@ export default function App() {
{/* Left sidebar - Navigation */}
{/* Logo */}
-
-
+
+
- {/* Nav buttons */}
-
- {navItems.map(item => (
-
}
- label={item.label}
- active={page === item.id}
- onClick={() => setPage(item.id as typeof page)}
- />
- ))}
+ {/* Room list */}
+
+ {rooms.map(r => {
+ const isActive = activeRoomId === r.id
+ const initial = r.name[0]?.toUpperCase() || '?'
+ const color = r.color || '#5865F2'
+ return (
+
+ )
+ })}
+
+ {/* Add room button */}
+
- {/* Divider */}
-
+ {/* Bottom buttons */}
+
+ {/* Market */}
+
- {/* Theme toggle */}
-
+ {/* Theme toggle */}
+
- {/* Settings */}
-
+ {/* Settings */}
+
+
+ {/* Create room modal */}
+ {creating && (
+
setCreating(false)}>
+
e.stopPropagation()}>
+
创建新群聊
+
setNewRoomName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && createRoom()}
+ />
+
+
+
+
+
+
+ )}
+
{/* Main content */}
- {page === 'chat' &&
}
- {page === 'chat' &&
}
+ {page === 'chat' && (
+ <>
+
+
+
+ >
+ )}
{page === 'market' &&
}
{page === 'settings' &&
}
>
)
-}
-
-function NavBtn({ icon, label, active, onClick }: { icon: React.ReactNode; label: string; active: boolean; onClick: () => void }) {
- return (
-
- )
}
\ No newline at end of file
diff --git a/web/src/components/ChatView.tsx b/web/src/components/ChatView.tsx
index a9e7e4f..9235cd8 100644
--- a/web/src/components/ChatView.tsx
+++ b/web/src/components/ChatView.tsx
@@ -3,38 +3,23 @@ import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import {
Hash,
- Users,
- ListTodo,
- FileText,
- X,
+ Crown,
Plus,
Smile,
Send,
- Crown,
} from 'lucide-react'
import { useStore } from '../store'
import type { Message } from '../types'
-const API = '/api'
-
export function ChatView() {
- const { activeRoomId, rooms, messages, tasks, workspace, sendMessage } = useStore()
+ const { activeRoomId, rooms, messages, sendMessage, user } = useStore()
const [input, setInput] = useState('')
- const [drawer, setDrawer] = useState
(null)
- const [previewFile, setPreviewFile] = useState<{ name: string; content: string } | null>(null)
const bottomRef = useRef(null)
const room = rooms.find(r => r.id === activeRoomId)
const msgs: Message[] = activeRoomId ? (messages[activeRoomId] || []) : []
- const tasksMd = activeRoomId ? (tasks[activeRoomId] || '') : ''
- const files = activeRoomId ? (workspace[activeRoomId] || []) : []
- useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs])
-
- const openFile = async (filename: string) => {
- const d = await fetch(`${API}/rooms/${activeRoomId}/workspace/${filename}`).then(r => r.json())
- setPreviewFile({ name: filename, content: d.content || '' })
- }
+ useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [msgs, room?.status])
if (!room) {
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.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 (
{/* Main chat area */}
@@ -94,7 +73,11 @@ export function ChatView() {
发送消息开始对话
)}
- {msgs.map(msg => )}
+ {msgs.map(msg => )}
+ {/* Thinking indicator */}
+ {room.status === 'thinking' && msgs[msgs.length - 1]?.role === 'user' && (
+
+ )}
@@ -134,124 +117,14 @@ export function ChatView() {
-
- {/* Right panel */}
-
- {/* Panel tabs */}
-
- {drawerButtons.map(btn => (
-
- ))}
-
-
- {/* Panel content */}
-
- {/* Members */}
- {(drawer === 'members' || drawer === null) && (
-
-
-
- 成员 — {1 + msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).length}
-
-
- {msgs.filter(m => m.role === 'member').map(m => m.agent).filter((v, i, a) => a.indexOf(v) === i).map(name => (
-
- ))}
-
- )}
-
- {/* Tasks */}
- {(drawer === 'tasks' || drawer === null) && tasksMd && (
-
-
-
- 任务列表
-
-
- {tasksMd}
-
-
- )}
-
- {/* Files */}
- {(drawer === 'files' || drawer === null) && files.length > 0 && (
-
-
-
- 产物文件
-
- {files.map(f => (
- 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"
- >
-
- {f}
-
- ))}
-
- )}
-
-
-
- {/* File preview modal */}
- {previewFile && (
- setPreviewFile(null)}
- >
-
e.stopPropagation()}
- >
-
-
-
- {previewFile.name}
-
-
-
-
- {previewFile.content}
-
-
-
- )}
)
}
-function MessageBubble({ msg }: { msg: Message }) {
+function MessageBubble({ msg, userName }: { msg: Message; userName?: string }) {
const isUser = msg.role === 'user'
const isMaster = msg.role === 'master'
+ const agentName = msg.agent || 'Unknown'
const avatarColors = [
'bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-yellow-500',
@@ -259,41 +132,38 @@ function MessageBubble({ msg }: { msg: Message }) {
'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'
]
- const colorIndex = msg.agent.charCodeAt(0) % avatarColors.length
+ const colorIndex = agentName.charCodeAt(0) % avatarColors.length
+ const displayName = isUser ? (userName || agentName) : agentName
return (
{/* Avatar */}
- {!isUser && (
-
- {isMaster ? : msg.agent[0]?.toUpperCase()}
-
- )}
+
+ {isMaster ? : displayName[0]?.toUpperCase()}
+
{/* Content */}
- {/* Name + timestamp */}
- {!isUser && (
-
-
- {msg.agent}
+ {/* Name */}
+
+
+ {displayName}
+
+ {isMaster && (
+
+ MASTER
- {isMaster && (
-
- MASTER
-
- )}
-
- )}
+ )}
+
{/* Message bubble */}
-
-
- {role === 'master' ? : name[0]?.toUpperCase()}
-
-
+
+
+
-
-
- {role === 'master' &&
}
-
{name}
- {role === 'master' && (
-
- MASTER
-
- )}
+
+
{agent || 'Master'}
+
+
+
+
-
- {statusText}
-
)
}
diff --git a/web/src/components/MarkdownEditor.tsx b/web/src/components/MarkdownEditor.tsx
new file mode 100644
index 0000000..cf35154
--- /dev/null
+++ b/web/src/components/MarkdownEditor.tsx
@@ -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 (
+
+
+
+
+
+
+
+ )
+}
diff --git a/web/src/components/MarketPage.tsx b/web/src/components/MarketPage.tsx
index 409283c..b31faa5 100644
--- a/web/src/components/MarketPage.tsx
+++ b/web/src/components/MarketPage.tsx
@@ -1,5 +1,5 @@
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 { TeamDetail } from './TeamDetail'
@@ -18,56 +18,20 @@ interface Team {
const PRESET_TEAMS: Team[] = [
{
name: '法律咨询团队',
- description: '法律咨询团队,包含合同审查和法律风险评估',
+ description: '法律咨询团队,包含合同审查、法律风险评估和合规管理',
author: 'Agent Team',
- repo_url: 'https://github.com/sdaduanbilei/legal-team',
+ repo_url: 'https://gitea.catter.cn/agent-teams/legal-team',
agents: ['法律总监', '合同律师', '合规专员'],
skills: ['合同审查', '法律知识库'],
installed_at: '',
},
- {
- name: '开发团队',
- description: '开发团队,包含代码审查、架构设计、全栈测试',
- author: 'Agent Team',
- repo_url: 'https://github.com/sdaduanbilei/dev-team',
- agents: ['技术主管', '代码审查员', '测试工程师'],
- skills: ['代码扫描', '性能分析'],
- installed_at: '',
- },
{
name: '营销团队',
- description: '营销团队,包含内容创意、广告投放策略制定',
+ description: '营销团队,包含市场分析、竞品研究和内容策划',
author: 'Agent Team',
- repo_url: 'https://github.com/sdaduanbilei/marketing-team',
- agents: ['创意总监', '策略分析师', '文案', '运营'],
- 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: ['情绪评估', '心理疏导'],
+ repo_url: 'https://gitea.catter.cn/agent-teams/marketing-team',
+ agents: ['营销总监', '市场分析师', '内容策划师'],
+ skills: ['数据分析', '用户画像'],
installed_at: '',
},
]
@@ -81,6 +45,7 @@ export function MarketPage() {
const [uploading, setUploading] = useState(false)
const [teams, setTeams] = useState
([])
const [selectedTeam, setSelectedTeam] = useState(null)
+ const [installingTeams, setInstallingTeams] = useState>(new Set())
useEffect(() => {
if (tab === 'teams') {
@@ -94,6 +59,25 @@ export function MarketPage() {
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 () => {
if (!repo.trim()) return
setStatus('loading')
@@ -149,13 +133,6 @@ export function MarketPage() {
fetchTeams()
}
- const isSelected = (teamName: string) => {
- if (selectedTeam) {
- return selectedTeam.name === teamName
- }
- return false
- }
-
return (
@@ -209,10 +186,10 @@ export function MarketPage() {
- {selectedTeam && !isSelected(selectedTeam.name) && (
+ {selectedTeam && (
t.name === selectedTeam.name)}
+ installed={tab === 'teams' || teams.some(t => t.name === selectedTeam.name)}
onBack={() => setSelectedTeam(null)}
onInstalled={() => {
setSelectedTeam(null)
@@ -237,13 +214,13 @@ export function MarketPage() {
{PRESET_TEAMS.map(team => {
const isInstalled = teams.some(t => t.name === team.name)
+ const isInstalling = installingTeams.has(team.name)
return (
setSelectedTeam(team)}
>
@@ -274,17 +251,38 @@ export function MarketPage() {
{team.skills?.length || 0} Skills
- {team.repo_url && (
-
-
- 查看详情
-
- )}
+
+ {team.repo_url && (
+
+
+ 查看详情
+
+ )}
+ {!isInstalled && team.repo_url && (
+
+ )}
+
)
})}
@@ -405,23 +403,25 @@ export function MarketPage() {
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"
>
-
-
{team.name}
-
+
+
+
{team.name}
+
{team.description || '暂无描述'}
+
+
+
+
-
- {team.description || '暂无描述'}
-
-
+
- {team.agents?.length || 0} agents
+ {team.agents?.length || 0} 成员
- {team.skills?.length || 0} skills
+ {team.skills?.length || 0} Skills
diff --git a/web/src/components/MemberList.tsx b/web/src/components/MemberList.tsx
new file mode 100644
index 0000000..2793f81
--- /dev/null
+++ b/web/src/components/MemberList.tsx
@@ -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
([])
+ 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 (
+
+
+ 成员
+
+
+ 选择群聊查看成员
+
+
+ )
+ }
+
+ if (!room.team) {
+ return (
+
+
+ 成员
+
+
+
+
选择一个团队开始工作
+
+ {teams.map(t => (
+
+ ))}
+ {teams.length === 0 && (
+
暂无已安装团队
+ )}
+
+
+
+ )
+ }
+
+ const memberAgents = room.members || []
+
+ return (
+
+
+ 成员 — {1 + memberAgents.length}
+
+
+
+
+
+
+ {memberAgents.map(name => (
+
+ ))}
+
+
+
+ )
+}
+
+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 (
+
+
+
+ {role === 'master' ? : name[0]?.toUpperCase()}
+
+
+
+
+
+ {role === 'master' && }
+ {name}
+
+
+ {statusText}
+
+
+
+ )
+}
diff --git a/web/src/components/Onboarding.tsx b/web/src/components/Onboarding.tsx
index f0bc205..b2c6eed 100644
--- a/web/src/components/Onboarding.tsx
+++ b/web/src/components/Onboarding.tsx
@@ -1,6 +1,6 @@
import { useState } from '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'
@@ -211,9 +211,9 @@ export function Onboarding({ onComplete }: OnboardingProps) {
-
-
+ setProfile(v || '')}
- 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',
- }
- }}
- />
-
+ onChange={(v: string) => setProfile(v)}
+ height={250}
+ />
+
)}
diff --git a/web/src/components/RightSidebar.tsx b/web/src/components/RightSidebar.tsx
new file mode 100644
index 0000000..be202c1
--- /dev/null
+++ b/web/src/components/RightSidebar.tsx
@@ -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 (
+
+
+ 工作区
+
+
+ 选择群聊查看工作区
+
+
+ )
+ }
+
+ return (
+ <>
+
+
+ 工作区
+
+
+
+ {/* TodoList */}
+
+
+
+ 任务列表
+
+
+ {tasksMd ? (
+
+ {tasksMd}
+
+ ) : (
+
暂无任务
+ )}
+
+
+
+ {/* Modified Files */}
+
+
+
+ 修改的文件 ({files.length})
+
+
+ {files.length > 0 ? (
+
+ {files.map(f => (
+
+ ))}
+
+ ) : (
+
暂无修改的文件
+ )}
+
+
+
+
+
+ {/* File preview modal */}
+ {previewFile && (
+
setPreviewFile(null)}
+ >
+
e.stopPropagation()}
+ >
+
+
+
+ {previewFile.name}
+
+
+
+
+ {previewFile.content}
+
+
+
+ )}
+ >
+ )
+}
\ No newline at end of file
diff --git a/web/src/components/TeamDetail.tsx b/web/src/components/TeamDetail.tsx
index f5b11f2..71fa52e 100644
--- a/web/src/components/TeamDetail.tsx
+++ b/web/src/components/TeamDetail.tsx
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
-import MDEditor from '@uiw/react-md-editor'
-import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus } from 'lucide-react'
+import { ChevronLeft, Users, Package, BookOpen, ChevronDown, ChevronRight, Save, Loader2, Trash2, Plus, ExternalLink } from 'lucide-react'
+import { MarkdownEditor } from './MarkdownEditor'
const API = '/api'
@@ -170,18 +170,31 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
{team.name}
{installed ? (
-
{/* Agents */}
@@ -270,18 +274,16 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
))}
{/* Editor */}
-
+
{loading ? (
-
+
) : (
-
setAgentContent(v || '')}
+ onChange={(v: string) => setAgentContent(v)}
height={250}
- preview="edit"
- visibleDragbar={false}
/>
)}
@@ -372,13 +374,11 @@ export function TeamDetail({ team, installed, onBack, onInstalled, onUninstalled
{selectedKnowledge ? (
<>
-
-
+ setKnowledgeContent(v || '')}
- height={300}
- preview="edit"
- visibleDragbar={false}
+ onChange={(v: string) => setKnowledgeContent(v)}
+ height={320}
/>
diff --git a/web/src/components/UserSettings.tsx b/web/src/components/UserSettings.tsx
index d0d6c1e..de3fb43 100644
--- a/web/src/components/UserSettings.tsx
+++ b/web/src/components/UserSettings.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
-import MDEditor from '@uiw/react-md-editor'
import { User, Key, Save, Loader2 } from 'lucide-react'
+import { MarkdownEditor } from './MarkdownEditor'
const API = '/api'
@@ -142,25 +142,10 @@ export function UserSettings() {
- setProfile(v || '')}
+ onChange={(v: string) => setProfile(v)}
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)',
- }
- }}
/>
>
diff --git a/web/src/index.css b/web/src/index.css
index 426e095..0be1fc8 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -75,145 +75,6 @@
--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 {
margin: 0;
font-family: var(--font-sans);
diff --git a/web/src/store.ts b/web/src/store.ts
index ff5e576..4e58f6d 100644
--- a/web/src/store.ts
+++ b/web/src/store.ts
@@ -29,6 +29,7 @@ interface AppState {
fetchSkills: () => Promise
sendMessage: (roomId: string, content: string) => void
connectRoom: (roomId: string) => void
+ setRoomTeam: (roomId: string, team: string) => Promise
}
const API = '/api'
@@ -171,5 +172,18 @@ export const useStore = create((set, get) => {
set(s => ({ messages: { ...s.messages, [roomId]: [...(s.messages[roomId] || []), userMsg] } }))
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)
+ }))
+ }
+ },
}
})
diff --git a/web/src/types.ts b/web/src/types.ts
index abbfb42..4a97f88 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -7,8 +7,11 @@ export interface Room {
type: RoomType
status: RoomStatus
master: string
+ members?: string[]
activeAgent?: string
action?: string
+ color?: string
+ team?: string
}
export interface Message {