agent-team/internal/api/server.go
sdaduanbilei 9e279a0627 fix
2026-03-05 17:34:49 +08:00

673 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/labstack/echo/v4"
"github.com/sdaduanbilei/agent-team/internal/hub"
"github.com/sdaduanbilei/agent-team/internal/room"
"github.com/sdaduanbilei/agent-team/internal/user"
)
type Server struct {
e *echo.Echo
agentsDir string
skillsDir string
roomsDir string
usersDir string
teamsDir string
rooms map[string]*room.Room
user *user.User
mu sync.RWMutex
clients map[string]map[*websocket.Conn]bool // roomID -> conns
clientsMu sync.Mutex
upgrader websocket.Upgrader
}
func New(agentsDir, skillsDir, roomsDir, usersDir, teamsDir string) *Server {
s := &Server{
e: echo.New(),
agentsDir: agentsDir,
skillsDir: skillsDir,
roomsDir: roomsDir,
usersDir: usersDir,
teamsDir: teamsDir,
rooms: make(map[string]*room.Room),
clients: make(map[string]map[*websocket.Conn]bool),
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
}
s.loadUser()
s.loadRooms()
s.routes()
return s
}
func (s *Server) Start(addr string) error {
return s.e.Start(addr)
}
func (s *Server) routes() {
s.e.Static("/", "web/dist")
g := s.e.Group("/api")
g.GET("/rooms", s.listRooms)
g.POST("/rooms", s.createRoom)
g.GET("/agents", s.listAgents)
g.GET("/agents/:name/files/:file", s.readAgentFile)
g.PUT("/agents/:name/files/:file", s.writeAgentFile)
g.POST("/agents", s.createAgent)
g.DELETE("/agents/:name", s.deleteAgent)
g.POST("/hub/install", s.hubInstall)
g.POST("/hub/install-zip", s.hubInstallZIP)
g.GET("/teams", s.listTeams)
g.GET("/teams/:name", s.getTeam)
g.GET("/teams/:name/agents/:agent/files/:file", s.getTeamAgentFile)
g.PUT("/teams/:name/agents/:agent/files/:file", s.saveTeamAgentFile)
g.GET("/teams/:name/knowledge", s.listTeamKnowledge)
g.GET("/teams/:name/knowledge/:file", s.getTeamKnowledgeFile)
g.PUT("/teams/:name/knowledge/:file", s.saveTeamKnowledgeFile)
g.DELETE("/teams/:name", s.deleteTeam)
g.GET("/rooms/:id/workspace", s.listWorkspace)
g.GET("/rooms/:id/workspace/:file", s.getWorkspaceFile)
g.GET("/rooms/:id/tasks", s.getTasks)
g.GET("/rooms/:id/history", s.listHistory)
g.GET("/rooms/:id/messages", s.getMessages)
g.GET("/user", s.getUser)
g.GET("/user/profile", s.getUserProfile)
g.PUT("/user/profile", s.saveUserProfile)
g.PUT("/user/config", s.saveUserConfig)
s.e.GET("/ws/:roomID", s.wsHandler)
}
func (s *Server) loadRooms() {
entries, _ := os.ReadDir(s.roomsDir)
for _, e := range entries {
if !e.IsDir() {
continue
}
r, err := room.Load(filepath.Join(s.roomsDir, e.Name()), s.agentsDir, s.skillsDir)
if err != nil {
continue
}
r.User = s.user
r.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
s.rooms[e.Name()] = r
}
}
func (s *Server) loadUser() {
u, err := user.Load(s.usersDir)
if err != nil {
u = &user.User{
Config: user.Config{
Name: "用户",
Provider: "deepseek",
Model: "deepseek-chat",
APIKeyEnv: "DEEPSEEK_API_KEY",
AvatarColor: "#5865F2",
},
Profile: "# 我的简介\n\n## 我是谁\n介绍你的身份、职业背景\n\n## 工作风格\n- 喜欢的沟通方式\n- 重视的点\n- 不喜欢的内容\n",
Dir: filepath.Join(s.usersDir, user.DefaultUser),
}
os.MkdirAll(u.Dir, 0755)
os.WriteFile(filepath.Join(u.Dir, "USER.md"), []byte("---\nname: 用户\ndescription: \nprovider: deepseek\nmodel: deepseek-chat\napi_key_env: DEEPSEEK_API_KEY\navatar_color: \"#5865F2\"\n---\n"), 0644)
os.WriteFile(filepath.Join(u.Dir, "PROFILE.md"), []byte(u.Profile), 0644)
}
s.user = u
}
func (s *Server) broadcast(roomID string, ev room.Event) {
s.clientsMu.Lock()
defer s.clientsMu.Unlock()
data, _ := json.Marshal(ev)
for conn := range s.clients[roomID] {
conn.WriteMessage(websocket.TextMessage, data)
}
}
func (s *Server) wsHandler(c echo.Context) error {
roomID := c.Param("roomID")
conn, err := s.upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
s.clientsMu.Lock()
if s.clients[roomID] == nil {
s.clients[roomID] = make(map[*websocket.Conn]bool)
}
s.clients[roomID][conn] = true
s.clientsMu.Unlock()
defer func() {
s.clientsMu.Lock()
delete(s.clients[roomID], conn)
s.clientsMu.Unlock()
conn.Close()
}()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
break
}
var ev struct {
Type string `json:"type"`
Content string `json:"content"`
UserName string `json:"user_name"`
}
if json.Unmarshal(msg, &ev) != nil || ev.Type != "user_message" {
continue
}
s.mu.RLock()
r := s.rooms[roomID]
s.mu.RUnlock()
if r == nil {
continue
}
userName := ev.UserName
if userName == "" {
userName = s.user.GetName()
}
go r.HandleUserMessage(context.Background(), userName, ev.Content)
}
return nil
}
// --- REST handlers ---
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"`
}
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})
}
return c.JSON(200, list)
}
func (s *Server) createRoom(c echo.Context) error {
var cfg room.Config
if err := c.Bind(&cfg); err != nil {
return err
}
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)
r, err := room.Load(dir, s.agentsDir, s.skillsDir)
if err != nil {
return c.JSON(500, map[string]string{"error": err.Error()})
}
r.Broadcast = func(ev room.Event) { s.broadcast(ev.RoomID, ev) }
s.mu.Lock()
s.rooms[cfg.Name] = r
s.mu.Unlock()
return c.JSON(201, map[string]string{"id": cfg.Name})
}
func (s *Server) listAgents(c echo.Context) error {
entries, _ := os.ReadDir(s.agentsDir)
type agentInfo struct {
Name string `json:"name"`
}
var list []agentInfo
for _, e := range entries {
if e.IsDir() {
list = append(list, agentInfo{Name: e.Name()})
}
}
return c.JSON(200, list)
}
func (s *Server) readAgentFile(c echo.Context) error {
name := c.Param("name")
file := c.Param("file") // AGENT.md or SOUL.md
data, err := os.ReadFile(filepath.Join(s.agentsDir, name, file))
if err != nil {
return c.JSON(404, map[string]string{"error": "not found"})
}
return c.JSON(200, map[string]string{"content": string(data)})
}
func (s *Server) writeAgentFile(c echo.Context) error {
name := c.Param("name")
file := c.Param("file")
var body struct {
Content string `json:"content"`
}
if err := c.Bind(&body); err != nil {
return err
}
dir := filepath.Join(s.agentsDir, name)
os.MkdirAll(dir, 0755)
return os.WriteFile(filepath.Join(dir, file), []byte(body.Content), 0644)
}
func (s *Server) createAgent(c echo.Context) error {
var body struct {
Name string `json:"name"`
}
if err := c.Bind(&body); err != nil {
return err
}
dir := filepath.Join(s.agentsDir, body.Name)
os.MkdirAll(filepath.Join(dir, "memory"), 0755)
agentMD := "---\nname: " + body.Name + "\ndescription: \nprovider: deepseek\nmodel: deepseek-chat\napi_key_env: DEEPSEEK_API_KEY\nskills: []\n---\n"
os.WriteFile(filepath.Join(dir, "AGENT.md"), []byte(agentMD), 0644)
os.WriteFile(filepath.Join(dir, "SOUL.md"), []byte("You are "+body.Name+"."), 0644)
return c.JSON(201, map[string]string{"name": body.Name})
}
func (s *Server) deleteAgent(c echo.Context) error {
name := c.Param("name")
return os.RemoveAll(filepath.Join(s.agentsDir, name))
}
func (s *Server) hubInstall(c echo.Context) error {
var body struct {
Repo string `json:"repo"`
}
if err := c.Bind(&body); err != nil {
return err
}
team, err := hub.Install(body.Repo, s.agentsDir, s.skillsDir, s.teamsDir)
if err != nil {
return c.JSON(500, map[string]string{"error": err.Error()})
}
s.loadRooms()
return c.JSON(200, team)
}
func (s *Server) hubInstallZIP(c echo.Context) error {
file, err := c.FormFile("file")
if err != nil {
return c.JSON(400, map[string]string{"error": "no file uploaded"})
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
tmpFile := filepath.Join(os.TempDir(), file.Filename)
dst, err := os.Create(tmpFile)
if err != nil {
return err
}
defer os.Remove(tmpFile)
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return err
}
team, err := hub.InstallZIP(tmpFile, s.agentsDir, s.skillsDir, s.teamsDir)
if err != nil {
return c.JSON(500, map[string]string{"error": err.Error()})
}
s.loadRooms()
return c.JSON(200, team)
}
func (s *Server) listWorkspace(c echo.Context) error {
id := c.Param("id")
dir := filepath.Join(s.roomsDir, id, "workspace")
entries, _ := os.ReadDir(dir)
var files []string
for _, e := range entries {
if !e.IsDir() {
files = append(files, e.Name())
}
}
return c.JSON(200, files)
}
func (s *Server) getTasks(c echo.Context) error {
id := c.Param("id")
data, _ := os.ReadFile(filepath.Join(s.roomsDir, id, "tasks.md"))
return c.JSON(200, map[string]string{"content": string(data)})
}
func (s *Server) listHistory(c echo.Context) error {
id := c.Param("id")
entries, _ := os.ReadDir(filepath.Join(s.roomsDir, id, "history"))
var files []string
for _, e := range entries {
files = append(files, e.Name())
}
return c.JSON(200, files)
}
func (s *Server) createSkill(c echo.Context) error {
var body struct {
Name string `json:"name"`
Content string `json:"content"`
}
if err := c.Bind(&body); err != nil {
return err
}
dir := filepath.Join(s.skillsDir, body.Name)
os.MkdirAll(dir, 0755)
content := body.Content
if content == "" {
content = "---\nname: " + body.Name + "\ndescription: \n---\n\n# " + body.Name + "\n\n描述这个 skill 的用途和使用步骤。\n"
}
return os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(content), 0644)
}
func (s *Server) getWorkspaceFile(c echo.Context) error {
id := c.Param("id")
file := c.Param("file")
data, err := os.ReadFile(filepath.Join(s.roomsDir, id, "workspace", file))
if err != nil {
return c.JSON(404, map[string]string{"error": "not found"})
}
return c.JSON(200, map[string]string{"content": string(data)})
}
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 {
return c.JSON(200, []interface{}{})
}
type msg struct {
ID string `json:"id"`
Agent string `json:"agent"`
Role string `json:"role"`
Content string `json:"content"`
}
var msgs []msg
lines := strings.Split(string(data), "\n## ")
for i, block := range lines {
if block == "" {
continue
}
// Parse "**[HH:MM:SS] agentName** (role)\n\ncontent"
parts := strings.SplitN(block, "\n\n", 2)
if len(parts) < 2 {
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, ")")
if agentName == "" {
agentName = "unknown"
}
if role == "" {
role = "member"
}
msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: role, Content: content})
}
return c.JSON(200, msgs)
}
func (s *Server) getUser(c echo.Context) error {
if s.user == nil {
return c.JSON(404, map[string]string{"error": "user not found"})
}
return c.JSON(200, map[string]interface{}{
"name": s.user.GetName(),
"provider": s.user.GetProvider(),
"model": s.user.GetModel(),
"api_key_env": s.user.GetAPIKeyEnv(),
"avatar_color": s.user.GetAvatarColor(),
"description": s.user.Config.Description,
"has_profile": s.user.Profile != "",
})
}
func (s *Server) getUserProfile(c echo.Context) error {
if s.user == nil {
return c.JSON(404, map[string]string{"error": "user not found"})
}
return c.JSON(200, map[string]string{"content": s.user.Profile})
}
func (s *Server) saveUserProfile(c echo.Context) error {
if s.user == nil {
return c.JSON(404, map[string]string{"error": "user not found"})
}
var body struct {
Content string `json:"content"`
}
if err := c.Bind(&body); err != nil {
return err
}
if err := s.user.SaveProfile(body.Content); err != nil {
return err
}
return c.JSON(200, map[string]string{"status": "ok"})
}
func (s *Server) saveUserConfig(c echo.Context) error {
if s.user == nil {
return c.JSON(404, map[string]string{"error": "user not found"})
}
var body struct {
Name string `json:"name"`
Description string `json:"description"`
Provider string `json:"provider"`
Model string `json:"model"`
APIKeyEnv string `json:"api_key_env"`
AvatarColor string `json:"avatar_color"`
}
if err := c.Bind(&body); err != nil {
return err
}
cfg := s.user.Config
if body.Name != "" {
cfg.Name = body.Name
}
if body.Description != "" {
cfg.Description = body.Description
}
if body.Provider != "" {
cfg.Provider = body.Provider
}
if body.Model != "" {
cfg.Model = body.Model
}
if body.APIKeyEnv != "" {
cfg.APIKeyEnv = body.APIKeyEnv
}
if body.AvatarColor != "" {
cfg.AvatarColor = body.AvatarColor
}
if err := s.user.SaveConfig(cfg); err != nil {
return err
}
return c.JSON(200, map[string]string{"status": "ok"})
}
// --- Teams API ---
func (s *Server) listTeams(c echo.Context) error {
entries, err := os.ReadDir(s.teamsDir)
if err != nil {
return c.JSON(200, []interface{}{})
}
var teams []interface{}
for _, e := range entries {
if !e.IsDir() {
continue
}
team, err := loadTeam(filepath.Join(s.teamsDir, e.Name(), "team.yaml"))
if err != nil {
continue
}
teams = append(teams, map[string]interface{}{
"name": team.Name,
"description": team.Description,
"author": team.Author,
"repo_url": team.RepoURL,
"agents": team.Agents,
"skills": team.Skills,
"installed_at": team.InstalledAt,
})
}
return c.JSON(200, teams)
}
func (s *Server) getTeam(c echo.Context) error {
name := c.Param("name")
team, err := loadTeam(filepath.Join(s.teamsDir, name, "team.yaml"))
if err != nil {
return c.JSON(404, map[string]string{"error": "team not found"})
}
return c.JSON(200, map[string]interface{}{
"name": team.Name,
"description": team.Description,
"author": team.Author,
"repo_url": team.RepoURL,
"agents": team.Agents,
"skills": team.Skills,
"installed_at": team.InstalledAt,
})
}
func (s *Server) getTeamAgentFile(c echo.Context) error {
teamName := c.Param("name")
agentName := c.Param("agent")
fileName := c.Param("file")
path := filepath.Join(s.agentsDir, teamName, agentName, fileName)
data, err := os.ReadFile(path)
if err != nil {
return c.JSON(404, map[string]string{"error": "file not found"})
}
return c.JSON(200, map[string]string{"content": string(data)})
}
func (s *Server) saveTeamAgentFile(c echo.Context) error {
teamName := c.Param("name")
agentName := c.Param("agent")
fileName := c.Param("file")
path := filepath.Join(s.agentsDir, teamName, agentName, fileName)
var body struct {
Content string `json:"content"`
}
if err := c.Bind(&body); err != nil {
return err
}
os.MkdirAll(filepath.Dir(path), 0755)
if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil {
return err
}
return c.JSON(200, map[string]string{"status": "ok"})
}
func (s *Server) listTeamKnowledge(c echo.Context) error {
name := c.Param("name")
dir := filepath.Join(s.agentsDir, name, "knowledge")
entries, err := os.ReadDir(dir)
if err != nil {
return c.JSON(200, []string{})
}
var files []string
for _, e := range entries {
if !e.IsDir() {
files = append(files, e.Name())
}
}
return c.JSON(200, files)
}
func (s *Server) getTeamKnowledgeFile(c echo.Context) error {
name := c.Param("name")
fileName := c.Param("file")
path := filepath.Join(s.agentsDir, name, "knowledge", fileName)
data, err := os.ReadFile(path)
if err != nil {
return c.JSON(404, map[string]string{"error": "file not found"})
}
return c.JSON(200, map[string]string{"content": string(data)})
}
func (s *Server) saveTeamKnowledgeFile(c echo.Context) error {
name := c.Param("name")
fileName := c.Param("file")
path := filepath.Join(s.agentsDir, name, "knowledge", fileName)
var body struct {
Content string `json:"content"`
}
if err := c.Bind(&body); err != nil {
return err
}
os.MkdirAll(filepath.Dir(path), 0755)
if err := os.WriteFile(path, []byte(body.Content), 0644); err != nil {
return err
}
return c.JSON(200, map[string]string{"status": "ok"})
}
func (s *Server) deleteTeam(c echo.Context) error {
name := c.Param("name")
// Delete team directory
if err := os.RemoveAll(filepath.Join(s.teamsDir, name)); err != nil {
return err
}
// Delete agents
if err := os.RemoveAll(filepath.Join(s.agentsDir, name)); err != nil {
// Continue even if error
}
return c.JSON(200, map[string]string{"status": "ok"})
}
func loadTeam(path string) (*hub.Team, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Simple YAML parse
team := &hub.Team{}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "name: ") {
team.Name = strings.TrimSpace(strings.TrimPrefix(line, "name: "))
} else if strings.HasPrefix(line, "description: ") {
team.Description = strings.TrimSpace(strings.TrimPrefix(line, "description: "))
} else if strings.HasPrefix(line, "author: ") {
team.Author = strings.TrimSpace(strings.TrimPrefix(line, "author: "))
} else if strings.HasPrefix(line, "repo_url: ") {
team.RepoURL = strings.TrimSpace(strings.TrimPrefix(line, "repo_url: "))
} else if strings.HasPrefix(line, "installed_at: ") {
team.InstalledAt = strings.TrimSpace(strings.TrimPrefix(line, "installed_at: "))
} else if strings.HasPrefix(line, " - ") {
team.Agents = append(team.Agents, strings.TrimSpace(strings.TrimPrefix(line, " - ")))
}
}
return team, nil
}