Compare commits

...

3 Commits

Author SHA1 Message Date
5b50d6ff9a refactor: 更新skill文件结构
- 删除旧的skill定义文件
- 新增 skills@xiaohongshu-creator skill

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:17:16 +08:00
19f5c79d58 feat: 更新前端页面和agent应用
- Agents.vue: 更新agent列表和创建功能
- Skill.vue: 更新skill页面
- skill.ts: 更新skill编辑逻辑
- agent/app/main.py: 更新agent应用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:17:08 +08:00
7795685f43 feat: 更新后端agent和skill服务
- agent_handler.go: 新增ListAgents、CreateAgent接口
- skill_handler.go: 更新skill内容获取和保存功能
- agent_service.go: 新增agent服务逻辑
- main.go: 新增agent路由

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 17:16:53 +08:00
11 changed files with 569 additions and 215 deletions

View File

@@ -142,6 +142,31 @@ class TeamChatRequest(BaseModel):
strategy: str = "parallel" strategy: str = "parallel"
class CreateAgentRequest(BaseModel):
"""创建智能体请求"""
name: str
description: Optional[str] = None
avatar: str = "🤖"
# 技能配置
skills_mode: str = "all" # all / include / exclude
skills: list[str] = [] # 技能ID列表
# 知识库
knowledge: str = "general" # general / codebase / docs / api
# 自定义提示词
prompt: Optional[str] = None
# 模型配置
model_provider: Optional[str] = None
model_name: Optional[str] = None
user_id: int = 1
class CreateAgentResponse(BaseModel):
"""创建智能体响应"""
agent_id: int
name: str
message: str = "Agent created successfully"
class ChatResponse(BaseModel): class ChatResponse(BaseModel):
"""对话响应""" """对话响应"""
agent_id: int agent_id: int
@@ -443,6 +468,73 @@ async def team_chat(request: TeamChatRequest):
} }
@app.post("/agent/create", response_model=CreateAgentResponse)
async def create_agent(request: CreateAgentRequest):
"""
创建新的智能体
"""
import json
import uuid
# 生成唯一的 agent_id
agent_id = int(datetime.now().timestamp() * 1000) % 100000
# 构建 Agent 配置
agent_config = {
"id": agent_id,
"name": request.name,
"description": request.description or "",
"avatar": request.avatar,
"skills_mode": request.skills_mode,
"skills": request.skills,
"knowledge": request.knowledge,
"role_description": request.prompt or f"You are {request.name}. {request.description or ''}",
"model_provider": request.model_provider or "anthropic",
"model_name": request.model_name or "claude-sonnet-4-20250514",
}
# 保存到 agents 目录
agents_dir = os.path.join(os.path.dirname(__file__), "agents")
os.makedirs(agents_dir, exist_ok=True)
config_file = os.path.join(agents_dir, f"agent_{agent_id}.json")
with open(config_file, "w", encoding="utf-8") as f:
json.dump(agent_config, f, ensure_ascii=False, indent=2)
logger.info(f"Agent created: {request.name} (ID: {agent_id})")
return CreateAgentResponse(
agent_id=agent_id,
name=request.name,
message="Agent created successfully"
)
@app.get("/agent/list")
async def list_agents():
"""
获取智能体列表
"""
import json
agents_dir = os.path.join(os.path.dirname(__file__), "agents")
if not os.path.exists(agents_dir):
return {"agents": []}
agents = []
for file in os.listdir(agents_dir):
if file.endswith(".json"):
config_file = os.path.join(agents_dir, file)
try:
with open(config_file, "r", encoding="utf-8") as f:
agent = json.load(f)
agents.append(agent)
except:
continue
return {"agents": agents}
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
port = int(os.getenv("AGENT_PORT", "8081")) port = int(os.getenv("AGENT_PORT", "8081"))

View File

@@ -1,8 +0,0 @@
---
name: 123
description: 123
---
# 123
123

View File

