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 ( +
+
+ + +
+
+ {mode === 'edit' ? ( +