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:
renjianbo
2026-06-17 00:09:54 +08:00
parent 7f4aeb021b
commit f612248f9e
8 changed files with 4046 additions and 0 deletions

299
backend/app/api/teams.py Normal file
View 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",
},
)

View 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),
}

File diff suppressed because it is too large Load Diff

View 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
> 基于「软件公司」「教育培训」「天工平台工程」三个模板的实现经验

View 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
View 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
View 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
}

View 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>