@@ -1,155 +0,0 @@
---
name: jimliu/baoyu-skills@baoyu-article-illustrator
description: Analyzes article structure, identifies positions requiring visual aids, generates illustrations with Type × Style two-dimension approach. Use when user asks to "illustrate article", "add images", "generate images for article", or "为文章配图".
---
# Article Illustrator
Analyze articles, identify illustration positions, generate images with Type × Style consistency.
## Two Dimensions
| Dimension | Controls | Examples |
|-----------|----------|----------|
| **Type** | Information structure | infographic, scene, flowchart, comparison, framework, timeline |
| **Style** | Visual aesthetics | notion, warm, minimal, blueprint, watercolor, elegant |
Combine freely: `--type infographic --style blueprint`
## Types
| Type | Best For |
|------|----------|
| `infographic` | Data, metrics, technical |
| `scene` | Narratives, emotional |
| `flowchart` | Processes, workflows |
| `comparison` | Side-by-side, options |
| `framework` | Models, architecture |
| `timeline` | History, evolution |
## Styles
See [references/styles.md](references/styles.md) for Core Styles, full gallery, and Type × Style compatibility.
## Workflow
```
- [ ] Step 1: Pre-check (EXTEND.md, references, config)
- [ ] Step 2: Analyze content
- [ ] Step 3: Confirm settings (AskUserQuestion)
- [ ] Step 4: Generate outline
- [ ] Step 5: Generate images
- [ ] Step 6: Finalize
```
### Step 1: Pre-check
**1.5 Load Preferences (EXTEND.md) ⛔ BLOCKING**
```bash
test -f .baoyu-skills/baoyu-article-illustrator/EXTEND.md && echo "project"
test -f "$HOME/.baoyu-skills/baoyu-article-illustrator/EXTEND.md" && echo "user"
```
| Result | Action |
|--------|--------|
| Found | Read, parse, display summary |
| Not found | ⛔ Run [first-time-setup](references/config/first-time-setup.md) |
Full procedures: [references/workflow.md](references/workflow.md#step-1-pre-check)
### Step 2: Analyze
| Analysis | Output |
|----------|--------|
| Content type | Technical / Tutorial / Methodology / Narrative |
| Purpose | information / visualization / imagination |
| Core arguments | 2-5 main points |
| Positions | Where illustrations add value |
**CRITICAL**: Metaphors → visualize underlying concept, NOT literal image.
Full procedures: [references/workflow.md](references/workflow.md#step-2-setup--analyze)
### Step 3: Confirm Settings ⚠️
**ONE AskUserQuestion, max 4 Qs. Q1-Q3 REQUIRED.**
| Q | Options |
|---|---------|
| **Q1: Type** | [Recommended], infographic, scene, flowchart, comparison, framework, timeline, mixed |
| **Q2: Density** | minimal (1-2), balanced (3-5), per-section (Recommended), rich (6+) |
| **Q3: Style** | [Recommended], minimal-flat, sci-fi, hand-drawn, editorial, scene, Other |
| Q4: Language | When article language ≠ EXTEND.md setting |
Full procedures: [references/workflow.md](references/workflow.md#step-3-confirm-settings-)
### Step 4: Generate Outline
Save `outline.md` with frontmatter (type, density, style, image_count) and entries:
```yaml
## Illustration 1
**Position**: [section/paragraph]
**Purpose**: [why]
**Visual Content**: [what]
**Filename**: 01-infographic-concept-name.png
```
Full template: [references/workflow.md](references/workflow.md#step-4-generate-outline)
### Step 5: Generate Images
**BLOCKING: Prompt files MUST be saved before ANY image generation.**
1. For each illustration, create a prompt file per [references/prompt-construction.md](references/prompt-construction.md)
2. Save to `prompts/NN-{type}-{slug}.md` with YAML frontmatter
3. Prompts **MUST** use type-specific templates with structured sections (ZONES / LABELS / COLORS / STYLE / ASPECT)
4. LABELS **MUST** include article-specific data: actual numbers, terms, metrics, quotes
5. **DO NOT** pass ad-hoc inline prompts to `--prompt` without saving prompt files first
6. Select generation skill, process references (`direct`/`style`/`palette`)
7. Apply watermark if EXTEND.md enabled
8. Generate from saved prompt files; retry once on failure
Full procedures: [references/workflow.md](references/workflow.md#step-5-generate-images)
### Step 6: Finalize
Insert `![description](path/NN-{type}-{slug}.png)` after paragraphs.
```
Article Illustration Complete!
Article: [path] | Type: [type] | Density: [level] | Style: [style]
Images: X/N generated
```
## Output Directory
```
illustrations/{topic-slug}/
├── source-{slug}.{ext}
├── references/ # if provided
├── outline.md
├── prompts/
└── NN-{type}-{slug}.png
```
**Slug**: 2-4 words, kebab-case. **Conflict**: append `-YYYYMMDD-HHMMSS`.
## Modification
| Action | Steps |
|--------|-------|
| Edit | Update prompt → Regenerate → Update reference |
| Add | Position → Prompt → Generate → Update outline → Insert |
| Delete | Delete files → Remove reference → Update outline |
## References
| File | Content |
|------|---------|
| [references/workflow.md](references/workflow.md) | Detailed procedures |
| [references/usage.md](references/usage.md) | Command syntax |
| [references/styles.md](references/styles.md) | Style gallery |
| [references/prompt-construction.md](references/prompt-construction.md) | Prompt templates |
| [references/config/first-time-setup.md](references/config/first-time-setup.md) | First-time setup |

View File

@@ -500,6 +500,8 @@ func main() {
// Agent管理模块 (调用 Python Agent Engine) // Agent管理模块 (调用 Python Agent Engine)
agentGroup := r.Group("/api/agent") agentGroup := r.Group("/api/agent")
{ {
agentGroup.GET("/list", agentHandler.ListAgents)
agentGroup.POST("/create", agentHandler.CreateAgent)
agentGroup.POST("/chat", agentHandler.Chat) agentGroup.POST("/chat", agentHandler.Chat)
agentGroup.POST("/chat/stream", agentHandler.ChatStream) agentGroup.POST("/chat/stream", agentHandler.ChatStream)
agentGroup.POST("/team/chat", agentHandler.TeamChat) agentGroup.POST("/team/chat", agentHandler.TeamChat)

View File

@@ -40,7 +40,27 @@ type ChatResponse struct {
Metadata interface{} `json:"metadata"` Metadata interface{} `json:"metadata"`
} }
// CreateAgentResponse 创建智能体响应
type CreateAgentResponse struct {
AgentID int `json:"agent_id"`
Name string `json:"name"`
Message string `json:"message"`
}
// ListAgentsResponse 获取智能体列表响应
type ListAgentsResponse struct {
Agents []interface{} `json:"agents"`
}
// Chat 单智能体对话 // Chat 单智能体对话
// @Summary 单智能体对话
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body ChatRequest true "对话请求"
// @Success 200 {object} ChatResponse
// @Router /api/agent/chat [post]
func (h *AgentHandler) Chat(c *gin.Context) { func (h *AgentHandler) Chat(c *gin.Context) {
var req ChatRequest var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -88,6 +108,12 @@ func (h *AgentHandler) Chat(c *gin.Context) {
} }
// ChatStream 单智能体对话(流式输出) // ChatStream 单智能体对话(流式输出)
// @Summary 单智能体对话(流式输出)
// @Tags 智能体管理
// @Accept json
// @Produce text/event-stream
// @Param request body ChatRequest true "对话请求"
// @Router /api/agent/chat/stream [post]
func (h *AgentHandler) ChatStream(c *gin.Context) { func (h *AgentHandler) ChatStream(c *gin.Context) {
var req ChatRequest var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -132,6 +158,13 @@ type TeamChatResponse struct {
} }
// TeamChat 多智能体群聊 // TeamChat 多智能体群聊
// @Summary 多智能体群聊
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body TeamChatRequest true "群聊请求"
// @Success 200 {object} TeamChatResponse
// @Router /api/agent/team/chat [post]
func (h *AgentHandler) TeamChat(c *gin.Context) { func (h *AgentHandler) TeamChat(c *gin.Context) {
var req TeamChatRequest var req TeamChatRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
@@ -167,3 +200,46 @@ func (h *AgentHandler) TeamChat(c *gin.Context) {
Metadata: nil, Metadata: nil,
}) })
} }
// CreateAgent 创建智能体
// @Summary 创建智能体
// @Tags 智能体管理
// @Accept json
// @Produce json
// @Param request body CreateAgentRequest true "创建智能体请求"
// @Success 200 {object} CreateAgentResponse
// @Router /api/agent/create [post]
func (h *AgentHandler) CreateAgent(c *gin.Context) {
var req service.CreateAgentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取用户 ID
userID := 1 // TODO: 从 c.Get("user_id") 获取
result, err := h.agentService.CreateAgent(req, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
// ListAgents 获取智能体列表
// @Summary 获取智能体列表
// @Tags 智能体管理
// @Produce json
// @Success 200 {object} ListAgentsResponse
// @Router /api/agent/list [get]
func (h *AgentHandler) ListAgents(c *gin.Context) {
result, err := h.agentService.ListAgents()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}

View File

@@ -2,9 +2,12 @@ package handler
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings"
"x-agents/server/internal/model" "x-agents/server/internal/model"
"x-agents/server/internal/service" "x-agents/server/internal/service"
@@ -115,18 +118,17 @@ func (h *SkillHandler) Create(c *gin.Context) {
} }
userID, _ := c.Get("userID") userID, _ := c.Get("userID")
// skillName 和 skillDesc 改为可选,如果没传则从文件内容解析
// 先从前端获取,但如果上传了文件则以文件内容解析的为准
skillName := c.PostForm("skill_name") skillName := c.PostForm("skill_name")
skillDesc := c.PostForm("skill_desc") skillDesc := c.PostForm("skill_desc")
skillType := c.PostForm("skill_type") skillType := c.PostForm("skill_type")
if skillName == "" { // 记录原始输入(用于调试)
c.JSON(http.StatusBadRequest, gin.H{"error": "skill_name is required"}) originalSkillName := skillName
return
}
// 确定技能类型优先使用传入的type否则根据用户角色判断 // 确定技能类型优先使用传入的type否则根据用户角色判断
if skillType == "" { if skillType == "" {
// 判断是否为管理员用户
if username == "admin" { if username == "admin" {
skillType = "system" skillType = "system"
} else { } else {
@@ -147,24 +149,87 @@ func (h *SkillHandler) Create(c *gin.Context) {
skillDir = "system" skillDir = "system"
} }
// 用于存储最终路径
var skillPath string
// 处理文件上传
file, err := c.FormFile("file")
if err == nil {
// 读取文件内容,解析 YAML front matter 获取 name
fileContent, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read file: " + err.Error()})
return
}
defer fileContent.Close()
content := make([]byte, 1024*1024) // 最多读取 1MB
n, _ := fileContent.Read(content)
contentStr := string(content[:n])
// 解析 name
parsedName, parsedDesc := parseSkillMeta(contentStr)
log.Printf("[SkillHandler] Original skill_name from form: %s, parsed name from file: %s", originalSkillName, parsedName)
// 优先使用文件解析出的 name
if parsedName != "" {
skillName = parsedName
} else if skillName == "" {
// 如果解析不到且表单也没传,用文件名
skillName = filepath.Base(file.Filename)
}
if parsedDesc != "" {
skillDesc = parsedDesc
}
// 清理 skillName只保留纯文件名去除所有路径
skillName = filepath.Base(skillName)
// 去除 .md 后缀
skillName = strings.TrimSuffix(skillName, ".md")
// 去除空格
skillName = strings.TrimSpace(skillName)
// 如果包含路径分隔符,取最后一部分
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
skillName = skillName[idx+1:]
}
log.Printf("[SkillHandler] Final skill name: %s", skillName)
// 创建技能目录 // 创建技能目录
skillPath := filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName) skillPath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
if err := os.MkdirAll(skillPath, 0755); err != nil { if err := os.MkdirAll(skillPath, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
return return
} }
// 处理文件上传 // 保存文件(使用之前读取的内容)
file, err := c.FormFile("file")
if err == nil {
// 保存上传的文件为 SKILL.md
skillFilePath := filepath.Join(skillPath, "SKILL.md") skillFilePath := filepath.Join(skillPath, "SKILL.md")
if err := c.SaveUploadedFile(file, skillFilePath); err != nil { if err := os.WriteFile(skillFilePath, []byte(contentStr), 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save skill file: " + err.Error()})
return return
} }
} else { } else {
// 如果没有上传文件,创建一个默认的 SKILL.md // 如果没有上传文件但提供了 name 和 desc创建默认文件
if skillName == "" || skillDesc == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill_name and skill_desc are required when no file uploaded"})
return
}
// 清理 skillName
skillName = filepath.Base(skillName)
skillName = strings.TrimSuffix(skillName, ".md")
skillName = strings.TrimSpace(skillName)
if idx := strings.LastIndexAny(skillName, "/\\"); idx >= 0 {
skillName = skillName[idx+1:]
}
skillPath = filepath.Join(projectRoot, "core", "agents", "skills", skillDir, skillName)
if err := os.MkdirAll(skillPath, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create skill directory: " + err.Error()})
return
}
defaultContent := "---\nname: " + skillName + "\ndescription: " + skillDesc + "\n---\n\n# " + skillName + "\n\n" + skillDesc defaultContent := "---\nname: " + skillName + "\ndescription: " + skillDesc + "\n---\n\n# " + skillName + "\n\n" + skillDesc
skillFilePath := filepath.Join(skillPath, "SKILL.md") skillFilePath := filepath.Join(skillPath, "SKILL.md")
if err := os.WriteFile(skillFilePath, []byte(defaultContent), 0644); err != nil { if err := os.WriteFile(skillFilePath, []byte(defaultContent), 0644); err != nil {
@@ -196,6 +261,45 @@ func (h *SkillHandler) Create(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill}) c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill})
} }
// parseSkillMeta 解析 SKILL.md 内容,提取 name 和 description
func parseSkillMeta(content string) (name string, description string) {
contentStr := strings.TrimSpace(content)
if !strings.HasPrefix(contentStr, "---") {
return "", ""
}
// 找到第二个 ---
lines := strings.Split(contentStr, "\n")
var endIdx int
for i := 1; i < len(lines); i++ {
if strings.TrimSpace(lines[i]) == "---" {
endIdx = i
break
}
}
if endIdx == 0 {
return "", ""
}
// 提取 YAML 内容
yamlContent := strings.Join(lines[1:endIdx], "\n")
// 解析 name
nameMatch := regexp.MustCompile(`name:\s*(.+)`).FindStringSubmatch(yamlContent)
if len(nameMatch) > 1 {
name = strings.TrimSpace(nameMatch[1])
}
// 解析 description
descMatch := regexp.MustCompile(`description:\s*(.+)`).FindStringSubmatch(yamlContent)
if len(descMatch) > 1 {
description = strings.TrimSpace(descMatch[1])
}
return name, description
}
// getProjectRoot 获取项目根目录 // getProjectRoot 获取项目根目录
func (h *SkillHandler) getProjectRoot() string { func (h *SkillHandler) getProjectRoot() string {
execPath, _ := os.Getwd() execPath, _ := os.Getwd()
@@ -262,11 +366,28 @@ func (h *SkillHandler) Update(c *gin.Context) {
return return
} }
skillName := c.PostForm("skill_name") // 支持 JSON 格式的请求
skillDesc := c.PostForm("skill_desc") var req struct {
skillType := c.PostForm("skill_type") SkillName string `json:"skill_name"`
SkillDesc string `json:"skill_desc"`
SkillType string `json:"skill_type"`
Status string `json:"status"`
}
// 尝试解析 JSON如果失败则用 PostForm
if err := c.ShouldBindJSON(&req); err != nil {
req.SkillName = c.PostForm("skill_name")
req.SkillDesc = c.PostForm("skill_desc")
req.SkillType = c.PostForm("skill_type")
req.Status = c.PostForm("status")
}
// 如果没有传则使用现有值 // 如果没有传则使用现有值
skillName := req.SkillName
skillDesc := req.SkillDesc
skillType := req.SkillType
status := req.Status
if skillName == "" { if skillName == "" {
skillName = existingSkill.SkillName skillName = existingSkill.SkillName
} }
@@ -276,6 +397,9 @@ func (h *SkillHandler) Update(c *gin.Context) {
if skillType == "" { if skillType == "" {
skillType = existingSkill.SkillType skillType = existingSkill.SkillType
} }
if status == "" {
status = existingSkill.Status
}
// 获取项目根目录 // 获取项目根目录
projectRoot := h.getProjectRoot() projectRoot := h.getProjectRoot()
@@ -321,6 +445,7 @@ func (h *SkillHandler) Update(c *gin.Context) {
existingSkill.SkillName = skillName existingSkill.SkillName = skillName
existingSkill.SkillDesc = skillDesc existingSkill.SkillDesc = skillDesc
existingSkill.SkillType = skillType existingSkill.SkillType = skillType
existingSkill.Status = status
if err := h.skillService.UpdateSkill(existingSkill); err != nil { if err := h.skillService.UpdateSkill(existingSkill); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

View File

@@ -279,3 +279,118 @@ func (s *AgentService) ChatStream(c interface{}, agentID int, message, sessionID
return nil return nil
} }
// CreateAgentRequest 创建智能体请求
type CreateAgentRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Avatar string `json:"avatar"`
SkillsMode string `json:"skills_mode"`
Skills []string `json:"skills"`
Knowledge string `json:"knowledge"`
Prompt string `json:"prompt"`
ModelProvider string `json:"model_provider"`
ModelName string `json:"model_name"`
UserID int `json:"user_id"`
}
// CreateAgentResponse 创建智能体响应
type CreateAgentResponse struct {
AgentID int `json:"agent_id"`
Name string `json:"name"`
Message string `json:"message"`
}
// CreateAgent 创建智能体
func (s *AgentService) CreateAgent(req CreateAgentRequest, userID int) (*CreateAgentResponse, error) {
url := fmt.Sprintf("%s/agent/create", s.pythonURL)
// 构建请求体
pythonReq := CreateAgentRequest{
Name: req.Name,
Description: req.Description,
Avatar: req.Avatar,
SkillsMode: req.SkillsMode,
Skills: req.Skills,
Knowledge: req.Knowledge,
Prompt: req.Prompt,
ModelProvider: req.ModelProvider,
ModelName: req.ModelName,
UserID: userID,
}
jsonData, err := json.Marshal(pythonReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to call python agent: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("python agent error: %s", string(body))
}
var result CreateAgentResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
log.Printf("[AgentService] Agent created: %s (ID: %d)", result.Name, result.AgentID)
return &result, nil
}
// ListAgentsResponse 获取智能体列表响应
type ListAgentsResponse struct {
Agents []interface{} `json:"agents"`
}
// ListAgents 获取智能体列表
func (s *AgentService) ListAgents() (*ListAgentsResponse, error) {
url := fmt.Sprintf("%s/agent/list", s.pythonURL)
httpReq, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to call python agent: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("python agent error: %s", string(body))
}
var result ListAgentsResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
log.Printf("[AgentService] Listed agents: %d", len(result.Agents))
return &result, nil
}

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
const API_BASE = 'http://localhost:8082' const API_BASE = 'http://localhost:8082'
@@ -14,13 +15,35 @@ interface Skill {
} }
// Agents 数据 // Agents 数据
const agents = ref([ const agents = ref<any[]>([])
{ id: 1, name: 'Claude Agent', avatar: '🧠', description: 'General purpose AI assistant', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-2.0-flash', mcpServers: 2, createdAt: '2025-04-10' },
{ id: 2, name: 'Code Assistant', avatar: '💻', description: 'Specialized in code generation', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'running' as const, framework: 'OpenAI', model: 'gpt-4o', mcpServers: 1, createdAt: '2025-04-08' }, // 获取智能体列表
{ id: 3, name: 'Data Analyst', avatar: '📊', description: 'Data analysis and visualization', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'stopped' as const, framework: 'PydanticAI', model: 'gpt-4o-mini', mcpServers: 3, createdAt: '2025-04-05' }, const fetchAgents = async () => {
{ id: 4, name: 'Research Bot', avatar: '🔬', description: 'Academic research assistant', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'running' as const, framework: 'LangChain', model: 'claude-3-5-sonnet', mcpServers: 2, createdAt: '2025-04-12' }, try {
{ id: 5, name: '客服助手', avatar: '🎧', description: 'Customer support agent', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-1.5-pro', mcpServers: 4, createdAt: '2025-04-11' }, const response = await fetch('http://localhost:8082/api/agent/list')
]) if (!response.ok) throw new Error('Failed to fetch agents')
const data = await response.json()
// 转换后端数据为前端格式
agents.value = (data.agents || []).map((agent: any) => ({
id: agent.id,
name: agent.name,
avatar: agent.avatar || '🤖',
description: agent.description || '',
accentColor: '#f97316',
gradient: 'from-orange-500/20 to-amber-500/20',
status: 'stopped' as const,
framework: agent.skills?.length > 0 ? agent.skills.join(', ') : 'None',
model: agent.model_name || 'claude-sonnet-4-20250514',
mcpServers: 0,
createdAt: new Date().toISOString().split('T')[0],
}))
} catch (error) {
console.error('Failed to fetch agents:', error)
// 保持空数组,不使用假数据
}
}
// 创建智能体弹窗状态 // 创建智能体弹窗状态
const showCreateModal = ref(false) const showCreateModal = ref(false)
@@ -65,8 +88,10 @@ const selectSkillsMode = (mode: 'all' | 'include' | 'exclude') => {
} }
// 处理技能模式点击(用于模板) // 处理技能模式点击(用于模板)
const handleSkillsModeClick = (mode: 'all' | 'include' | 'exclude') => { const handleSkillsModeClick = (mode: string) => {
if (mode === 'all' || mode === 'include' || mode === 'exclude') {
selectSkillsMode(mode) selectSkillsMode(mode)
}
} }
// 切换子下拉框 // 切换子下拉框
@@ -214,9 +239,10 @@ const handleClickOutside = (e: MouseEvent) => {
} }
} }
// 页面加载时获取 skills // 页面加载时获取 skills 和 agents
onMounted(() => { onMounted(() => {
fetchSkills() fetchSkills()
fetchAgents()
document.addEventListener('click', handleClickOutside) document.addEventListener('click', handleClickOutside)
}) })
@@ -246,13 +272,35 @@ const createAgent = async () => {
isCreating.value = true isCreating.value = true
try { try {
// 调用后端 API 创建智能体
const response = await fetch('http://localhost:8082/api/agent/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: newAgent.value.name,
description: newAgent.value.description,
avatar: newAgent.value.avatar,
skills_mode: newAgent.value.skillsMode,
skills: newAgent.value.selectedSkills,
knowledge: newAgent.value.knowledge,
prompt: newAgent.value.prompt,
}),
})
if (!response.ok) {
throw new Error(`Failed to create agent: ${response.status}`)
}
const result = await response.json()
// 处理 skills 标签 // 处理 skills 标签
const skillsLabels = newAgent.value.selectedSkills.map((id: string) => getSkillLabel(id)).join(', ') const skillsLabels = newAgent.value.selectedSkills.map((id: string) => getSkillLabel(id)).join(', ')
// 模拟创建 // 添加到列表
const newId = Math.max(...agents.value.map(a => a.id)) + 1
agents.value.unshift({ agents.value.unshift({
id: newId, id: result.agent_id,
name: newAgent.value.name, name: newAgent.value.name,
avatar: newAgent.value.avatar, avatar: newAgent.value.avatar,
description: newAgent.value.description, description: newAgent.value.description,
@@ -264,7 +312,12 @@ const createAgent = async () => {
mcpServers: 0, mcpServers: 0,
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
}) })
showCreateModal.value = false showCreateModal.value = false
ElMessage.success('Agent created successfully')
} catch (error) {
console.error('Failed to create agent:', error)
ElMessage.error('Failed to create agent')
} finally { } finally {
isCreating.value = false isCreating.value = false
} }
@@ -345,7 +398,7 @@ const deleteAgent = (id: number) => {
<!-- Agents 列表 --> <!-- Agents 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden"> <div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full"> <table v-if="filteredAgents.length > 0" class="w-full">
<thead class="bg-dark-600"> <thead class="bg-dark-600">
<tr> <tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th> <th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th>
@@ -416,9 +469,12 @@ const deleteAgent = (id: number) => {
</table> </table>
<!-- 空状态 --> <!-- 空状态 -->
<div v-if="filteredAgents.length === 0" class="py-12 text-center text-gray-500"> <div v-if="filteredAgents.length === 0" class="empty-box">
<i class="fa-solid fa-robot text-4xl mb-3"></i> <div class="empty-icon">
<p>No agents found</p> <i class="fa-solid fa-robot"></i>
</div>
<p class="empty-text">No agents found</p>
<p class="empty-tip">Click "New Agent" to create one</p>
</div> </div>
</div> </div>
</div> </div>
@@ -506,7 +562,7 @@ const deleteAgent = (id: number) => {
:key="option.value" :key="option.value"
class="skills-mode-item" class="skills-mode-item"
:class="{ 'active': newAgent.skillsMode === option.value }" :class="{ 'active': newAgent.skillsMode === option.value }"
@click="handleSkillsModeClick(option.value as 'all' | 'include' | 'exclude')" @click="handleSkillsModeClick(option.value)"
> >
<div class="mode-radio"> <div class="mode-radio">
<div v-if="newAgent.skillsMode === option.value" class="radio-dot"></div> <div v-if="newAgent.skillsMode === option.value" class="radio-dot"></div>
@@ -655,7 +711,7 @@ const deleteAgent = (id: number) => {
/* Skills Mode Options */ /* Skills Mode Options */
.skills-mode-options { .skills-mode-options {
padding: 8px; padding: 8px;
border-bottom: 1px solid #4b5563; border-bottom: 1px solid #2a2c36;
} }
.skills-mode-item { .skills-mode-item {
@@ -669,7 +725,7 @@ const deleteAgent = (id: number) => {
} }
.skills-mode-item:hover { .skills-mode-item:hover {
background: #374151; background: #1a1c25;
} }
.skills-mode-item.active { .skills-mode-item.active {
@@ -734,8 +790,8 @@ const deleteAgent = (id: number) => {
left: 100%; left: 100%;
margin-left: 8px; margin-left: 8px;
width: 280px; width: 280px;
background: #374151; background: #171922;
border: 1px solid #4b5563; border: 1px solid #2a2c36;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
z-index: 100; z-index: 100;
@@ -747,8 +803,8 @@ const deleteAgent = (id: number) => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 16px; padding: 12px 16px;
border-bottom: 1px solid #4b5563; border-bottom: 1px solid #2a2c36;
background: #4b5563; background: #1a1c25;
} }
.sub-dropdown-title { .sub-dropdown-title {
@@ -794,8 +850,8 @@ const deleteAgent = (id: number) => {
justify-content: space-between; justify-content: space-between;
min-height: 44px; min-height: 44px;
padding: 0 12px; padding: 0 12px;
background: #374151; background: #171922;
border: 1px solid #4b5563; border: 1px solid #2a2c36;
border-radius: 8px; border-radius: 8px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
@@ -856,8 +912,8 @@ const deleteAgent = (id: number) => {
top: calc(100% + 8px); top: calc(100% + 8px);
left: 0; left: 0;
right: 0; right: 0;
background: #374151; background: #171922;
border: 1px solid #4b5563; border: 1px solid #2a2c36;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
z-index: 100; z-index: 100;
@@ -868,7 +924,7 @@ const deleteAgent = (id: number) => {
.search-box { .search-box {
position: relative; position: relative;
padding: 12px; padding: 12px;
border-bottom: 1px solid #4b5563; border-bottom: 1px solid #2a2c36;
} }
.search-icon { .search-icon {
@@ -957,7 +1013,7 @@ const deleteAgent = (id: number) => {
} }
.options-list::-webkit-scrollbar-track { .options-list::-webkit-scrollbar-track {
background: #374151; background: #171922;
} }
.options-list::-webkit-scrollbar-thumb { .options-list::-webkit-scrollbar-thumb {
@@ -1029,4 +1085,42 @@ const deleteAgent = (id: number) => {
input[type="checkbox"]:indeterminate { input[type="checkbox"]:indeterminate {
accent-color: #f97316; accent-color: #f97316;
} }
/* Empty Box */
.empty-box {
min-height: 340px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-icon {
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1f2937, #111827);
border-radius: 24px;
margin-bottom: 20px;
}
.empty-icon i {
font-size: 40px;
color: #6b7280;
}
.empty-text {
color: #d1d5db;
font-size: 1.25rem;
font-weight: 500;
margin-bottom: 8px;
}
.empty-tip {
font-size: 14px;
color: #9ca3af;
margin: 0;
}
</style> </style>

View File

@@ -26,6 +26,7 @@ const {
openEdit, openEdit,
closeEdit, closeEdit,
goToEditStep2, goToEditStep2,
goBackToEditStep1,
saveEditStep2, saveEditStep2,
closeEditStep2, closeEditStep2,
toggleStatus, toggleStatus,
@@ -266,7 +267,7 @@ onMounted(() => {
</div> </div>
<div class="flex items-center justify-between gap-3 p-5 border-t border-dark-600 bg-dark-700/50"> <div class="flex items-center justify-between gap-3 p-5 border-t border-dark-600 bg-dark-700/50">
<button @click="closeEditStep2" class="btn-secondary">Back</button> <button @click="goBackToEditStep1" class="btn-secondary">Back</button>
<button @click="saveEditStep2" class="btn-primary">Save Changes</button> <button @click="saveEditStep2" class="btn-primary">Save Changes</button>
</div> </div>
</div> </div>

View File

@@ -348,12 +348,20 @@ Example 1:
} }
} }
// 关闭编辑第二步弹窗 // 返回编辑第一步
const closeEditStep2 = () => { const goBackToEditStep1 = () => {
isEditingStep2.value = false isEditingStep2.value = false
isEditing.value = true isEditing.value = true
} }
// 关闭编辑第二步弹窗(完全关闭)
const closeEditStep2 = () => {
isEditingStep2.value = false
isEditing.value = false
editingSkill.value = null
editSkillContent.value = ''
}
// 切换状态 // 切换状态
const toggleStatus = async (skill: Skill) => { const toggleStatus = async (skill: Skill) => {
const newStatus = skill.status === 'active' ? 'inactive' : 'active' const newStatus = skill.status === 'active' ? 'inactive' : 'active'
@@ -444,11 +452,14 @@ Example 1:
const file = input.files[0] const file = input.files[0]
importFile.value = file importFile.value = file
importFileName.value = file.name
// 获取纯文件名,去除路径
const fileName = file.name.split(/[/\\]/).pop()?.replace('.md', '') || 'untitled'
importFileName.value = fileName
try { try {
const content = await file.text() const content = await file.text()
const { skillName, skillDesc } = parseSkillContent(content, file.name.replace('.md', '')) const { skillName, skillDesc } = parseSkillContent(content, fileName)
importSkillName.value = skillName importSkillName.value = skillName
importSkillDesc.value = skillDesc importSkillDesc.value = skillDesc
@@ -577,6 +588,7 @@ Example 1:
openEdit, openEdit,
closeEdit, closeEdit,
goToEditStep2, goToEditStep2,
goBackToEditStep1,
saveEditStep2, saveEditStep2,
closeEditStep2, closeEditStep2,
toggleStatus, toggleStatus,