- 新增 Plan/Build 模式切换(Tab 键),Plan 模式阻止任务执行 - Build 模式:成员输出智能判断,文档存 artifact,提问显示聊天 - 成员可直接与用户对话(多轮),不经过 master 传话 - 任务计划文档自动生成,沟通记录自动追加 - 右侧面板重构为产出物面板,支持查看/编辑/保存 - 输入框改为 textarea,支持 Shift+Enter 换行,修复输入法 Enter 误发送 - Master 会话历史持久化,支持多轮上下文 - parseAssignments 支持多行任务描述 - SOUL.md 热重载、skill 递归发现与内容注入 - 需求确认 skill(HARD-GATE 模式) - air 热重载配置 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
806 lines
22 KiB
Go
806 lines
22 KiB
Go
package api
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"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.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)
|
||
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.PUT("/rooms/:id/workspace/:file", s.putWorkspaceFile)
|
||
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"`
|
||
Mode string `json:"mode"`
|
||
}
|
||
if json.Unmarshal(msg, &ev) != nil {
|
||
continue
|
||
}
|
||
s.mu.RLock()
|
||
r := s.rooms[roomID]
|
||
s.mu.RUnlock()
|
||
if r == nil {
|
||
continue
|
||
}
|
||
if ev.Type == "set_mode" && (ev.Mode == "plan" || ev.Mode == "build") {
|
||
r.Mode = ev.Mode
|
||
s.broadcast(roomID, room.Event{Type: room.EvtModeChange, RoomID: roomID, Mode: ev.Mode})
|
||
continue
|
||
}
|
||
if ev.Type != "user_message" {
|
||
continue
|
||
}
|
||
userName := ev.UserName
|
||
if userName == "" {
|
||
userName = s.user.GetName()
|
||
}
|
||
go func() {
|
||
if err := r.HandleUserMessage(context.Background(), userName, ev.Content); err != nil {
|
||
log.Printf("[room %s] HandleUserMessage error: %v", roomID, err)
|
||
}
|
||
}()
|
||
}
|
||
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"`
|
||
Members []string `json:"members"`
|
||
Color string `json:"color"`
|
||
Team string `json:"team"`
|
||
Mode string `json:"mode"`
|
||
}
|
||
var list []roomInfo
|
||
for id, r := range s.rooms {
|
||
mode := r.Mode
|
||
if mode == "" {
|
||
mode = "plan"
|
||
}
|
||
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, Mode: mode})
|
||
}
|
||
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
|
||
}
|
||
if cfg.Type == "" {
|
||
cfg.Type = room.TypeProject
|
||
}
|
||
dir := filepath.Join(s.roomsDir, cfg.Name)
|
||
os.MkdirAll(filepath.Join(dir, "workspace"), 0755)
|
||
os.MkdirAll(filepath.Join(dir, "history"), 0755)
|
||
|
||
s.writeRoomFile(dir, cfg)
|
||
|
||
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) 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 {
|
||
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) putWorkspaceFile(c echo.Context) error {
|
||
id := c.Param("id")
|
||
file := c.Param("file")
|
||
var body struct {
|
||
Content string `json:"content"`
|
||
}
|
||
if err := c.Bind(&body); err != nil {
|
||
return err
|
||
}
|
||
dir := filepath.Join(s.roomsDir, id, "workspace")
|
||
os.MkdirAll(dir, 0755)
|
||
if err := os.WriteFile(filepath.Join(dir, file), []byte(body.Content), 0644); err != nil {
|
||
return err
|
||
}
|
||
return c.JSON(200, map[string]string{"status": "ok"})
|
||
}
|
||
|
||
func (s *Server) getMessages(c echo.Context) error {
|
||
id := c.Param("id")
|
||
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
|
||
// Format: "\n**[HH:MM:SS] agentName** (role)\n\ncontent\n"
|
||
blocks := strings.Split(string(data), "\n**[")
|
||
for i, block := range blocks {
|
||
if block == "" {
|
||
continue
|
||
}
|
||
// block starts with "HH:MM:SS] agentName** (role)\n\ncontent"
|
||
headerEnd := strings.Index(block, "\n\n")
|
||
if headerEnd < 0 {
|
||
continue
|
||
}
|
||
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 roleStr == "" {
|
||
roleStr = "member"
|
||
}
|
||
msgs = append(msgs, msg{ID: fmt.Sprintf("%d", i), Agent: agentName, Role: roleStr, 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{}{
|
||
"id": e.Name(),
|
||
"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
|
||
}
|
||
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, " - ") {
|
||
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
|
||
}
|