feat: add education-training and platform-engineering team templates
- Add 2 new team templates: 教育培训团队 (4 roles) and 天工平台工程团队 (5 roles) - Fix orchestrator to support multi-template workflow types (remove hardcoded PM planner) - Add _resolve_planner_role() to auto-detect planner based on team.config.workflow - Add frontend buttons and API clients for new templates - Merge 14 preset roles from 3 template families in get_preset_roles() - Add creation guide and platform engineering usage docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
299
backend/app/api/teams.py
Normal file
299
backend/app/api/teams.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Team API — 虚拟团队管理 + 项目执行
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
import logging
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.team_service import TeamService, get_preset_roles
|
||||
from app.services.team_orchestrator import TeamOrchestrator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/teams",
|
||||
tags=["teams"],
|
||||
responses={
|
||||
401: {"description": "未授权"},
|
||||
404: {"description": "资源不存在"},
|
||||
400: {"description": "请求参数错误"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# ──────────────────────────── Schemas ────────────────────────────
|
||||
|
||||
class TeamCreate(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
workspace_id: Optional[str] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class TeamUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
class MemberAdd(BaseModel):
|
||||
agent_id: str
|
||||
role: str
|
||||
position: int = 0
|
||||
is_lead: bool = False
|
||||
|
||||
|
||||
class ProjectExecute(BaseModel):
|
||||
project_description: str = Field(..., min_length=1, description="项目描述")
|
||||
auto_approve_files: bool = Field(default=True, description="是否自动批准文件写入(跳过确认弹窗)")
|
||||
|
||||
|
||||
# ──────────────────────────── Endpoints ────────────────────────────
|
||||
|
||||
@router.get("/preset-roles")
|
||||
def preset_roles():
|
||||
"""获取预置角色定义(PM/设计师/开发/测试/DevOps)。"""
|
||||
return {"roles": get_preset_roles()}
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_team(
|
||||
body: TeamCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""创建团队。"""
|
||||
svc = TeamService(db)
|
||||
team = svc.create_team(
|
||||
name=body.name,
|
||||
user_id=current_user.id,
|
||||
description=body.description,
|
||||
workspace_id=body.workspace_id,
|
||||
config=body.config,
|
||||
)
|
||||
return {"data": team.to_dict()}
|
||||
|
||||
|
||||
@router.post("/template/software-company")
|
||||
def create_software_company_template(
|
||||
workspace_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""一键创建「软件公司虚拟团队」模板。
|
||||
|
||||
自动创建 5 个角色 Agent(PM/设计师/开发/测试/DevOps)并组建团队。
|
||||
若用户已有同名 Agent 则复用。
|
||||
"""
|
||||
svc = TeamService(db)
|
||||
result = svc.create_software_company_template(
|
||||
user_id=current_user.id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@router.post("/template/education-training")
|
||||
def create_education_training_template(
|
||||
workspace_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""一键创建「教育培训团队」模板。
|
||||
|
||||
自动创建 4 个角色 Agent(课程设计/主讲讲师/作业助教/教务管理)并组建团队。
|
||||
若用户已有同名 Agent 则复用。
|
||||
"""
|
||||
svc = TeamService(db)
|
||||
result = svc.create_education_training_template(
|
||||
user_id=current_user.id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@router.post("/template/platform-engineering")
|
||||
def create_platform_engineering_template(
|
||||
workspace_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""一键创建「天工平台工程团队」模板。
|
||||
|
||||
自动创建 5 个角色 Agent(全栈开发/前端体验/平台运维/质量保障/产品负责人)并组建团队。
|
||||
专用于天工智能体平台的自我维护与迭代升级。
|
||||
若用户已有同名 Agent 则复用。
|
||||
"""
|
||||
svc = TeamService(db)
|
||||
result = svc.create_platform_engineering_template(
|
||||
user_id=current_user.id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_teams(
|
||||
workspace_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出当前用户/工作区的所有团队。"""
|
||||
svc = TeamService(db)
|
||||
teams = svc.list_teams(
|
||||
user_id=current_user.id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return {"data": [t.to_dict() for t in teams]}
|
||||
|
||||
|
||||
@router.get("/{team_id}")
|
||||
def get_team(
|
||||
team_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""获取团队详情(含成员列表)。"""
|
||||
svc = TeamService(db)
|
||||
result = svc.get_team_with_members(team_id)
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="团队不存在")
|
||||
return {"data": result}
|
||||
|
||||
|
||||
@router.put("/{team_id}")
|
||||
def update_team(
|
||||
team_id: str,
|
||||
body: TeamUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""更新团队信息。"""
|
||||
svc = TeamService(db)
|
||||
team = svc.update_team(team_id, **body.model_dump(exclude_none=True))
|
||||
if not team:
|
||||
raise HTTPException(status_code=404, detail="团队不存在")
|
||||
return {"data": team.to_dict()}
|
||||
|
||||
|
||||
@router.delete("/{team_id}")
|
||||
def delete_team(
|
||||
team_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""删除团队及其所有成员。"""
|
||||
svc = TeamService(db)
|
||||
ok = svc.delete_team(team_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="团队不存在")
|
||||
return {"message": "团队已删除"}
|
||||
|
||||
|
||||
@router.post("/{team_id}/members")
|
||||
def add_member(
|
||||
team_id: str,
|
||||
body: MemberAdd,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""为团队添加/替换角色成员。"""
|
||||
svc = TeamService(db)
|
||||
member = svc.add_member(
|
||||
team_id=team_id,
|
||||
agent_id=body.agent_id,
|
||||
role=body.role,
|
||||
position=body.position,
|
||||
is_lead=body.is_lead,
|
||||
)
|
||||
if not member:
|
||||
raise HTTPException(status_code=404, detail="团队不存在")
|
||||
return {"data": member.to_dict()}
|
||||
|
||||
|
||||
@router.delete("/{team_id}/members/{member_id}")
|
||||
def remove_member(
|
||||
team_id: str,
|
||||
member_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""从团队移除成员。"""
|
||||
svc = TeamService(db)
|
||||
ok = svc.remove_member(team_id, member_id)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="成员不存在")
|
||||
return {"message": "成员已移除"}
|
||||
|
||||
|
||||
@router.get("/{team_id}/members")
|
||||
def list_members(
|
||||
team_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出团队成员。"""
|
||||
svc = TeamService(db)
|
||||
members = svc.get_members(team_id)
|
||||
return {"data": [m.to_dict() for m in members]}
|
||||
|
||||
|
||||
@router.post("/{team_id}/execute")
|
||||
async def execute_project(
|
||||
team_id: str,
|
||||
body: ProjectExecute,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""执行团队项目(非流式)。
|
||||
|
||||
PM 先分解计划,各角色按阶段顺序执行,QA 最终审查。
|
||||
"""
|
||||
orch = TeamOrchestrator(db, team_id, current_user.id)
|
||||
try:
|
||||
result = await orch.execute(body.project_description, auto_approve_files=body.auto_approve_files)
|
||||
return {"data": result}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error("团队执行失败: %s", e, exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f"团队执行失败: {e}")
|
||||
|
||||
|
||||
@router.post("/{team_id}/execute/stream")
|
||||
async def execute_project_stream(
|
||||
team_id: str,
|
||||
body: ProjectExecute,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""执行团队项目(SSE 流式)。"""
|
||||
|
||||
async def event_generator():
|
||||
orch = TeamOrchestrator(db, team_id, current_user.id)
|
||||
try:
|
||||
async for event in orch.execute_stream(body.project_description, auto_approve_files=body.auto_approve_files):
|
||||
yield f"data: {json.dumps(event, ensure_ascii=False)}\n\n"
|
||||
except ValueError as e:
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
|
||||
except Exception as e:
|
||||
logger.error("团队流式执行失败: %s", e, exc_info=True)
|
||||
yield f"data: {json.dumps({'type': 'error', 'content': str(e)}, ensure_ascii=False)}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
638
backend/app/services/team_orchestrator.py
Normal file
638
backend/app/services/team_orchestrator.py
Normal file
@@ -0,0 +1,638 @@
|
||||
"""
|
||||
团队编排引擎 — PM 规划 → 顺序阶段执行 → QA 审查 → 交付物
|
||||
|
||||
用法:
|
||||
orchestrator = TeamOrchestrator(db, team_id, user_id)
|
||||
result = await orchestrator.execute("做一个个人博客")
|
||||
async for event in orchestrator.execute_stream("做一个个人博客"):
|
||||
...
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from app.models.team import Team, TeamMember
|
||||
from app.models.agent import Agent
|
||||
from app.agent_runtime.core import AgentRuntime
|
||||
from app.agent_runtime.schemas import (
|
||||
AgentConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentMemoryConfig,
|
||||
AgentResult,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_agent_config(agent: Agent, auto_approve_files: bool = True) -> AgentConfig:
|
||||
"""从 Agent 的 workflow_config 构建 AgentConfig,供 AgentRuntime 使用。"""
|
||||
wf = agent.workflow_config or {}
|
||||
nodes = wf.get("nodes", [])
|
||||
llm_node = None
|
||||
for n in nodes:
|
||||
if n.get("type") == "llm":
|
||||
llm_node = n
|
||||
break
|
||||
|
||||
llm_data = llm_node.get("data", {}) if llm_node else {}
|
||||
|
||||
tools = llm_data.get("selected_tools") or llm_data.get("tools") or []
|
||||
system_prompt = llm_data.get("prompt") or ""
|
||||
model = llm_data.get("model", "deepseek-v4-pro")
|
||||
provider = llm_data.get("provider", "deepseek")
|
||||
temperature = float(llm_data.get("temperature", 0.4))
|
||||
max_iterations = int(llm_data.get("max_iterations", 10))
|
||||
|
||||
perm = "acceptEdits" if auto_approve_files else "default"
|
||||
logger.info("_build_agent_config: agent=%s tools=%d permission_level=%s", agent.name, len(tools), perm)
|
||||
return AgentConfig(
|
||||
name=agent.name,
|
||||
system_prompt=system_prompt,
|
||||
user_id=agent.user_id,
|
||||
memory_scope_id=f"team_{agent.id}",
|
||||
llm=AgentLLMConfig(
|
||||
provider=provider,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_iterations=max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(include_tools=tools, permission_level=perm),
|
||||
memory=AgentMemoryConfig(
|
||||
enabled=True,
|
||||
max_history_messages=30,
|
||||
persist_to_db=True,
|
||||
learning_enabled=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _parse_plan_json(raw_output: str) -> Dict[str, Any]:
|
||||
"""从 LLM 原始输出中提取 JSON 计划。"""
|
||||
text = raw_output.strip()
|
||||
# 尝试直接解析
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# 从 ```json ... ``` 中提取
|
||||
import re
|
||||
m = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
|
||||
if m:
|
||||
try:
|
||||
return json.loads(m.group(1))
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# 从 { ... } 中提取
|
||||
m = re.search(r'\{[\s\S]*"phases"[\s\S]*\}', text)
|
||||
if m:
|
||||
try:
|
||||
return json.loads(m.group())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.warning("无法从输出中解析 JSON 计划,原始输出: %.500s", text)
|
||||
return {}
|
||||
|
||||
|
||||
def _resolve_workspace_root() -> Path:
|
||||
"""获取文件工具的工作区根目录(与 builtin_tools.py 逻辑一致)。"""
|
||||
from app.core.config import settings
|
||||
raw = (getattr(settings, "LOCAL_FILE_TOOLS_ROOT", None) or "").strip()
|
||||
if raw:
|
||||
return Path(raw).expanduser().resolve()
|
||||
# fallback: 4 级父目录 (builtin_tools.py → app/services → app → backend → repo root)
|
||||
return Path(__file__).resolve().parent.parent.parent.parent
|
||||
|
||||
|
||||
def _safe_project_name(description: str) -> str:
|
||||
"""从项目描述中提取安全的目录名(英文/拼音/数字/连字符)。"""
|
||||
# 取前 40 个字符,只保留英文、数字、中文转拼音首字母的近似
|
||||
cleaned = description.strip().replace(" ", "-").replace("/", "-").replace("\\", "-")
|
||||
# 去掉非 ASCII 和非基本标点
|
||||
safe = re.sub(r'[^\w\-]', '', cleaned, flags=re.ASCII)
|
||||
if not safe:
|
||||
# 如果全是中文,用时间戳
|
||||
from datetime import datetime
|
||||
safe = f"project-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||||
return safe[:40].strip("-")
|
||||
|
||||
|
||||
def _scan_directory_files(directory: Path) -> List[str]:
|
||||
"""扫描目录下所有文件的绝对路径列表(递归)。"""
|
||||
files: List[str] = []
|
||||
if not directory.exists():
|
||||
return files
|
||||
for p in directory.rglob("*"):
|
||||
if p.is_file():
|
||||
files.append(str(p))
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def _extract_written_files(result: AgentResult) -> List[str]:
|
||||
"""从 AgentResult.steps 中提取 file_write 成功产出的绝对路径列表。"""
|
||||
files: List[str] = []
|
||||
seen = set()
|
||||
for step in result.steps:
|
||||
if step.tool_name == "file_write":
|
||||
# 优先从 tool_result JSON 中取绝对路径(仅当写入成功时)
|
||||
if step.tool_result:
|
||||
try:
|
||||
data = json.loads(step.tool_result)
|
||||
# 跳过失败的调用(权限不足 / 需确认)
|
||||
if data.get("error") or data.get("requires_confirmation"):
|
||||
logger.warning("file_write 未成功(%s),跳过文件追踪", data.get("error", "requires_confirmation"))
|
||||
continue
|
||||
fp = data.get("file_path")
|
||||
if fp and fp not in seen:
|
||||
files.append(fp)
|
||||
seen.add(fp)
|
||||
continue
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
# 回退:从 tool_input 取相对路径,再拼接工作区根(仅当 tool_result 为空时)
|
||||
if step.tool_input:
|
||||
fp = step.tool_input.get("file_path")
|
||||
if fp:
|
||||
abs_path = str(_resolve_workspace_root() / fp)
|
||||
if abs_path not in seen:
|
||||
files.append(abs_path)
|
||||
seen.add(abs_path)
|
||||
return files
|
||||
|
||||
|
||||
class TeamOrchestrator:
|
||||
"""虚拟团队编排器 — 按角色分工顺序执行软件项目。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
db: Session,
|
||||
team_id: str,
|
||||
user_id: str,
|
||||
auto_approve_files: bool = True,
|
||||
):
|
||||
self.db = db
|
||||
self.team_id = team_id
|
||||
self.user_id = user_id
|
||||
self.auto_approve_files = auto_approve_files
|
||||
|
||||
def _load_team(self) -> Team:
|
||||
team = self.db.query(Team).filter(Team.id == self.team_id).first()
|
||||
if not team:
|
||||
raise ValueError(f"团队不存在: {self.team_id}")
|
||||
return team
|
||||
|
||||
def _load_members(self) -> List[TeamMember]:
|
||||
return (
|
||||
self.db.query(TeamMember)
|
||||
.filter(TeamMember.team_id == self.team_id)
|
||||
.order_by(TeamMember.position)
|
||||
.all()
|
||||
)
|
||||
|
||||
def _get_agent_by_role(self, members: List[TeamMember], role: str) -> Optional[Agent]:
|
||||
"""按角色查找 Agent。"""
|
||||
for m in members:
|
||||
if m.role == role:
|
||||
return m.agent
|
||||
return None
|
||||
|
||||
def _resolve_planner_role(self, members: List[TeamMember], workflow: str) -> str:
|
||||
"""根据团队 workflow 类型解析规划者角色。
|
||||
|
||||
- software_company → pm
|
||||
- education_training → curriculum_designer
|
||||
- platform_engineering → product_lead (产品负责人做规划)
|
||||
- 回退: 第一个 is_lead=True 的成员
|
||||
- 最终回退: 第一个成员
|
||||
"""
|
||||
workflow_planner_map = {
|
||||
"software_company": "pm",
|
||||
"education_training": "curriculum_designer",
|
||||
"platform_engineering": "product_lead",
|
||||
}
|
||||
role = workflow_planner_map.get(workflow)
|
||||
if role and self._get_agent_by_role(members, role):
|
||||
return role
|
||||
|
||||
# 回退:找第一个 Leader
|
||||
for m in sorted(members, key=lambda x: x.position):
|
||||
if m.is_lead:
|
||||
return m.role
|
||||
|
||||
# 最终回退:第一个成员
|
||||
if members:
|
||||
return members[0].role
|
||||
return "pm"
|
||||
|
||||
async def execute(self, project_description: str, auto_approve_files: bool = True) -> Dict[str, Any]:
|
||||
"""执行团队项目(非流式)。
|
||||
|
||||
Returns:
|
||||
{
|
||||
"project_name": str,
|
||||
"plan": {...},
|
||||
"phases": [{phase, role, agent_name, output, steps, success, error}],
|
||||
"qa_review": {...} | None,
|
||||
"final_deliverable": str,
|
||||
"success": bool,
|
||||
}
|
||||
"""
|
||||
self.auto_approve_files = auto_approve_files
|
||||
team = self._load_team()
|
||||
members = self._load_members()
|
||||
if not members:
|
||||
raise ValueError("团队没有成员,请先为角色分配 Agent")
|
||||
|
||||
# ─── 创建项目目录 ───
|
||||
workspace_root = _resolve_workspace_root()
|
||||
safe_name = _safe_project_name(project_description)
|
||||
project_path = workspace_root / "team_projects" / self.team_id / safe_name
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
logger.info("团队编排 [%s]: 项目目录 %s", self.team_id, project_path)
|
||||
|
||||
prev_files = set(_scan_directory_files(project_path))
|
||||
all_files: List[str] = []
|
||||
|
||||
results: Dict[str, Any] = {
|
||||
"team_id": self.team_id,
|
||||
"team_name": team.name,
|
||||
"project_description": project_description,
|
||||
"project_path": str(project_path),
|
||||
"files": [],
|
||||
"started_at": datetime.now().isoformat(),
|
||||
"plan": None,
|
||||
"phases": [],
|
||||
"qa_review": None,
|
||||
"final_deliverable": "",
|
||||
"success": True,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
# ─── Phase 0: 规划阶段(由 Leader Agent 担任) ───
|
||||
# 选择规划者:优先用 team.config 中指定的 workflow,否则回退兼容
|
||||
workflow = (team.config or {}).get("workflow", "software_company")
|
||||
planner_role = self._resolve_planner_role(members, workflow)
|
||||
planner_agent = self._get_agent_by_role(members, planner_role)
|
||||
if not planner_agent:
|
||||
raise ValueError(
|
||||
f"团队缺少 Leader 角色(需要 role='{planner_role}'),无法分解项目。"
|
||||
f"可用角色: {[m.role for m in members]}"
|
||||
)
|
||||
|
||||
# 动态构建可用的角色列表(来自团队成员实际角色)
|
||||
available_roles = [m.role for m in members]
|
||||
roles_hint = "/".join(available_roles)
|
||||
|
||||
logger.info("团队编排 [%s]: 规划开始 (workflow=%s, planner=%s, role=%s)",
|
||||
self.team_id, workflow, planner_agent.name, planner_role)
|
||||
|
||||
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
|
||||
planner_runtime = AgentRuntime(planner_config)
|
||||
planner_input = (
|
||||
f"请分析以下需求,按 JSON 格式输出执行计划:\n\n"
|
||||
f"需求描述:{project_description}\n"
|
||||
f"输出文件目录:{project_path}\n\n"
|
||||
f"你的团队有以下角色(每个 phase 必须使用这些角色之一):{roles_hint}\n"
|
||||
f"请输出包含 phases 数组的完整 JSON 计划,每个 phase 需指定 role 为上述角色之一。"
|
||||
)
|
||||
planner_result = await planner_runtime.run(planner_input)
|
||||
plan = _parse_plan_json(planner_result.content) if planner_result.success else {}
|
||||
results["plan"] = plan
|
||||
|
||||
if not plan or not plan.get("phases"):
|
||||
results["success"] = False
|
||||
results["error"] = f"{planner_role} 未能生成有效计划"
|
||||
results["final_deliverable"] = planner_result.content
|
||||
return results
|
||||
|
||||
logger.info("团队编排 [%s]: PM 生成 %d 个阶段", self.team_id, len(plan["phases"]))
|
||||
|
||||
# ─── Phase 1..N: 顺序执行 ───
|
||||
all_outputs: List[str] = []
|
||||
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
|
||||
|
||||
for i, phase in enumerate(plan["phases"]):
|
||||
phase_num = phase.get("phase", i + 1)
|
||||
role = phase.get("role", "developer")
|
||||
phase_name = phase.get("name", f"阶段 {phase_num}")
|
||||
phase_desc = phase.get("description", "")
|
||||
expected = phase.get("expected_output", "")
|
||||
|
||||
agent = self._get_agent_by_role(members, role)
|
||||
if not agent:
|
||||
logger.warning("团队编排 [%s]: 阶段 %s 缺少角色 '%s',跳过", self.team_id, phase_num, role)
|
||||
all_outputs.append(f"[跳过] 阶段 {phase_num} ({phase_name}): 无 {role} 角色")
|
||||
results["phases"].append({
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent_name": None,
|
||||
"output": f"跳过: 无 {role} 角色",
|
||||
"success": False,
|
||||
"error": f"missing_role:{role}",
|
||||
"files": [],
|
||||
})
|
||||
continue
|
||||
|
||||
logger.info("团队编排 [%s]: 执行阶段 %s — %s (角色: %s, Agent: %s)",
|
||||
self.team_id, phase_num, phase_name, role, agent.name)
|
||||
|
||||
try:
|
||||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||||
runtime = AgentRuntime(agent_config)
|
||||
|
||||
phase_input = (
|
||||
f"{context_doc}\n"
|
||||
f"## 当前阶段: {phase_name}\n"
|
||||
f"任务描述: {phase_desc}\n"
|
||||
f"期望产出: {expected}\n"
|
||||
f"项目文件保存目录: {project_path}\n"
|
||||
f"请将所有产出文件写入此目录,完成此阶段的工作并输出你的交付物。"
|
||||
)
|
||||
phase_result = await runtime.run(phase_input)
|
||||
|
||||
output_text = phase_result.content if phase_result.success else (
|
||||
phase_result.error or "执行失败"
|
||||
)
|
||||
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:2000]}\n"
|
||||
|
||||
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
|
||||
|
||||
# 提取本阶段写入的文件(从 AgentResult + 目录扫描双保险)
|
||||
extracted = _extract_written_files(phase_result)
|
||||
current_files = set(_scan_directory_files(project_path))
|
||||
phase_files = sorted(current_files - prev_files)
|
||||
# 同时保留从 AgentResult 提取的文件(可能包含项目目录外的文件)
|
||||
for fp in extracted:
|
||||
if fp not in phase_files:
|
||||
phase_files.append(fp)
|
||||
prev_files = current_files
|
||||
all_files.extend(phase_files)
|
||||
|
||||
results["phases"].append({
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent_name": agent.name,
|
||||
"output": output_text,
|
||||
"success": phase_result.success,
|
||||
"iterations": phase_result.iterations_used,
|
||||
"tool_calls": phase_result.tool_calls_made,
|
||||
"error": phase_result.error if not phase_result.success else None,
|
||||
"files": phase_files,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error("团队编排 [%s]: 阶段 %s 异常: %s", self.team_id, phase_num, e)
|
||||
results["phases"].append({
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent_name": agent.name if agent else None,
|
||||
"output": f"执行异常: {e}",
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"files": [],
|
||||
})
|
||||
results["success"] = False
|
||||
|
||||
# ─── QA 审查 ───
|
||||
qa_agent = self._get_agent_by_role(members, "qa")
|
||||
if qa_agent and all_outputs:
|
||||
logger.info("团队编排 [%s]: QA 审查开始", self.team_id)
|
||||
try:
|
||||
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
|
||||
qa_runtime = AgentRuntime(qa_config)
|
||||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||||
qa_input = (
|
||||
f"请审查以下项目交付物:\n\n"
|
||||
f"项目: {project_description}\n\n"
|
||||
f"{deliverables[:8000]}\n\n"
|
||||
f"请按 JSON 格式输出审查结果,包含 pass/score/issues/overall_assessment。"
|
||||
)
|
||||
qa_result = await qa_runtime.run(qa_input)
|
||||
qa_review = _parse_plan_json(qa_result.content) if qa_result.success else {}
|
||||
results["qa_review"] = {
|
||||
"output": qa_result.content,
|
||||
"review": qa_review,
|
||||
"success": qa_result.success,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("团队编排 [%s]: QA 审查异常: %s", self.team_id, e)
|
||||
results["qa_review"] = {"error": str(e)}
|
||||
|
||||
# ─── 汇总最终交付物 ───
|
||||
results["final_deliverable"] = "\n\n---\n\n".join(all_outputs)
|
||||
# 最终文件列表以目录扫描为准,合并从 AgentResult 提取的
|
||||
final_scan = set(_scan_directory_files(project_path))
|
||||
for fp in all_files:
|
||||
final_scan.add(fp)
|
||||
results["files"] = sorted(final_scan)
|
||||
results["completed_at"] = datetime.now().isoformat()
|
||||
return results
|
||||
|
||||
async def execute_stream(self, project_description: str, auto_approve_files: bool = True) -> AsyncGenerator[Dict[str, Any], None]:
|
||||
"""流式执行团队项目,yield SSE 事件。
|
||||
|
||||
事件类型:
|
||||
- plan_start / plan / plan_done — PM 规划阶段
|
||||
- phase_start / phase_output / phase_done — 各执行阶段
|
||||
- qa_start / qa_review / qa_done — QA 审查
|
||||
- final — 全部完成
|
||||
- error — 错误
|
||||
"""
|
||||
self.auto_approve_files = auto_approve_files
|
||||
try:
|
||||
team = self._load_team()
|
||||
members = self._load_members()
|
||||
except Exception as e:
|
||||
yield {"type": "error", "content": str(e)}
|
||||
return
|
||||
|
||||
if not members:
|
||||
yield {"type": "error", "content": "团队没有成员"}
|
||||
return
|
||||
|
||||
# ─── 创建项目目录 ───
|
||||
workspace_root = _resolve_workspace_root()
|
||||
safe_name = _safe_project_name(project_description)
|
||||
project_path = workspace_root / "team_projects" / self.team_id / safe_name
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
prev_files = set(_scan_directory_files(project_path))
|
||||
all_files: List[str] = []
|
||||
|
||||
yield {
|
||||
"type": "plan_start",
|
||||
"team_name": team.name,
|
||||
"member_count": len(members),
|
||||
"project_path": str(project_path),
|
||||
}
|
||||
|
||||
all_outputs: List[str] = []
|
||||
|
||||
# ─── 规划阶段(由 Leader Agent 担任) ───
|
||||
workflow = (team.config or {}).get("workflow", "software_company")
|
||||
planner_role = self._resolve_planner_role(members, workflow)
|
||||
planner_agent = self._get_agent_by_role(members, planner_role)
|
||||
if not planner_agent:
|
||||
yield {"type": "error", "content": f"团队缺少 Leader 角色(需要 role='{planner_role}'),可用角色: {[m.role for m in members]}"}
|
||||
return
|
||||
|
||||
try:
|
||||
available_roles = [m.role for m in members]
|
||||
roles_hint = "/".join(available_roles)
|
||||
|
||||
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
|
||||
planner_runtime = AgentRuntime(planner_config)
|
||||
planner_input = (
|
||||
f"请分析以下需求,按 JSON 格式输出执行计划:\n\n"
|
||||
f"需求描述:{project_description}\n"
|
||||
f"输出文件目录:{project_path}\n"
|
||||
f"你的团队有以下角色(每个 phase 必须使用这些角色之一):{roles_hint}\n"
|
||||
f"请输出包含 phases 数组的完整 JSON 计划,每个 phase 需指定 role 为上述角色之一。"
|
||||
)
|
||||
|
||||
yield {"type": "plan", "status": "running", "agent": planner_agent.name}
|
||||
planner_result = await planner_runtime.run(planner_input)
|
||||
plan = _parse_plan_json(planner_result.content) if planner_result.success else {}
|
||||
|
||||
yield {
|
||||
"type": "plan_done",
|
||||
"plan": plan,
|
||||
"raw_output": planner_result.content[:2000],
|
||||
"success": planner_result.success,
|
||||
}
|
||||
except Exception as e:
|
||||
yield {"type": "error", "content": f"{planner_role} 规划失败: {e}"}
|
||||
return
|
||||
|
||||
if not plan or not plan.get("phases"):
|
||||
yield {"type": "error", "content": f"{planner_role} 未能生成有效计划"}
|
||||
return
|
||||
|
||||
phases = plan["phases"]
|
||||
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
|
||||
|
||||
# ─── 顺序执行各阶段 ───
|
||||
for i, phase in enumerate(phases):
|
||||
phase_num = phase.get("phase", i + 1)
|
||||
role = phase.get("role", "developer")
|
||||
phase_name = phase.get("name", f"阶段 {phase_num}")
|
||||
phase_desc = phase.get("description", "")
|
||||
expected = phase.get("expected_output", "")
|
||||
|
||||
agent = self._get_agent_by_role(members, role)
|
||||
if not agent:
|
||||
msg = f"阶段 {phase_num} ({phase_name}) 跳过: 无 {role} 角色"
|
||||
yield {"type": "phase_done", "phase": phase_num, "name": phase_name,
|
||||
"role": role, "success": False, "error": f"missing_role:{role}"}
|
||||
all_outputs.append(f"[跳过] {msg}")
|
||||
continue
|
||||
|
||||
yield {
|
||||
"type": "phase_start",
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent": agent.name,
|
||||
}
|
||||
|
||||
try:
|
||||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||||
runtime = AgentRuntime(agent_config)
|
||||
|
||||
phase_input = (
|
||||
f"{context_doc}\n"
|
||||
f"## 当前阶段: {phase_name}\n"
|
||||
f"任务描述: {phase_desc}\n"
|
||||
f"期望产出: {expected}\n"
|
||||
f"项目文件保存目录: {project_path}\n"
|
||||
f"请将所有产出文件写入此目录,完成此阶段的工作并输出你的交付物。"
|
||||
)
|
||||
phase_result = await runtime.run(phase_input)
|
||||
|
||||
output_text = phase_result.content if phase_result.success else (
|
||||
phase_result.error or "执行失败"
|
||||
)
|
||||
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:2000]}\n"
|
||||
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
|
||||
|
||||
# 提取本阶段写入的文件(从 AgentResult + 目录扫描双保险)
|
||||
extracted = _extract_written_files(phase_result)
|
||||
current_files = set(_scan_directory_files(project_path))
|
||||
phase_files = sorted(current_files - prev_files)
|
||||
for fp in extracted:
|
||||
if fp not in phase_files:
|
||||
phase_files.append(fp)
|
||||
prev_files = current_files
|
||||
all_files.extend(phase_files)
|
||||
|
||||
yield {
|
||||
"type": "phase_done",
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent": agent.name,
|
||||
"output": output_text,
|
||||
"success": phase_result.success,
|
||||
"iterations": phase_result.iterations_used,
|
||||
"tool_calls": phase_result.tool_calls_made,
|
||||
"error": phase_result.error if not phase_result.success else None,
|
||||
"files": phase_files,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("团队编排: 阶段 %s 异常: %s", phase_num, e)
|
||||
yield {
|
||||
"type": "phase_done",
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"agent": agent.name,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
# ─── QA 审查 ───
|
||||
qa_agent = self._get_agent_by_role(members, "qa")
|
||||
if qa_agent and all_outputs:
|
||||
yield {"type": "qa_start", "agent": qa_agent.name}
|
||||
try:
|
||||
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
|
||||
qa_runtime = AgentRuntime(qa_config)
|
||||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||||
qa_input = (
|
||||
f"请审查以下项目交付物:\n\n"
|
||||
f"项目: {project_description}\n\n"
|
||||
f"{deliverables[:8000]}\n\n"
|
||||
f"请按 JSON 格式输出审查结果,包含 pass/score/issues/overall_assessment。"
|
||||
)
|
||||
qa_result = await qa_runtime.run(qa_input)
|
||||
qa_review_data = _parse_plan_json(qa_result.content) if qa_result.success else {}
|
||||
yield {
|
||||
"type": "qa_done",
|
||||
"output": qa_result.content,
|
||||
"review": qa_review_data,
|
||||
"success": qa_result.success,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error("团队编排: QA 审查异常: %s", e)
|
||||
yield {"type": "qa_done", "error": str(e)}
|
||||
|
||||
# ─── 最终交付物 ───
|
||||
final = "\n\n---\n\n".join(all_outputs)
|
||||
final_scan = set(_scan_directory_files(project_path))
|
||||
for fp in all_files:
|
||||
final_scan.add(fp)
|
||||
yield {
|
||||
"type": "final",
|
||||
"deliverable": final,
|
||||
"phase_count": len(phases),
|
||||
"team_name": team.name,
|
||||
"project_path": str(project_path),
|
||||
"files": sorted(final_scan),
|
||||
}
|
||||
1100
backend/app/services/team_service.py
Normal file
1100
backend/app/services/team_service.py
Normal file
File diff suppressed because it is too large
Load Diff
321
docs/agent-guides/创建团队模板.md
Normal file
321
docs/agent-guides/创建团队模板.md
Normal file
@@ -0,0 +1,321 @@
|
||||
# 创建虚拟团队模板指南
|
||||
|
||||
> 本文档记录如何为新业务场景创建虚拟团队模板,基于已有 3 个模板的实现经验总结。
|
||||
|
||||
---
|
||||
|
||||
## 一、模板系统架构
|
||||
|
||||
### 1.1 什么是团队模板
|
||||
|
||||
团队模板是一键创建的虚拟团队,包含预定义角色 + 专属 Agent(含系统提示词),用户点击按钮即可获得一个可执行项目的完整团队。
|
||||
|
||||
### 1.2 现有模板
|
||||
|
||||
| # | 模板 | 角色数 | 适用场景 |
|
||||
|---|------|--------|----------|
|
||||
| 1 | 软件公司虚拟团队 | 5 | 新项目从零开发 |
|
||||
| 2 | 教育培训团队 | 4 | 在线教育课程建设 |
|
||||
| 3 | 天工平台工程团队 | 5 | 已有平台持续迭代运维 |
|
||||
|
||||
### 1.3 涉及文件
|
||||
|
||||
创建新模板需要修改 **4 个文件**:
|
||||
|
||||
| 层级 | 文件路径 | 改动内容 |
|
||||
|------|----------|----------|
|
||||
| 后端服务 | `backend/app/services/team_service.py` | 角色定义 + 系统提示词 + 工厂方法 |
|
||||
| 后端 API | `backend/app/api/teams.py` | 新增路由端点 |
|
||||
| 前端 API | `frontend/src/api/teams.ts` | 新增请求函数 |
|
||||
| 前端页面 | `frontend/src/views/TeamBuilder.vue` | 新增按钮 + 事件处理 |
|
||||
|
||||
---
|
||||
|
||||
## 二、创建步骤(以教育培训团队为例)
|
||||
|
||||
### 步骤 1:设计角色体系
|
||||
|
||||
先确定角色数量(建议 4-5 个)、每个角色的职责边界和产出物。
|
||||
|
||||
**示例 — 教育培训团队:**
|
||||
- 课程设计师:课程体系、学习目标、评估方法
|
||||
- 主讲讲师:课件、讲授方案、互动设计
|
||||
- 作业助教:批改、辅导、进度跟踪
|
||||
- 教务管理:排课、学员管理、证书
|
||||
|
||||
### 步骤 2:编写系统提示词
|
||||
|
||||
在 `team_service.py` 中为每个角色编写专属提示词。遵循以下规范:
|
||||
|
||||
```python
|
||||
ROLE_NAME_PROMPT = """You are a [Role Name] for [context]. Your responsibilities:
|
||||
1. [职责 1]
|
||||
2. [职责 2]
|
||||
...
|
||||
|
||||
When you receive a task:
|
||||
- [行为规范 1]
|
||||
- [行为规范 2]
|
||||
|
||||
Output format — include [期望输出格式说明]:
|
||||
{
|
||||
"key": "value"
|
||||
}"""
|
||||
```
|
||||
|
||||
**提示词编写要点:**
|
||||
- 用英文写角色定义(与 LLM 训练数据对齐),中文写具体场景说明
|
||||
- 明确输出格式(JSON schema 或 Markdown 结构)
|
||||
- 指定工具使用偏好(什么时候用 `file_write`,什么时候用 `web_search`)
|
||||
- 给出 3-5 条具体行为规范,避免模糊描述
|
||||
- 提示词长度控制在 30-60 行
|
||||
|
||||
### 步骤 3:定义角色元数据
|
||||
|
||||
在 `PRESET_ROLES` 同级位置添加新角色字典:
|
||||
|
||||
```python
|
||||
EDUCATION_ROLES = {
|
||||
"curriculum_designer": {
|
||||
"label": "课程设计师",
|
||||
"icon": "📐",
|
||||
"description": "设计课程体系、学习目标、知识模块、评估方法",
|
||||
},
|
||||
"instructor": {
|
||||
"label": "主讲讲师",
|
||||
"icon": "🎓",
|
||||
"description": "授课讲解、制作课件、设计互动、辅导答疑",
|
||||
},
|
||||
# ... 其他角色
|
||||
}
|
||||
```
|
||||
|
||||
**要点:**
|
||||
- `key` 用英文下划线命名(与 role 标识一致)
|
||||
- `label` 用中文显示名称
|
||||
- `icon` 用一个 emoji 表示
|
||||
- `description` 用一句话概括职责
|
||||
|
||||
### 步骤 4:编写工厂方法
|
||||
|
||||
在 `TeamService` 类中添加 `create_*_template()` 方法。可直接复制已有模板方法,修改以下内容:
|
||||
|
||||
```python
|
||||
def create_education_training_template(
|
||||
self, user_id: str, workspace_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""创建「模板名称」模板:N 个角色 Agent + 1 个 Team。"""
|
||||
|
||||
role_configs = [
|
||||
{
|
||||
"role": "curriculum_designer", # 角色 key
|
||||
"name": "课程设计师", # Agent 名称
|
||||
"description": "...", # Agent 描述
|
||||
"system_prompt": CURRICULUM_DESIGNER_PROMPT, # 提示词常量
|
||||
"tools": ["web_search", "file_write", ...], # 工具列表
|
||||
"temperature": 0.4, # 温度参数
|
||||
"model": "deepseek-v4-pro", # 模型
|
||||
"max_iterations": 15, # 最大迭代次数
|
||||
"is_lead": True, # 是否为团队 Leader
|
||||
},
|
||||
# ... 其他角色
|
||||
]
|
||||
|
||||
# Agent 创建/复用逻辑(无需修改,直接复制)
|
||||
created_agents: List[Dict] = []
|
||||
for rc in role_configs:
|
||||
existing = (
|
||||
self.db.query(Agent)
|
||||
.filter(Agent.name == rc["name"], Agent.user_id == user_id)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
created_agents.append({"agent": existing, **rc})
|
||||
continue
|
||||
agent = Agent(...) # 标准 Agent 创建
|
||||
# ...
|
||||
|
||||
# Team 创建(需修改名称和描述)
|
||||
team = Team(
|
||||
name="模板团队名称",
|
||||
description="团队描述",
|
||||
config={"workflow": "模板标识", "roles": list(EDUCATION_ROLES.keys())},
|
||||
# ...
|
||||
)
|
||||
```
|
||||
|
||||
**参数选择经验:**
|
||||
|
||||
| 角色类型 | 推荐模型 | 推荐温度 | 推荐迭代 |
|
||||
|----------|---------|---------|---------|
|
||||
| 需要创造力的角色(设计/产品/讲师) | v4-pro | 0.4-0.6 | 15-20 |
|
||||
| 需要精确执行的角色(开发/测试/运维) | v4-pro 或 flash | 0.3-0.4 | 12-25 |
|
||||
| 批处理/管理类角色(教务/助教) | v4-flash | 0.3 | 12-15 |
|
||||
|
||||
### 步骤 5:注册 API 端点
|
||||
|
||||
在 `teams.py` 中添加新路由,直接复制已有端点并修改路径和方法名:
|
||||
|
||||
```python
|
||||
@router.post("/template/education-training")
|
||||
def create_education_training_template(
|
||||
workspace_id: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""模板说明 docstring"""
|
||||
svc = TeamService(db)
|
||||
result = svc.create_education_training_template(
|
||||
user_id=current_user.id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
return {"data": result}
|
||||
```
|
||||
|
||||
**命名规范:**
|
||||
- URL 路径:`/template/{kebab-case-模板名}`
|
||||
- 函数名:`create_{snake_case_模板名}_template`
|
||||
|
||||
### 步骤 6:合并预设角色
|
||||
|
||||
更新 `get_preset_roles()` 函数,将新角色集合并进去:
|
||||
|
||||
```python
|
||||
def get_preset_roles() -> Dict[str, Any]:
|
||||
"""返回所有预置角色定义(供前端展示)。"""
|
||||
return {**PRESET_ROLES, **EDUCATION_ROLES, **NEW_TEAM_ROLES}
|
||||
```
|
||||
|
||||
> 这一步很关键,否则前端不会渲染新角色的槽位。
|
||||
|
||||
### 步骤 7:前端 API 客户端
|
||||
|
||||
在 `frontend/src/api/teams.ts` 中添加:
|
||||
|
||||
```typescript
|
||||
/** 一键创建教育培训团队模板 */
|
||||
export function createEducationTrainingTemplate(workspaceId?: string) {
|
||||
return api.post('/api/v1/teams/template/education-training', null, {
|
||||
params: workspaceId ? { workspace_id: workspaceId } : {},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 8:前端 UI 按钮
|
||||
|
||||
在 `frontend/src/views/TeamBuilder.vue` 中做 3 处修改:
|
||||
|
||||
**a) 导入函数**
|
||||
```typescript
|
||||
import {
|
||||
// ... 已有导入
|
||||
createEducationTrainingTemplate,
|
||||
} from '@/api/teams'
|
||||
```
|
||||
|
||||
**b) 添加按钮**
|
||||
```vue
|
||||
<el-button type="warning" @click="handleCreateEducationTemplate"
|
||||
:loading="creatingEduTemplate">
|
||||
<el-icon><MagicStick /></el-icon> 教育培训模板
|
||||
</el-button>
|
||||
```
|
||||
|
||||
**c) 添加 handler**
|
||||
```typescript
|
||||
const creatingEduTemplate = ref(false)
|
||||
|
||||
async function handleCreateEducationTemplate() {
|
||||
creatingEduTemplate.value = true
|
||||
try {
|
||||
const res = await createEducationTrainingTemplate()
|
||||
const team = (res.data as any)?.data
|
||||
if (team) {
|
||||
currentTeamId.value = team.id
|
||||
teamName.value = team.name
|
||||
await loadAgents()
|
||||
const slots: Record<string, any> = {}
|
||||
if (team.members) {
|
||||
for (const m of team.members) {
|
||||
const agent = agents.value.find(a => a.id === m.agent_id)
|
||||
if (agent) slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
|
||||
}
|
||||
}
|
||||
roleSlots.value = slots
|
||||
ElMessage.success('教育培训团队创建成功!')
|
||||
}
|
||||
await loadTeams()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
|
||||
} finally {
|
||||
creatingEduTemplate.value = false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 该 handler 逻辑与 `handleCreateTemplate` 完全相同,只需修改函数名和成功提示文字。
|
||||
|
||||
### 步骤 9:验证
|
||||
|
||||
```bash
|
||||
# 1. 清除 Python 缓存并重启后端
|
||||
find backend -name "*.pyc" -path "*__pycache__/*" -delete
|
||||
# 重启 uvicorn(端口以实际为准)
|
||||
|
||||
# 2. 验证预设角色
|
||||
curl http://127.0.0.1:8041/api/v1/teams/preset-roles
|
||||
# 应包含新增角色
|
||||
|
||||
# 3. 获取 token 并创建模板
|
||||
TOKEN=$(curl -s -X POST "http://127.0.0.1:8037/api/v1/auth/login" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "username=admin&password=123456" \
|
||||
| python -c "import json,sys; print(json.load(sys.stdin)['access_token'])")
|
||||
|
||||
curl -X POST "http://127.0.0.1:8041/api/v1/teams/template/education-training" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 4. 前端验证:访问 http://localhost:3001/teams,点击新按钮
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、模板设计原则
|
||||
|
||||
### 3.1 角色数量
|
||||
- 最少 3 个,最多 6 个
|
||||
- 少于 3 个失去团队协作意义
|
||||
- 多于 6 个导致执行链条过长,容易失败
|
||||
|
||||
### 3.2 角色分工
|
||||
- 每个角色职责边界清晰,避免重叠
|
||||
- 设置 1-2 个 Leader 角色(负责规划和决策)
|
||||
- 执行类角色负责具体产出
|
||||
|
||||
### 3.3 提示词设计
|
||||
- 明确输入是什么、输出是什么
|
||||
- 给出输出格式约束(JSON Schema 优先)
|
||||
- 注明需要使用的工具
|
||||
- 中文场景用中文输出,英文场景用英文
|
||||
|
||||
### 3.4 工具选择
|
||||
- 核心工具必选:`file_write`、`file_read`
|
||||
- 信息获取:`web_search`
|
||||
- 分析和处理:`text_analyze`、`json_process`
|
||||
- 代码相关:`execute_code`、`grep_search`
|
||||
- 平台专用:`database_query`、`git_operation`、`docker_manage`
|
||||
|
||||
---
|
||||
|
||||
## 四、后续优化方向
|
||||
|
||||
1. **模板参数化**:允许用户在创建时自定义角色数量、模型选择
|
||||
2. **模板市场**:用户可自行创建和分享模板
|
||||
3. **模板版本管理**:模板升级时不影响已创建的团队
|
||||
4. **前端模板选择器**:统一为弹窗选择,而非多个按钮
|
||||
5. **动态角色加载**:根据模板类型动态渲染不同角色槽位
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-16
|
||||
> 基于「软件公司」「教育培训」「天工平台工程」三个模板的实现经验
|
||||
249
docs/agent-guides/天工平台工程团队使用指南.md
Normal file
249
docs/agent-guides/天工平台工程团队使用指南.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# 天工平台工程团队 — 使用指南
|
||||
|
||||
> 如何使用虚拟团队自举维护和迭代天工智能体平台本身
|
||||
|
||||
---
|
||||
|
||||
## 一、快速开始
|
||||
|
||||
### 1.1 创建团队
|
||||
|
||||
1. 访问 `http://localhost:3001/teams`
|
||||
2. 点击红色 **「天工平台工程团队」** 按钮
|
||||
3. 系统自动创建 5 个角色 Agent 并组建团队,5 个角色槽位自动填充
|
||||
|
||||
### 1.2 团队角色分工
|
||||
|
||||
| 角色 | 负责范围 | 规划者 |
|
||||
|------|----------|--------|
|
||||
| 📊 产品负责人 | 需求分析、任务分解、优先级排序、计划制定 | **是** |
|
||||
| 🏗️ 全栈开发 | 代码实现、Bug 修复、API 开发、数据库迁移 | **是** |
|
||||
| 🎯 前端体验 | 工作流编辑器、移动端、交互打磨、飞书 Bot 体验 | 否 |
|
||||
| 🚀 平台运维 | 部署、CI/CD、监控、日志、K8s | 否 |
|
||||
| 🔍 质量保障 | 测试、性能压测、安全审计、回归测试 | 否 |
|
||||
|
||||
> 执行项目时,📊**产品负责人**会自动负责规划阶段,将任务分解后分配给对应角色。
|
||||
|
||||
---
|
||||
|
||||
## 二、不同场景的使用方法
|
||||
|
||||
### 场景 1:修复 Bug
|
||||
|
||||
**输入示例:**
|
||||
```
|
||||
修复 Bug:用户执行 Agent 对话时,超过 30000ms 报 timeout 错误。
|
||||
|
||||
现象:
|
||||
- 前端对话框中显示 "发送失败: timeout of 30000ms exceeded"
|
||||
- 后端日志显示 Celery Worker 无法连接 Redis
|
||||
|
||||
线索:
|
||||
- backend/.env 中 REDIS_URL 可能配置错误
|
||||
- 检查 app/core/celery_app.py 的超时配置
|
||||
```
|
||||
|
||||
**执行流程:**
|
||||
1. 产品负责人 → 分析 Bug 影响范围,制定修复计划
|
||||
2. 全栈开发 → 检查代码、定位根因、编写修复
|
||||
3. 质量保障 → 验证修复、回归测试
|
||||
4. 平台运维 → 确认配置正确、验证健康检查
|
||||
|
||||
**输出位置:** `team_projects/{team_id}/{project-name}/` 包含修复 diff、测试脚本
|
||||
|
||||
---
|
||||
|
||||
### 场景 2:开发新功能
|
||||
|
||||
**输入示例:**
|
||||
```
|
||||
新增功能:Agent 市场模板评分系统
|
||||
|
||||
需求:
|
||||
- 用户可以对 Agent 市场中的模板进行 1-5 星评分
|
||||
- 显示平均评分和评分人数
|
||||
- 用户只能评分一次(可以修改)
|
||||
|
||||
涉及:
|
||||
- 后端:新增评分表 + API(已预设 POST /api/v1/agent-market/{id}/rate)
|
||||
- 前端:Agent 市场详情页添加星级评分组件
|
||||
- 测试:评分增删改查 + 权限验证
|
||||
```
|
||||
|
||||
**执行流程:**
|
||||
1. 产品负责人 → 拆分为后端/前端/测试三个子任务
|
||||
2. 全栈开发 → 后端数据库迁移 + API 实现
|
||||
3. 前端体验 → 星级组件 + 交互优化
|
||||
4. 质量保障 → 接口测试 + 边界用例
|
||||
5. 平台运维 → 数据库迁移执行 + 部署验证
|
||||
|
||||
---
|
||||
|
||||
### 场景 3:性能优化
|
||||
|
||||
**输入示例:**
|
||||
```
|
||||
性能优化:Agent 列表页加载缓慢
|
||||
|
||||
现状:
|
||||
- /api/v1/agents 接口响应时间 p95 = 2.3s
|
||||
- 页面首次加载 100 个 Agent 需要 4s+
|
||||
|
||||
目标:
|
||||
- API 响应 p95 < 500ms
|
||||
- 前端首屏渲染 < 1.5s
|
||||
|
||||
优化方向:
|
||||
- 后端:添加数据库索引、分页优化、Redis 缓存
|
||||
- 前端:虚拟滚动、懒加载
|
||||
```
|
||||
|
||||
**执行流程:**
|
||||
1. 产品负责人 → 确定性能目标、划分优化优先级
|
||||
2. 全栈开发 → 后端查询优化、缓存策略
|
||||
3. 前端体验 → 虚拟滚动实现、代码分割
|
||||
4. 质量保障 → 压测验证、基准对比
|
||||
5. 平台运维 → 生产环境监控配置
|
||||
|
||||
---
|
||||
|
||||
### 场景 4:安全加固
|
||||
|
||||
**输入示例:**
|
||||
```
|
||||
安全加固:全平台安全审计与修复
|
||||
|
||||
检查项:
|
||||
- SQL 注入风险扫描
|
||||
- JWT Token 过期与刷新机制
|
||||
- 文件上传路径遍历防护
|
||||
- API 接口鉴权覆盖率检查
|
||||
- 依赖库漏洞扫描(pip-audit / pnpm audit)
|
||||
- 密码加密强度验证
|
||||
|
||||
已有工具:CodeQL、Trivy、Gitleaks 已配置在 CI 中
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景 5:系统升级迭代
|
||||
|
||||
**输入示例:**
|
||||
```
|
||||
平台迭代 v2.1.0:
|
||||
|
||||
1. 多租户支持(数据隔离 + 工作区管理)
|
||||
2. 飞书 Bot 体验优化(反馈按钮 + 会话摘要)
|
||||
3. Android App 完成度提升至 90%
|
||||
4. 模板市场上线(支持用户发布/安装模板)
|
||||
5. 开发文档完善至 80%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、编写任务描述的最佳实践
|
||||
|
||||
### 3.1 好的任务描述六要素
|
||||
|
||||
| 要素 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **做什么** | 一句话概括 | "修复 Agent 执行超时 Bug" |
|
||||
| **现象/背景** | 当前问题或需求来源 | "用户反馈:点击执行后 30s 报错" |
|
||||
| **涉及范围** | 哪些模块/文件可能需要改动 | "backend/app/core/celery_app.py + .env" |
|
||||
| **期望结果** | 完成后的行为 | "Agent 执行成功返回结果,不再超时" |
|
||||
| **约束条件** | 不破坏什么 | "不影响已有 Agent 的执行逻辑" |
|
||||
| **技术提示** | 已知的线索或方向 | "怀疑 Redis URL 配置不一致" |
|
||||
|
||||
### 3.2 避免的错误描述
|
||||
|
||||
**不好:** "修一下超时"
|
||||
**不好:** "前端有点慢,优化一下"
|
||||
**不好:** "所有东西都检查一遍"
|
||||
|
||||
**好的:** "修复 Agent 对话超时(30000ms),检查 Redis 连接和 Celery 配置,在 backend/.env 和 app/core/celery_app.py 中排查,完成后验证 /health 接口正常"
|
||||
|
||||
---
|
||||
|
||||
## 四、执行过程与产出
|
||||
|
||||
### 4.1 两种执行模式
|
||||
|
||||
| 模式 | 操作 | 特点 |
|
||||
|------|------|------|
|
||||
| **同步执行** | 点击「执行项目」 | 等待完成后一次性查看所有产出 |
|
||||
| **流式执行** | 点击「流式执行」 | 实时推送每个阶段的进度 |
|
||||
|
||||
推荐日常任务用**同步执行**,大型任务用**流式执行**观察中间过程。
|
||||
|
||||
### 4.2 产出物的存放与查看
|
||||
|
||||
执行完成后,文件保存在:
|
||||
```
|
||||
D:\aaa\aiagent\team_projects\{team_id}\{project-name}/
|
||||
```
|
||||
|
||||
页面会展示:
|
||||
- **项目计划** tab:产品负责人的 JSON 规划
|
||||
- **各阶段** tab:每个角色的产出内容
|
||||
- **项目文件** tab:所有生成文件的路径清单
|
||||
|
||||
### 4.3 产出的典型文件类型
|
||||
|
||||
| 任务类型 | 典型产出 |
|
||||
|----------|----------|
|
||||
| Bug 修复 | 修复代码 diff、测试用例、根因分析报告 |
|
||||
| 新功能 | 完整代码文件、API 接口定义、前端组件、测试脚本 |
|
||||
| 性能优化 | 优化前后对比、压测报告、配置变更 |
|
||||
| 安全加固 | 漏洞报告、修复代码、安全配置 |
|
||||
| 文档完善 | Markdown 文档、API 参考、使用教程 |
|
||||
|
||||
---
|
||||
|
||||
## 五、注意事项与技巧
|
||||
|
||||
### 5.1 执行前检查
|
||||
|
||||
- [ ] 5 个角色槽位都已分配 Agent(未分配的角色会被跳过)
|
||||
- [ ] 任务描述足够具体,包含明确的目标和约束
|
||||
- [ ] 如有参考文件/配置,在描述中提及具体路径
|
||||
|
||||
### 5.2 执行后验证
|
||||
|
||||
- [ ] 检查「项目文件」tab 确认文件已生成
|
||||
- [ ] 在本地文件系统中验证生成的文件内容
|
||||
- [ ] 对于代码产出,不要直接复制到生产目录,先 review
|
||||
- [ ] 数据库迁移类产出,先备份再执行
|
||||
|
||||
### 5.3 迭代式使用
|
||||
|
||||
大型任务建议**分多次执行**,而非一次描述所有需求:
|
||||
|
||||
```
|
||||
第 1 轮: "分析平台当前性能瓶颈,输出性能评估报告"
|
||||
↓ 拿到报告后
|
||||
第 2 轮: "根据报告前三项问题,实施优化方案"
|
||||
↓ 验证优化效果后
|
||||
第 3 轮: "继续优化剩余问题"
|
||||
```
|
||||
|
||||
### 5.4 与其他模板配合
|
||||
|
||||
| 场景 | 使用模板 |
|
||||
|------|----------|
|
||||
| 平台核心引擎升级 | 天工平台工程团队 |
|
||||
| 为平台开发全新子模块 | 软件公司虚拟团队(新项目从零开发) |
|
||||
| 平台文档/培训体系 | 教育培训团队(制作使用教程) |
|
||||
|
||||
---
|
||||
|
||||
## 六、已知限制
|
||||
|
||||
1. **编排引擎依赖 Leader 做规划** — 如果产品负责人的规划质量不佳(角色分配不合理),后续阶段会受影响
|
||||
2. **顺序执行** — 不同于 Agent 编排的并行模式,团队按顺序执行,不适合需要实时协作的任务
|
||||
3. **产出需人工审核** — AI 生成的代码/配置必须 review 后才能合入主分支
|
||||
4. **每次执行是独立的** — 不会记住上次执行的内容,需要在新描述中提供足够上下文
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026-06-17
|
||||
> 配套修改:编排引擎已支持多模板类型,自动识别团队 workflow 选择规划者
|
||||
323
docs/virtual-team.md
Normal file
323
docs/virtual-team.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 虚拟团队 — 软件公司协作开发
|
||||
|
||||
> **Virtual Software Company Team** — 基于多 Agent 角色编排的自动化软件开发流水线
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
虚拟团队是天工智能体平台的核心高级功能,将软件公司的五大经典角色(PM / 设计师 / 开发 / 测试 / DevOps)映射为 AI Agent,组成一个可自动协作的虚拟开发团队。用户只需输入项目描述,团队即可自动完成从需求分析到部署上线的全流程,并产出实际代码文件。
|
||||
|
||||
### 核心能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
|:-----|:-----|
|
||||
| **一键创建团队** | 点击"软件公司模板",自动创建 5 个角色 Agent 并组建团队 |
|
||||
| **PM 智能规划** | 项目经理 Agent 分析需求,输出包含阶段划分的 JSON 开发计划 |
|
||||
| **顺序流水线执行** | 按 PM → Designer → Developer → QA → DevOps 顺序执行各阶段 |
|
||||
| **文件产出** | 各 Agent 将代码、文档、配置写入指定项目目录 |
|
||||
| **QA 自动审查** | 测试 Agent 审查全部交付物,输出缺陷报告和评分 |
|
||||
| **流式进度推送** | SSE 实时推送每个阶段的开始/完成状态 |
|
||||
| **项目文件追溯** | 执行完成后展示项目文件路径和完整文件清单 |
|
||||
|
||||
---
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 2.1 角色定义
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 虚拟软件团队 (5 Roles) │
|
||||
├──────────┬──────────┬──────────┬──────────┬─────────────────┤
|
||||
│ 📋 PM │ 🎨 UI │ 💻 Dev │ 🔍 QA │ 🚀 DevOps │
|
||||
│ 项目经理 │ 设计师 │ 全栈开发 │ 测试工程 │ DevOps工程师 │
|
||||
│ │ │ │ │ │
|
||||
│ 需求分析 │ 交互设计 │ 前后端 │ 审查交付 │ 部署配置 │
|
||||
│ 任务分解 │ 视觉规范 │ 代码编写 │ 发现缺陷 │ CI/CD │
|
||||
│ 计划制定 │ 组件规划 │ 数据库 │ 验收标准 │ 环境管理 │
|
||||
│ │ │ │ │ │
|
||||
│ Lead ✅ │ │ │ │ │
|
||||
├──────────┴──────────┴──────────┴──────────┴─────────────────┤
|
||||
│ 温度: 0.4 0.6 0.3 0.3 0.3 │
|
||||
│ 模型: v4-pro flash v4-pro flash flash │
|
||||
│ 迭代: 15 12 25 15 12 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 执行流程
|
||||
|
||||
```
|
||||
用户输入项目描述
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Phase 0: PM 规划 │ ─── 输出 JSON 执行计划(4~7 个阶段)
|
||||
└──────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ Phase 1..N │ 按角色顺序执行:
|
||||
│ 各阶段 Agent │ Designer → Developer → QA → DevOps
|
||||
│ 写入项目文件 │ 每个 Agent 接收上下文 + 项目目录路径
|
||||
└──────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ QA 审查 │ 汇总审核全部交付物,给出评分
|
||||
└──────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 最终交付物汇总 │ 项目路径 + 文件清单 + 评审报告
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型
|
||||
|
||||
### 3.1 Team(团队)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `id` | UUID | 团队唯一标识 |
|
||||
| `name` | str | 团队名称(默认"软件公司虚拟团队") |
|
||||
| `description` | str | 团队描述 |
|
||||
| `workspace_id` | UUID? | 所属工作区 |
|
||||
| `user_id` | UUID | 创建者 |
|
||||
| `is_template` | bool | 是否为模板(模板创建时为 True) |
|
||||
| `config` | JSON | 扩展配置(角色列表等) |
|
||||
| `status` | str | 状态:active / archived |
|
||||
|
||||
### 3.2 TeamMember(团队成员)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `id` | UUID | 记录 ID |
|
||||
| `team_id` | UUID FK | 所属团队 |
|
||||
| `agent_id` | UUID FK | 关联的 Agent |
|
||||
| `role` | str | 角色标识:pm / designer / developer / qa / devops |
|
||||
| `position` | int | 排序位置 |
|
||||
| `is_lead` | bool | 是否为该角色 Leader |
|
||||
|
||||
---
|
||||
|
||||
## 四、API 参考
|
||||
|
||||
**Base**: `/api/v1/teams`
|
||||
|
||||
### 4.1 团队 CRUD
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `GET` | `/teams` | 列出用户的所有团队 |
|
||||
| `POST` | `/teams` | 创建团队 |
|
||||
| `GET` | `/teams/{id}` | 获取团队详情(含成员列表) |
|
||||
| `PUT` | `/teams/{id}` | 更新团队信息 |
|
||||
| `DELETE` | `/teams/{id}` | 删除团队 |
|
||||
|
||||
### 4.2 成员管理
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `GET` | `/teams/{id}/members` | 列出团队成员 |
|
||||
| `POST` | `/teams/{id}/members` | 添加成员(agent_id + role) |
|
||||
| `DELETE` | `/teams/{id}/members/{mid}` | 移除成员 |
|
||||
|
||||
### 4.3 模板工厂
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `POST` | `/teams/template/software-company` | **一键创建软件公司模板** — 自动创建 5 个角色 Agent + 团队 |
|
||||
| `GET` | `/teams/preset-roles` | 获取预置角色定义(名称、图标、描述) |
|
||||
|
||||
### 4.4 项目执行
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|:-----|:-----|:-----|
|
||||
| `POST` | `/teams/{id}/execute` | **同步执行** — 返回完整执行结果(含计划、各阶段产出、文件清单) |
|
||||
| `POST` | `/teams/{id}/execute/stream` | **流式执行** — SSE 事件流,实时推送执行进度 |
|
||||
|
||||
#### 同步执行响应结构
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"team_id": "uuid",
|
||||
"team_name": "软件公司虚拟团队",
|
||||
"project_description": "...",
|
||||
"project_path": "D:\\aaa\\aiagent\\team_projects\\{team_id}\\{project}/",
|
||||
"files": ["D:\\...\\index.html", "D:\\...\\api.py", ...],
|
||||
"plan": { "project_name": "...", "phases": [...] },
|
||||
"phases": [
|
||||
{
|
||||
"phase": 1,
|
||||
"name": "需求分析与产品定义",
|
||||
"role": "pm",
|
||||
"agent_name": "项目经理 (PM)",
|
||||
"output": "...",
|
||||
"success": true,
|
||||
"iterations": 1,
|
||||
"tool_calls": 2,
|
||||
"files": ["..."]
|
||||
}
|
||||
],
|
||||
"qa_review": { "pass": true, "score": 85, "issues": [...] },
|
||||
"final_deliverable": "# 全部交付物汇总 ...",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### SSE 流式事件类型
|
||||
|
||||
| 事件类型 | 说明 | 关键字段 |
|
||||
|:---------|:-----|:---------|
|
||||
| `plan_start` | PM 开始规划 | `team_name`, `member_count`, `project_path` |
|
||||
| `plan_done` | PM 规划完成 | `plan` (JSON), `success` |
|
||||
| `phase_start` | 阶段开始执行 | `phase`, `name`, `role`, `agent` |
|
||||
| `phase_done` | 阶段执行完成 | `output`, `success`, `iterations`, `tool_calls`, `files` |
|
||||
| `qa_done` | QA 审查完成 | `output`, `review` (JSON) |
|
||||
| `final` | 全部完成 | `deliverable`, `project_path`, `files`, `phase_count` |
|
||||
| `error` | 错误 | `content` |
|
||||
|
||||
---
|
||||
|
||||
## 五、Agent 系统提示词设计
|
||||
|
||||
每个角色 Agent 在创建时被注入专用的系统提示词,确保执行风格符合角色定位:
|
||||
|
||||
### PM 系统提示词要点
|
||||
- 分析需求 → 用户故事 → 阶段分解(4-7 个)
|
||||
- 每个阶段指定 role + 描述 + 期望产出
|
||||
- 输出严格 JSON 格式
|
||||
|
||||
### Designer 系统提示词要点
|
||||
- 用户流程 / 线框图 / 组件层级 / 视觉设计 tokens
|
||||
- 指定颜色、字体、间距系统
|
||||
- 输出结构化 Markdown + HTML/CSS 原型
|
||||
|
||||
### Developer 系统提示词要点
|
||||
- 技术栈:Python/FastAPI + Vue 3/TypeScript + SQLAlchemy + Element Plus
|
||||
- 使用 `file_write` 产出完整、可运行的代码文件
|
||||
- 包含类型标注、错误处理、输入验证
|
||||
|
||||
### QA 系统提示词要点
|
||||
- 审查验收标准覆盖、边界情况、安全漏洞、API 契约一致性
|
||||
- 输出 JSON 评审报告(pass / score / issues / overall_assessment)
|
||||
|
||||
### DevOps 系统提示词要点
|
||||
- Dockerfile + docker-compose + nginx.conf
|
||||
- .env.example + 健康检查 + 部署 README
|
||||
|
||||
---
|
||||
|
||||
## 六、前后端文件索引
|
||||
|
||||
### 后端
|
||||
|
||||
| 文件 | 说明 |
|
||||
|:-----|:-----|
|
||||
| `backend/app/models/team.py` | Team / TeamMember SQLAlchemy 模型 |
|
||||
| `backend/app/api/teams.py` | 12 个 API 端点(CRUD + 模板 + 执行) |
|
||||
| `backend/app/services/team_service.py` | TeamService + 模板工厂 + 5 种角色系统提示词 |
|
||||
| `backend/app/services/team_orchestrator.py` | TeamOrchestrator 执行引擎(同步 + SSE 流式) |
|
||||
|
||||
### 前端
|
||||
|
||||
| 文件 | 说明 |
|
||||
|:-----|:-----|
|
||||
| `frontend/src/views/TeamBuilder.vue` | 虚拟团队主页面(~850 行),含角色槽位、拖拽、执行结果展示 |
|
||||
| `frontend/src/api/teams.ts` | TypeScript API 客户端(接口 + 请求函数) |
|
||||
| `frontend/src/router/index.ts` | `/teams` 路由注册 |
|
||||
| `frontend/src/components/MainLayout.vue` | 导航栏"虚拟团队"菜单项 |
|
||||
|
||||
---
|
||||
|
||||
## 七、使用指南
|
||||
|
||||
### 7.1 创建虚拟团队
|
||||
|
||||
1. 访问 `http://localhost:3001/teams`
|
||||
2. 点击 **"软件公司模板"** 按钮
|
||||
3. 系统自动创建 5 个角色 Agent(如已有同名则复用)并组建团队
|
||||
4. 5 个角色槽位自动填充对应 Agent
|
||||
|
||||
### 7.2 手动分配 Agent
|
||||
|
||||
1. 左侧面板显示所有可用 Agent
|
||||
2. 拖拽 Agent 到中间对应的角色卡片上
|
||||
3. 也可从槽位中移除已分配的 Agent
|
||||
|
||||
### 7.3 执行项目
|
||||
|
||||
1. 在底部输入框输入项目描述,例如:
|
||||
> 做一个支持用户注册登录的个人博客系统,包含文章发布、评论功能
|
||||
|
||||
2. 点击 **"执行项目"**(同步,有 tab 页展示)或 **"流式执行"**(实时进度推送)
|
||||
3. 观察执行过程:
|
||||
- PM 需求分析 → 生成开发计划
|
||||
- 各阶段按角色顺序执行
|
||||
- QA 审查最终交付物
|
||||
4. 查看结果:
|
||||
- **"项目计划"** tab:PM 的 JSON 规划
|
||||
- **各阶段 tab**:Agent 的产出内容(Markdown 渲染)
|
||||
- **"QA 审查"** tab:质量报告
|
||||
- **"最终交付物"** tab:汇总的全部产出
|
||||
- **"项目文件"** tab:保存路径 + 产出文件清单
|
||||
|
||||
### 7.4 查找交付文件
|
||||
|
||||
执行完成后,文件保存在:
|
||||
```
|
||||
<项目根目录>/team_projects/<team_id>/<项目名>/
|
||||
```
|
||||
|
||||
页面上的"项目文件"区域会显示完整路径(可复制)和所有产出文件的列表。
|
||||
|
||||
---
|
||||
|
||||
## 八、技术要点
|
||||
|
||||
### 8.1 项目目录管理
|
||||
|
||||
- 执行前在 `<workspace_root>/team_projects/<team_id>/<safe_name>/` 创建目录
|
||||
- 安全命名:取项目描述前 40 字符,仅保留 ASCII 字母数字和连字符
|
||||
- 每个阶段 Agent 通过 prompt 获取该路径,`file_write` 工具将文件写入此目录
|
||||
|
||||
### 8.2 Agent 配置构建
|
||||
|
||||
`_build_agent_config()` 从 Agent 的 `workflow_config` JSON 中提取:
|
||||
- `nodes[type=llm].data.prompt` → 系统提示词
|
||||
- `nodes[type=llm].data.model` → 模型名称
|
||||
- `nodes[type=llm].data.temperature` → 温度参数
|
||||
- `nodes[type=llm].data.selected_tools` → 工具白名单
|
||||
- `nodes[type=llm].data.max_iterations` → 最大 ReAct 迭代次数
|
||||
|
||||
### 8.3 上下文传递
|
||||
|
||||
每个阶段 Agent 接收前序阶段产出的摘要(上限 2000 字符),形成渐进式上下文积累,确保后续阶段了解之前的工作。
|
||||
|
||||
### 8.4 文件追踪(双保险机制)
|
||||
|
||||
1. **AgentResult.steps 提取**:扫描 `AgentResult.steps` 中的 `file_write` 工具调用记录
|
||||
2. **目录扫描**:每个阶段后对比项目目录快照,检测新创建的文件
|
||||
3. 最终文件列表以目录扫描为准,同时合并 AgentResult 提取的路径
|
||||
|
||||
---
|
||||
|
||||
## 九、与相关功能的对比
|
||||
|
||||
| 功能 | Agent 编排 | 虚拟团队 |
|
||||
|:-----|:----------|:---------|
|
||||
| 编排方式 | 可视化 DAG 画布(节点+连线) | 固定角色流水线 |
|
||||
| 角色定义 | 无(通用节点) | PM/Designer/Dev/QA/DevOps 五种角色 |
|
||||
| 执行模式 | 5 种协作模式(顺序/并行/投票/Swarm/辩论) | 顺序执行 |
|
||||
| 适用场景 | 通用多 Agent 协作 | 软件项目开发 |
|
||||
| 文件产出 | 依赖节点配置 | 强制写入项目目录 |
|
||||
| 项目路径 | 无 | 自动创建并返回 |
|
||||
|
||||
---
|
||||
|
||||
> 最后更新:2026 年 6 月 16 日
|
||||
146
frontend/src/api/teams.ts
Normal file
146
frontend/src/api/teams.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import api from './index'
|
||||
|
||||
export interface TeamMember {
|
||||
id: string
|
||||
team_id: string
|
||||
agent_id: string
|
||||
role: string
|
||||
position: number
|
||||
is_lead: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
workspace_id: string | null
|
||||
user_id: string
|
||||
is_template: boolean
|
||||
config: any
|
||||
status: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
members?: TeamMember[]
|
||||
}
|
||||
|
||||
export interface PresetRole {
|
||||
label: string
|
||||
icon: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface PhaseResult {
|
||||
phase: number
|
||||
name: string
|
||||
role: string
|
||||
agent_name: string | null
|
||||
output: string
|
||||
success: boolean
|
||||
iterations?: number
|
||||
tool_calls?: number
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export interface ExecuteResult {
|
||||
team_id: string
|
||||
team_name: string
|
||||
project_description: string
|
||||
started_at: string
|
||||
completed_at?: string
|
||||
plan: any
|
||||
phases: PhaseResult[]
|
||||
qa_review: any
|
||||
final_deliverable: string
|
||||
success: boolean
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
/** 获取预置角色定义 */
|
||||
export function getPresetRoles() {
|
||||
return api.get('/api/v1/teams/preset-roles')
|
||||
}
|
||||
|
||||
/** 列出团队 */
|
||||
export function listTeams(params?: { workspace_id?: string }) {
|
||||
return api.get('/api/v1/teams', { params })
|
||||
}
|
||||
|
||||
/** 获取团队详情 */
|
||||
export function getTeam(teamId: string) {
|
||||
return api.get(`/api/v1/teams/${teamId}`)
|
||||
}
|
||||
|
||||
/** 创建团队 */
|
||||
export function createTeam(data: {
|
||||
name: string
|
||||
description?: string
|
||||
workspace_id?: string
|
||||
config?: any
|
||||
}) {
|
||||
return api.post('/api/v1/teams', data)
|
||||
}
|
||||
|
||||
/** 更新团队 */
|
||||
export function updateTeam(teamId: string, data: {
|
||||
name?: string
|
||||
description?: string
|
||||
config?: any
|
||||
status?: string
|
||||
}) {
|
||||
return api.put(`/api/v1/teams/${teamId}`, data)
|
||||
}
|
||||
|
||||
/** 删除团队 */
|
||||
export function deleteTeam(teamId: string) {
|
||||
return api.delete(`/api/v1/teams/${teamId}`)
|
||||
}
|
||||
|
||||
/** 一键创建软件公司虚拟团队模板 */
|
||||
export function createSoftwareCompanyTemplate(workspaceId?: string) {
|
||||
return api.post('/api/v1/teams/template/software-company', null, {
|
||||
params: workspaceId ? { workspace_id: workspaceId } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 一键创建教育培训团队模板 */
|
||||
export function createEducationTrainingTemplate(workspaceId?: string) {
|
||||
return api.post('/api/v1/teams/template/education-training', null, {
|
||||
params: workspaceId ? { workspace_id: workspaceId } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 一键创建天工平台工程团队模板 */
|
||||
export function createPlatformEngineeringTemplate(workspaceId?: string) {
|
||||
return api.post('/api/v1/teams/template/platform-engineering', null, {
|
||||
params: workspaceId ? { workspace_id: workspaceId } : {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 添加团队成员 */
|
||||
export function addMember(teamId: string, data: {
|
||||
agent_id: string
|
||||
role: string
|
||||
position?: number
|
||||
is_lead?: boolean
|
||||
}) {
|
||||
return api.post(`/api/v1/teams/${teamId}/members`, data)
|
||||
}
|
||||
|
||||
/** 移除团队成员 */
|
||||
export function removeMember(teamId: string, memberId: string) {
|
||||
return api.delete(`/api/v1/teams/${teamId}/members/${memberId}`)
|
||||
}
|
||||
|
||||
/** 列出团队成员 */
|
||||
export function listMembers(teamId: string) {
|
||||
return api.get(`/api/v1/teams/${teamId}/members`)
|
||||
}
|
||||
|
||||
/** 执行团队项目 */
|
||||
export function executeProject(teamId: string, projectDescription: string, autoApproveFiles = true) {
|
||||
return api.post(`/api/v1/teams/${teamId}/execute`, {
|
||||
project_description: projectDescription,
|
||||
auto_approve_files: autoApproveFiles,
|
||||
}, { timeout: 600000 }) // 10 min timeout for long executions
|
||||
}
|
||||
970
frontend/src/views/TeamBuilder.vue
Normal file
970
frontend/src/views/TeamBuilder.vue
Normal file
@@ -0,0 +1,970 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<div class="team-builder">
|
||||
<!-- 顶部工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="teamName"
|
||||
placeholder="团队名称"
|
||||
style="width: 260px"
|
||||
@change="onTeamNameChange"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSaveTeam" :loading="saving">
|
||||
<el-icon><Check /></el-icon> 保存团队
|
||||
</el-button>
|
||||
<el-button type="success" @click="handleCreateTemplate" :loading="creatingTemplate">
|
||||
<el-icon><MagicStick /></el-icon> 软件公司模板
|
||||
</el-button>
|
||||
<el-button type="warning" @click="handleCreateEducationTemplate" :loading="creatingEduTemplate">
|
||||
<el-icon><MagicStick /></el-icon> 教育培训模板
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleCreatePlatformTemplate" :loading="creatingPlatformTemplate">
|
||||
<el-icon><MagicStick /></el-icon> 天工平台工程团队
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-select v-model="selectedTeamId" placeholder="加载已有团队" clearable style="width: 220px" @change="handleLoadTeam">
|
||||
<el-option v-for="t in teams" :key="t.id" :label="t.name" :value="t.id" />
|
||||
</el-select>
|
||||
<el-button @click="loadTeams" :loading="loadingTeams">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体区域 -->
|
||||
<div class="main-area">
|
||||
<!-- 左侧:可用 Agent 列表 -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-title">可用 Agent</div>
|
||||
<el-input v-model="agentSearch" placeholder="搜索Agent..." size="small" style="margin-bottom: 8px" clearable />
|
||||
<div class="agent-list">
|
||||
<div
|
||||
v-for="agent in filteredAgents"
|
||||
:key="agent.id"
|
||||
class="agent-item"
|
||||
:class="{ active: selectedAgent?.id === agent.id }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(agent)"
|
||||
@click="selectedAgent = agent"
|
||||
>
|
||||
<span class="agent-name">{{ agent.name }}</span>
|
||||
<el-tag size="small" type="info">{{ agent.agent_type || 'specialist' }}</el-tag>
|
||||
</div>
|
||||
<el-empty v-if="filteredAgents.length === 0" description="暂无可用Agent" :image-size="48" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:角色槽位 -->
|
||||
<div class="center-panel">
|
||||
<div class="panel-title">团队角色</div>
|
||||
<div class="role-slots">
|
||||
<div
|
||||
v-for="role in roles"
|
||||
:key="role.key"
|
||||
class="role-card"
|
||||
:class="{
|
||||
filled: roleSlots[role.key],
|
||||
'drag-over': dragOverRole === role.key,
|
||||
}"
|
||||
@dragover.prevent="dragOverRole = role.key"
|
||||
@dragleave="dragOverRole = null"
|
||||
@drop="onDrop(role.key)"
|
||||
>
|
||||
<div class="role-header">
|
||||
<span class="role-icon">{{ role.icon }}</span>
|
||||
<span class="role-label">{{ role.label }}</span>
|
||||
<el-tag v-if="roleSlots[role.key]?.is_lead" size="small" type="danger">Leader</el-tag>
|
||||
</div>
|
||||
<div class="role-desc">{{ role.description }}</div>
|
||||
<div v-if="roleSlots[role.key]" class="role-assigned">
|
||||
<el-tag type="success" closable @close="removeFromSlot(role.key)">
|
||||
{{ roleSlots[role.key].name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-else class="role-empty">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>拖拽 Agent 到此处</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:Agent 详情 -->
|
||||
<div class="right-panel" v-if="selectedAgent">
|
||||
<div class="panel-title">Agent 配置</div>
|
||||
<el-descriptions :column="1" size="small" border>
|
||||
<el-descriptions-item label="名称">{{ selectedAgent.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ selectedAgent.agent_type }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ selectedAgent.status }}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述">{{ selectedAgent.description || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div style="margin-top: 12px">
|
||||
<el-button size="small" @click="router.push(`/agents/${selectedAgent.id}/config`)">
|
||||
编辑 Agent
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:项目输入 + 执行 -->
|
||||
<div class="bottom-area">
|
||||
<div class="project-input">
|
||||
<el-input
|
||||
v-model="projectDescription"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="输入项目描述,例如:做一个支持用户注册登录的个人博客系统,包含文章发布、评论功能..."
|
||||
/>
|
||||
<el-switch
|
||||
v-model="autoApproveFiles"
|
||||
active-text="自动写入文件"
|
||||
style="margin-left: 12px"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleExecute('sync')"
|
||||
:loading="executing"
|
||||
:disabled="!projectDescription || !hasAnyRole"
|
||||
style="margin-left: 12px"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon> 执行项目
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
@click="handleExecute('stream')"
|
||||
:loading="executingStream"
|
||||
:disabled="!projectDescription || !hasAnyRole"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
<el-icon><Connection /></el-icon> 流式执行
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 执行结果 -->
|
||||
<div v-if="executing || executingStream || executeResult || streamEvents.length" class="result-area">
|
||||
<div class="result-header">
|
||||
<h3>执行结果</h3>
|
||||
<el-button v-if="executeResult || streamEvents.length" size="small" @click="executeResult = null; streamEvents = []; streamDeliverable = ''; streamProjectPath = ''; streamProjectFiles = []">清空</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 流式事件 -->
|
||||
<div v-if="streamEvents.length" class="stream-events">
|
||||
<div v-for="(evt, i) in streamEvents" :key="i" class="stream-event" :class="'event-' + evt.type">
|
||||
<template v-if="evt.type === 'plan_start'">
|
||||
<el-alert type="info" :closable="false">
|
||||
PM 开始规划 — 团队: {{ evt.team_name }} ({{ evt.member_count }} 名成员)
|
||||
</el-alert>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'plan_done'">
|
||||
<el-alert :type="evt.success ? 'success' : 'error'" :closable="false">
|
||||
PM 规划完成 — {{ evt.plan?.phases?.length || 0 }} 个阶段
|
||||
</el-alert>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'phase_start'">
|
||||
<div class="phase-start">
|
||||
<el-icon :size="18"><Loading /></el-icon>
|
||||
阶段 {{ evt.phase }}: {{ evt.name }} ({{ evt.role }}) — Agent: {{ evt.agent }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'phase_done'">
|
||||
<el-collapse>
|
||||
<el-collapse-item>
|
||||
<template #title>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<el-tag :type="evt.success ? 'success' : 'danger'" size="small">阶段{{ evt.phase }}</el-tag>
|
||||
<span>{{ evt.name }}</span>
|
||||
<span style="color:#909399;font-size:12px">({{ evt.role }})</span>
|
||||
<span v-if="evt.iterations" style="color:#909399;font-size:12px">{{ evt.iterations }}次迭代</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="output-content" v-html="renderMarkdown(evt.output || '')" />
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'qa_done'">
|
||||
<el-collapse>
|
||||
<el-collapse-item>
|
||||
<template #title>
|
||||
<el-tag :type="evt.success ? 'success' : 'info'" size="small">QA审查</el-tag>
|
||||
</template>
|
||||
<div class="output-content" v-html="renderMarkdown(evt.output || '')" />
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'final'">
|
||||
<el-alert type="success" :closable="false">
|
||||
项目交付完成!共 {{ evt.phase_count }} 个阶段
|
||||
</el-alert>
|
||||
</template>
|
||||
<template v-else-if="evt.type === 'error'">
|
||||
<el-alert type="error" :closable="false">{{ evt.content }}</el-alert>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流式完成后显示最终交付物 -->
|
||||
<div v-if="streamDeliverable" style="margin-top:12px">
|
||||
<el-divider />
|
||||
<h4 style="margin:0 0 8px 0">最终交付物</h4>
|
||||
<div class="output-content" v-html="renderMarkdown(streamDeliverable)" />
|
||||
</div>
|
||||
|
||||
<!-- 流式项目文件路径 -->
|
||||
<div v-if="streamProjectPath" style="margin-top:12px">
|
||||
<el-divider />
|
||||
<h4 style="margin:0 0 8px 0">项目文件</h4>
|
||||
<div style="margin-bottom:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="font-weight:600;white-space:nowrap">保存路径:</span>
|
||||
<el-input :model-value="streamProjectPath" readonly size="small" style="flex:1;min-width:300px" />
|
||||
<el-button size="small" @click="copyPath(streamProjectPath)">复制</el-button>
|
||||
</div>
|
||||
<div v-if="streamProjectFiles.length">
|
||||
<span style="font-weight:600;font-size:13px">产出文件 ({{ streamProjectFiles.length }}):</span>
|
||||
<ul style="margin-top:4px;padding-left:20px">
|
||||
<li v-for="f in streamProjectFiles" :key="f" style="font-size:13px;color:#409eff;margin-bottom:4px;word-break:break-all">{{ f }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-empty v-else description="暂无产出文件" :image-size="48" />
|
||||
</div>
|
||||
|
||||
<!-- 同步结果 -->
|
||||
<div v-if="executeResult" class="sync-result">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="项目计划">
|
||||
<pre class="plan-json">{{ JSON.stringify(executeResult.plan, null, 2) }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-for="p in executeResult.phases" :key="p.phase" :label="'阶段' + p.phase + ': ' + p.name">
|
||||
<div class="phase-output">
|
||||
<el-tag :type="p.success ? 'success' : 'danger'" style="margin-bottom: 8px">
|
||||
{{ p.role }} — {{ p.agent_name }}
|
||||
</el-tag>
|
||||
<div class="output-content" v-html="renderMarkdown(p.output)" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="executeResult.qa_review" label="QA审查">
|
||||
<pre class="plan-json">{{ JSON.stringify(executeResult.qa_review, null, 2) }}</pre>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="最终交付物">
|
||||
<div class="output-content" v-html="renderMarkdown(executeResult.final_deliverable)" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane v-if="executeResult.project_path" label="项目文件">
|
||||
<div style="margin-bottom:12px">
|
||||
<span style="font-weight:600;margin-right:8px">保存路径:</span>
|
||||
<el-input
|
||||
:model-value="executeResult.project_path"
|
||||
readonly
|
||||
size="small"
|
||||
style="width:500px;max-width:100%"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyPath(executeResult.project_path)">复制</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div v-if="executeResult.files?.length">
|
||||
<span style="font-weight:600">产出文件 ({{ executeResult.files.length }}):</span>
|
||||
<ul style="margin-top:8px;padding-left:20px">
|
||||
<li v-for="f in executeResult.files" :key="f" style="font-size:13px;color:#409eff;margin-bottom:4px;word-break:break-all">{{ f }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<el-empty v-else description="暂无产出文件" :image-size="48" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import {
|
||||
Check, MagicStick, Refresh, Plus, VideoPlay, Connection, Loading,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
listTeams, getTeam, createTeam, updateTeam, createSoftwareCompanyTemplate,
|
||||
createEducationTrainingTemplate, createPlatformEngineeringTemplate, addMember, removeMember, executeProject, getPresetRoles,
|
||||
} from '@/api/teams'
|
||||
import api from '@/api'
|
||||
import { marked } from 'marked'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
agent_type?: string
|
||||
status?: string
|
||||
workflow_config?: any
|
||||
}
|
||||
|
||||
interface RoleSlot {
|
||||
key: string
|
||||
label: string
|
||||
icon: string
|
||||
description: string
|
||||
}
|
||||
|
||||
// 团队
|
||||
const teamName = ref('我的软件团队')
|
||||
const selectedTeamId = ref<string | null>(null)
|
||||
const currentTeamId = ref<string | null>(null)
|
||||
const teams = ref<any[]>([])
|
||||
const saving = ref(false)
|
||||
const creatingTemplate = ref(false)
|
||||
const creatingEduTemplate = ref(false)
|
||||
const creatingPlatformTemplate = ref(false)
|
||||
const loadingTeams = ref(false)
|
||||
|
||||
// Agent
|
||||
const agents = ref<Agent[]>([])
|
||||
const agentSearch = ref('')
|
||||
const selectedAgent = ref<Agent | null>(null)
|
||||
|
||||
// 角色槽位
|
||||
const roles = ref<RoleSlot[]>([])
|
||||
const roleSlots = ref<Record<string, any>>({})
|
||||
|
||||
// 拖拽
|
||||
const dragOverRole = ref<string | null>(null)
|
||||
let draggingAgent: Agent | null = null
|
||||
|
||||
// 项目执行
|
||||
const projectDescription = ref('')
|
||||
const executing = ref(false)
|
||||
const executingStream = ref(false)
|
||||
const executeResult = ref<any>(null)
|
||||
const streamEvents = ref<any[]>([])
|
||||
const streamDeliverable = ref<string>('')
|
||||
const streamProjectPath = ref<string>('')
|
||||
const streamProjectFiles = ref<string[]>([])
|
||||
const autoApproveFiles = ref(true)
|
||||
|
||||
const hasAnyRole = computed(() => Object.values(roleSlots.value).some(v => !!v))
|
||||
|
||||
// 过滤 Agent
|
||||
const filteredAgents = computed(() => {
|
||||
if (!agentSearch.value) return agents.value
|
||||
const kw = agentSearch.value.toLowerCase()
|
||||
return agents.value.filter(a => a.name.toLowerCase().includes(kw))
|
||||
})
|
||||
|
||||
// 加载预置角色
|
||||
async function loadRoles() {
|
||||
try {
|
||||
const res = await getPresetRoles()
|
||||
const roleMap = res.data?.roles || {}
|
||||
roles.value = Object.entries(roleMap).map(([key, info]: [string, any]) => ({
|
||||
key,
|
||||
label: info.label,
|
||||
icon: info.icon,
|
||||
description: info.description,
|
||||
}))
|
||||
} catch (e) {
|
||||
// 使用默认角色
|
||||
roles.value = [
|
||||
{ key: 'pm', label: '项目经理', icon: '📋', description: '分析需求、分解任务、制定计划、协调团队' },
|
||||
{ key: 'designer', label: 'UI/UX设计师', icon: '🎨', description: '设计用户流程、线框图、组件层级、视觉规范' },
|
||||
{ key: 'developer', label: '开发工程师', icon: '💻', description: '编写后端API、前端组件、业务逻辑、数据库设计' },
|
||||
{ key: 'qa', label: '测试工程师', icon: '🔍', description: '审查交付物、发现缺陷、验证验收标准、质量把关' },
|
||||
{ key: 'devops', label: 'DevOps工程师', icon: '🚀', description: '部署配置、CI/CD、环境管理、健康检查' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 Agent 列表
|
||||
async function loadAgents() {
|
||||
try {
|
||||
const res = await api.get('/api/v1/agents', { params: { page_size: 200 } })
|
||||
agents.value = (Array.isArray(res.data) ? res.data : (res.data?.data || res.data?.items || [])) as Agent[]
|
||||
} catch (e) {
|
||||
console.error('加载 Agent 列表失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载团队列表
|
||||
async function loadTeams() {
|
||||
loadingTeams.value = true
|
||||
try {
|
||||
const res = await listTeams()
|
||||
teams.value = (res.data?.data || []) as any[]
|
||||
} catch (e) {
|
||||
console.error('加载团队列表失败:', e)
|
||||
} finally {
|
||||
loadingTeams.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载团队详情
|
||||
async function handleLoadTeam(teamId: string | null) {
|
||||
if (!teamId) {
|
||||
resetSlots()
|
||||
currentTeamId.value = null
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getTeam(teamId)
|
||||
const team = res.data?.data
|
||||
if (team) {
|
||||
teamName.value = team.name
|
||||
currentTeamId.value = team.id
|
||||
const slots: Record<string, any> = {}
|
||||
if (team.members) {
|
||||
for (const m of team.members) {
|
||||
// 找到对应的 Agent
|
||||
const agent = agents.value.find(a => a.id === m.agent_id)
|
||||
if (agent) {
|
||||
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
roleSlots.value = slots
|
||||
ElMessage.success(`已加载团队: ${team.name}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载团队失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存团队
|
||||
async function handleSaveTeam() {
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = { name: teamName.value }
|
||||
if (currentTeamId.value) {
|
||||
await updateTeam(currentTeamId.value, payload)
|
||||
} else {
|
||||
const res = await createTeam(payload)
|
||||
currentTeamId.value = (res.data as any)?.data?.id
|
||||
}
|
||||
|
||||
// 同步成员
|
||||
if (currentTeamId.value) {
|
||||
for (const [role, agent] of Object.entries(roleSlots.value)) {
|
||||
if (agent) {
|
||||
await addMember(currentTeamId.value, {
|
||||
agent_id: (agent as any).id,
|
||||
role,
|
||||
position: roles.value.findIndex(r => r.key === role),
|
||||
is_lead: (agent as any).is_lead || false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
ElMessage.success('团队已保存')
|
||||
await loadTeams()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 一键创建软件公司模板
|
||||
async function handleCreateTemplate() {
|
||||
creatingTemplate.value = true
|
||||
try {
|
||||
const res = await createSoftwareCompanyTemplate()
|
||||
const team = (res.data as any)?.data
|
||||
if (team) {
|
||||
currentTeamId.value = team.id
|
||||
teamName.value = team.name
|
||||
// 重新加载 agents(因为创建了新 agent)
|
||||
await loadAgents()
|
||||
// 映射成员到槽位
|
||||
const slots: Record<string, any> = {}
|
||||
if (team.members) {
|
||||
for (const m of team.members) {
|
||||
const agent = agents.value.find(a => a.id === m.agent_id)
|
||||
if (agent) {
|
||||
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
roleSlots.value = slots
|
||||
ElMessage.success('软件公司虚拟团队创建成功!')
|
||||
}
|
||||
await loadTeams()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
|
||||
} finally {
|
||||
creatingTemplate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 一键创建教育培训模板
|
||||
async function handleCreateEducationTemplate() {
|
||||
creatingEduTemplate.value = true
|
||||
try {
|
||||
const res = await createEducationTrainingTemplate()
|
||||
const team = (res.data as any)?.data
|
||||
if (team) {
|
||||
currentTeamId.value = team.id
|
||||
teamName.value = team.name
|
||||
await loadAgents()
|
||||
const slots: Record<string, any> = {}
|
||||
if (team.members) {
|
||||
for (const m of team.members) {
|
||||
const agent = agents.value.find(a => a.id === m.agent_id)
|
||||
if (agent) {
|
||||
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
roleSlots.value = slots
|
||||
ElMessage.success('教育培训团队创建成功!')
|
||||
}
|
||||
await loadTeams()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
|
||||
} finally {
|
||||
creatingEduTemplate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 一键创建天工平台工程团队模板
|
||||
async function handleCreatePlatformTemplate() {
|
||||
creatingPlatformTemplate.value = true
|
||||
try {
|
||||
const res = await createPlatformEngineeringTemplate()
|
||||
const team = (res.data as any)?.data
|
||||
if (team) {
|
||||
currentTeamId.value = team.id
|
||||
teamName.value = team.name
|
||||
await loadAgents()
|
||||
const slots: Record<string, any> = {}
|
||||
if (team.members) {
|
||||
for (const m of team.members) {
|
||||
const agent = agents.value.find(a => a.id === m.agent_id)
|
||||
if (agent) {
|
||||
slots[m.role] = { ...agent, is_lead: m.is_lead, member_id: m.id }
|
||||
}
|
||||
}
|
||||
}
|
||||
roleSlots.value = slots
|
||||
ElMessage.success('天工平台工程团队创建成功!')
|
||||
}
|
||||
await loadTeams()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '创建模板失败')
|
||||
} finally {
|
||||
creatingPlatformTemplate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 拖拽
|
||||
function onDragStart(agent: Agent) {
|
||||
draggingAgent = agent
|
||||
}
|
||||
|
||||
function onDrop(role: string) {
|
||||
if (draggingAgent) {
|
||||
roleSlots.value[role] = draggingAgent
|
||||
dragOverRole.value = null
|
||||
draggingAgent = null
|
||||
}
|
||||
}
|
||||
|
||||
function removeFromSlot(role: string) {
|
||||
delete roleSlots.value[role]
|
||||
}
|
||||
|
||||
function resetSlots() {
|
||||
roleSlots.value = {}
|
||||
}
|
||||
|
||||
// 执行项目
|
||||
async function handleExecute(mode: 'sync' | 'stream') {
|
||||
if (!currentTeamId.value) {
|
||||
ElMessage.warning('请先保存团队')
|
||||
return
|
||||
}
|
||||
if (!projectDescription.value) {
|
||||
ElMessage.warning('请输入项目描述')
|
||||
return
|
||||
}
|
||||
|
||||
if (mode === 'sync') {
|
||||
executing.value = true
|
||||
executeResult.value = null
|
||||
streamDeliverable.value = ''
|
||||
streamProjectPath.value = ''
|
||||
streamProjectFiles.value = []
|
||||
try {
|
||||
const res = await executeProject(currentTeamId.value, projectDescription.value, autoApproveFiles.value)
|
||||
executeResult.value = (res.data as any)?.data
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '执行失败')
|
||||
} finally {
|
||||
executing.value = false
|
||||
}
|
||||
} else {
|
||||
executingStream.value = true
|
||||
streamEvents.value = []
|
||||
streamDeliverable.value = ''
|
||||
streamProjectPath.value = ''
|
||||
streamProjectFiles.value = []
|
||||
executeResult.value = null
|
||||
|
||||
const token = localStorage.getItem('token') || ''
|
||||
const baseUrl = (api.defaults.baseURL || '').replace(/\/+$/, '')
|
||||
const url = `${baseUrl}/api/v1/teams/${currentTeamId.value}/execute/stream`
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ project_description: projectDescription.value, auto_approve_files: autoApproveFiles.value }),
|
||||
})
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) throw new Error('无法读取流')
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ''
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const evt = JSON.parse(line.slice(6))
|
||||
streamEvents.value.push(evt)
|
||||
if (evt.type === 'final') {
|
||||
if (evt.deliverable) streamDeliverable.value = evt.deliverable
|
||||
if (evt.project_path) streamProjectPath.value = evt.project_path
|
||||
if (evt.files) streamProjectFiles.value = evt.files
|
||||
}
|
||||
if (evt.type === 'plan_start' && evt.project_path) {
|
||||
streamProjectPath.value = evt.project_path
|
||||
}
|
||||
} catch { /* ignore parse errors */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
streamEvents.value.push({ type: 'error', content: e.message })
|
||||
} finally {
|
||||
executingStream.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Markdown 渲染
|
||||
function renderMarkdown(text: string): string {
|
||||
if (!text) return ''
|
||||
try {
|
||||
return marked.parse(text) as string
|
||||
} catch {
|
||||
return text.replace(/\n/g, '<br>')
|
||||
}
|
||||
}
|
||||
|
||||
function onTeamNameChange() {
|
||||
// 仅更新本地状态
|
||||
}
|
||||
|
||||
async function copyPath(path: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(path)
|
||||
ElMessage.success('路径已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.warning('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadRoles()
|
||||
loadAgents()
|
||||
loadTeams()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-builder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
width: 240px;
|
||||
min-width: 200px;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agent-item {
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.agent-item:hover {
|
||||
background: #ecf5ff;
|
||||
border-color: #c6e2ff;
|
||||
}
|
||||
|
||||
.agent-item.active {
|
||||
background: #d9ecff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.center-panel {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.role-slots {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.role-card {
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 140px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.role-card.filled {
|
||||
border-color: #67c23a;
|
||||
border-style: solid;
|
||||
background: #f0f9eb;
|
||||
}
|
||||
|
||||
.role-card.drag-over {
|
||||
border-color: #409eff;
|
||||
background: #ecf5ff;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.role-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.role-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.role-label {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.role-desc {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.role-assigned {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.role-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #c0c4cc;
|
||||
font-size: 12px;
|
||||
gap: 4px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
width: 260px;
|
||||
min-width: 220px;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bottom-area {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.project-input {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.project-input .el-textarea {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.result-area {
|
||||
margin-top: 12px;
|
||||
border-top: 1px solid #ebeef5;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-header h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.stream-events {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sync-result {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.plan-json {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.phase-output {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.output-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.output-content :deep(pre) {
|
||||
background: #f5f7fa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.output-content :deep(code) {
|
||||
background: #f5f7fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.phase-start {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
background: #ecf5ff;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user