Files
aiagent/backend/app/services/team_orchestrator.py
renjianbo beff3fac8d fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions,
  schedules, executions, team_members) and unbind goals/tasks before delete
- Remove hardcoded personality templates in Android, replace with dynamic
  system prompt generation from name + description
- Set promptSectionsEnabled=false to bypass PromptComposer for personality
- Add Tencent Cloud Linux deployment guide (Docker Compose)
- Accumulated backend service updates, frontend UI fixes, Android app changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-29 01:17:21 +08:00

1806 lines
80 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
团队编排引擎 — 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 asyncio
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__)
# 项目扫描器(共享模块,也供 builtin_tools 使用)
from app.services.project_scanner import (
identify_project_type,
scan_source_code,
scan_multiple_dirs,
)
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 计划(含 Markdown 表格回退)。"""
text = raw_output.strip()
# 尝试直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 从 ```json ... ``` 中提取
m = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', text)
if m:
try:
return json.loads(m.group(1))
except json.JSONDecodeError:
pass
# 从 { ... } 中提取(含 phases 键)
m = re.search(r'\{[\s\S]*"phases"[\s\S]*\}', text)
if m:
try:
return json.loads(m.group())
except json.JSONDecodeError:
pass
# ─── 回退:从 Markdown 表格提取 phases ───
# 匹配形如 | **phase-1** | `functional_tester` | ... | 的行
phases = _parse_phases_from_markdown(text)
if phases:
project_name = ""
m = re.search(r'(?:项目|测试).*?[:]\s*(.+?)(?:\n|$)', text)
if m:
project_name = m.group(1).strip()
return {
"project_name": project_name or "未命名项目",
"analysis": "",
"user_stories": [],
"phases": phases,
"acceptance_criteria": [],
}
logger.warning("无法从输出中解析 JSON 计划,原始输出: %.500s", text)
return {}
def _extract_paths_from_description(text: str) -> List[Path]:
"""从自然语言项目描述中提取存在的目录路径。
支持 Windows (D:\\... C:/...) 和 Unix (/home/...) 路径格式。
返回去重后的存在路径列表(仅保留目录,排除文件路径)。
"""
# 匹配绝对路径模式:盘符路径 / Unix绝对路径
candidates = re.findall(
r'(?:[A-Za-z]:[\\/][^\s,;\n]+|/[^\s,;\n]+)',
text
)
found: set = set()
for c in candidates:
# 规范化路径
p = Path(c)
try:
resolved = p.resolve()
except (OSError, ValueError):
continue
# 如果路径指向文件,取其父目录
if resolved.is_file():
resolved = resolved.parent
if resolved.is_dir() and resolved not in found:
found.add(resolved)
return list(found)
def _get_default_scan_paths() -> List[Path]:
"""当项目描述中未提取到路径时,返回默认的扫描目录。
默认扫描 workspace 下的 frontend/src 和 backend/app
若都不存在则回退到 workspace 根目录。
"""
root = _resolve_workspace_root()
candidates = [
root / "frontend" / "src",
root / "backend" / "app",
]
existing = [p for p in candidates if p.is_dir()]
return existing if existing else [root]
def _validate_and_fix_dependencies(phases: List[Dict[str, Any]]) -> int:
"""验证并修复阶段的 depends_on 引用。
- 移除对不存在阶段号的引用
- 移除自身引用phase 3 depends_on [3]
- 降级 forward 依赖为顺序暗示warning 但不阻断)
Returns:
修复的次数0 表示无问题)
"""
valid_numbers = {p["phase"] for p in phases}
fixes = 0
for p in phases:
deps = p.get("depends_on", [])
if not deps:
continue
fixed = []
for d in deps:
if d not in valid_numbers:
logger.warning(
"阶段 %d (%s): depends_on 引用了不存在的阶段 %d,已移除",
p["phase"], p.get("name", ""), d,
)
fixes += 1
elif d == p["phase"]:
logger.warning(
"阶段 %d (%s): depends_on 引用自身,已移除",
p["phase"], p.get("name", ""),
)
fixes += 1
elif d > p["phase"]:
logger.warning(
"阶段 %d (%s): depends_on=[%d] 是 forward 依赖(依赖后续阶段),"
"可能导致死锁,已移除",
p["phase"], p.get("name", ""), d,
)
fixes += 1
else:
fixed.append(d)
if len(fixed) != len(deps):
p["depends_on"] = fixed
return fixes
def _parse_depends_on(raw: str) -> List[int]:
"""解析 depends_on 列的值,支持 []、[1]、[1,2]、[1, 2] 等格式。返回 int 列表。"""
raw = raw.strip()
if not raw or raw == '[]':
return []
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return [int(x) for x in parsed]
except (json.JSONDecodeError, ValueError, TypeError):
pass
# 兼容非 JSON 写法: "1,2" 或 "1, 2"
nums = re.findall(r'\d+', raw)
if nums:
return [int(n) for n in nums]
return []
def _parse_phases_from_markdown(text: str) -> List[Dict[str, Any]]:
"""从 Markdown 表格中提取 phase 信息。支持格式:
| **phase-1** | `functional_tester` | 核心任务 | 工时 |
| phase-1 | functional_tester | 描述 |
| phase-1 | functional_tester | 描述 | [1,2] | ← 可选的 depends_on 列
"""
phases = []
# 匹配表格行: | (可选**的) phase-N (可选**) | role | description | [可选 depends_on] |
# role 可能带反引号 `role` 或直接是文本
table_row = re.compile(
r'^\|\s*\*{0,2}(?:phase[-]?\s*)?(\d+)\*{0,2}\s*\|'
r'\s*(?:`)?([a-z_]+)(?:`)?\s*\|'
r'\s*([^|]+?)\s*\|'
r'(?:\s*([^|]*?)\s*\|)?',
re.MULTILINE | re.IGNORECASE
)
for m in table_row.finditer(text):
phase_num = int(m.group(1))
role = m.group(2).strip().lower()
desc = m.group(3).strip()
depends_raw = (m.group(4) or '').strip() if m.lastindex and m.lastindex >= 4 else ''
# 验证 role 是合法角色名
if role not in ("test_planner", "functional_tester", "ux_reviewer", "edge_explorer",
"architect",
"performance_evaluator", "pm", "designer", "developer", "qa", "devops",
"curriculum_designer", "instructor", "teaching_assistant", "academic_admin",
"fullstack_dev", "fe_ux_engineer", "platform_devops", "qa_engineer", "product_lead",
"doc_architect", "tech_writer", "api_doc_specialist", "translator_reviewer",
"release_manager", "health_assessor", "nutritionist", "exercise_rehab",
"psychologist", "chronic_manager", "triage_specialist", "record_analyst",
"medication_reviewer", "followup_manager", "insurance_coordinator"):
continue
phases.append({
"phase": phase_num,
"name": desc[:60],
"role": role,
"description": desc,
"expected_output": f"{role} 阶段的产出物",
"depends_on": _parse_depends_on(depends_raw),
})
if phases:
logger.info("从 Markdown 表格提取到 %d 个 phases", len(phases))
return phases
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",
"tech_doc": "doc_architect",
"health_management": "health_assessor",
"medical_consultation": "triage_specialist",
"user_simulation_test": "test_planner",
}
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"
def _build_phase_input(
self,
phase: Dict[str, Any],
context_doc: str,
project_path: Path,
all_files: List[str],
dependency_outputs: Dict[int, str],
pre_scan_result: str = "",
) -> str:
"""为阶段构建结构化上下文输入。
包含:预扫描源码 + 前置阶段摘要 + 直接依赖的完整产出 + 文件清单 + 阶段任务说明。
"""
phase_name = phase.get("name", "")
phase_desc = phase.get("description", "")
expected = phase.get("expected_output", "")
deps = phase.get("depends_on", [])
parts = [context_doc]
# 预扫描源码(真实代码,非设计文档)
if pre_scan_result:
parts.append("\n## 📋 项目源码预扫描(真实代码)\n")
parts.append(pre_scan_result)
parts.append(
"\n> **重要提示**: 以上是项目实际源代码的预扫描结果。"
"请基于这些真实代码进行分析和审查,而非基于设计文档或推测。"
"引用具体文件路径、类名、方法名来支撑你的结论。\n"
)
# 直接依赖阶段的完整产出(非截断)
if deps and dependency_outputs:
parts.append("\n## 📎 直接依赖阶段的完整产出\n")
for dep_num in sorted(deps):
if dep_num in dependency_outputs:
dep_text = dependency_outputs[dep_num]
parts.append(f"### 阶段 {dep_num} 产出\n{dep_text[:8000]}\n")
# 当前工作目录文件清单
if all_files:
recent = sorted(all_files)[-60:]
parts.append(f"\n## 📁 当前项目文件 ({len(all_files)} 个已有文件)\n")
for fp in recent:
parts.append(f"- {fp}")
if len(all_files) > 60:
parts.append(f"- ... 还有 {len(all_files) - 60} 个文件未列出\n")
# 当前阶段任务
parts.append(f"\n## 🎯 当前阶段: {phase_name}\n")
parts.append(f"任务描述: {phase_desc}\n")
parts.append(f"期望产出: {expected}\n")
parts.append(f"项目文件保存目录: {project_path}\n")
parts.append("请将所有产出文件写入此目录,完成此阶段的工作并输出你的交付物。")
return "\n".join(parts)
async def _run_single_phase(
self,
phase: Dict[str, Any],
members: List[TeamMember],
context_doc: str,
project_path: Path,
prev_files: set,
all_files: Optional[List[str]] = None,
dependency_outputs: Optional[Dict[int, str]] = None,
pre_scan_result: str = "",
) -> Dict[str, Any]:
"""执行单个 phase可被 asyncio.gather 并发调用)。
每个 phase 创建独立的 AgentRuntime互不干扰。
支持 self_review 自优化agent 完成初稿后自动复盘改进。
"""
phase_num = phase.get("phase", 0)
role = phase.get("role", "developer")
phase_name = phase.get("name", f"阶段 {phase_num}")
phase_desc = phase.get("description", "")
expected = phase.get("expected_output", "")
self_review = phase.get("self_review", False)
# DB 查询在协程内完成members 已预加载,此处仅内存迭代)
agent = None
for m in members:
if m.role == role:
agent = m.agent
break
if not agent:
logger.warning("团队编排 [%s]: 阶段 %s 缺少角色 '%s',跳过",
self.team_id, phase_num, role)
return {
"phase": phase_num, "name": phase_name, "role": role,
"agent_name": None,
"output": f"跳过: 无 {role} 角色",
"success": False, "error": f"missing_role:{role}",
"files": [],
}
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 = self._build_phase_input(
phase, context_doc, project_path,
all_files or [], dependency_outputs or {},
pre_scan_result=pre_scan_result,
)
phase_result = await runtime.run(phase_input)
# ─── P2-2: Self-review 自优化 ───
if self_review and phase_result.success:
logger.info("团队编排 [%s]: 阶段 %s 进入自优化复盘",
self.team_id, phase_num)
review_input = (
f"你刚完成了以下任务:\n\n{phase_result.content[:3000]}\n\n"
f"现在请以审查者的视角审视你的产出:\n"
f"1. 是否有遗漏的功能或边界情况?\n"
f"2. 代码质量是否可以改进(命名、结构、注释)?\n"
f"3. 是否与现有项目风格一致?\n"
f"4. 是否有未处理的错误场景?\n\n"
f"请优化你的产出并输出最终版本。如果已经很好,直接输出原内容。"
)
review_result = await runtime.run(review_input)
if review_result.success and review_result.content:
phase_result = review_result
logger.info("团队编排 [%s]: 阶段 %s 自优化完成",
self.team_id, phase_num)
output_text = phase_result.content if phase_result.success else (
phase_result.error or "执行失败"
)
# 提取本阶段写入的文件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)
return {
"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)
return {
"phase": phase_num, "name": phase_name, "role": role,
"agent_name": agent.name,
"output": f"执行异常: {e}",
"success": False, "error": str(e),
"files": [],
}
async def _run_phase_stream_and_collect(
self,
phase: Dict[str, Any],
members: List[TeamMember],
context_doc: str,
project_path: Path,
prev_files: set,
event_queue: asyncio.Queue,
all_files: Optional[List[str]] = None,
dependency_outputs: Optional[Dict[int, str]] = None,
pre_scan_result: str = "",
) -> Dict[str, Any]:
"""流式执行单个 phase —— 用 run_stream() 替代 run()。
将 Agent 内部事件think/tool_call/tool_result/final实时放入 event_queue
前端可据此渲染每个 Agent 的独立执行面板。
Returns:
最终结果字典(与 _run_single_phase 相同结构)
"""
phase_num = phase.get("phase", 0)
role = phase.get("role", "developer")
phase_name = phase.get("name", f"阶段 {phase_num}")
agent = None
for m in members:
if m.role == role:
agent = m.agent
break
base_event = {
"phase": phase_num,
"role": role,
"name": phase_name,
"agent": agent.name if agent else None,
}
if not agent:
event_queue.put_nowait({
**base_event,
"type": "phase_done",
"success": False,
"error": f"missing_role:{role}",
"output": f"跳过: 无 {role} 角色",
"files": [],
})
return {
"phase": phase_num, "name": phase_name, "role": role,
"agent_name": None,
"output": f"跳过: 无 {role} 角色",
"success": False, "error": f"missing_role:{role}",
"files": [],
}
try:
agent_config = _build_agent_config(agent, self.auto_approve_files)
runtime = AgentRuntime(agent_config)
phase_input = self._build_phase_input(
phase, context_doc, project_path,
all_files or [], dependency_outputs or {},
pre_scan_result=pre_scan_result,
)
output_parts: List[str] = []
last_iteration = 0
tool_count = 0
async for agent_event in runtime.run_stream(phase_input):
event_type = agent_event.get("type", "")
# 转发 Agent 内部事件到前端(带 phase 元数据)
event_queue.put_nowait({
**base_event,
"type": "agent_event",
"data": agent_event,
})
if event_type == "final":
content = agent_event.get("content", "")
if content:
output_parts.append(content)
elif event_type == "tool_result":
tool_count += 1
last_iteration = agent_event.get("iteration", last_iteration)
output_text = "\n\n".join(output_parts) if output_parts else "执行完成"
# ─── P2-2: Self-review 自优化 ───
self_review = phase.get("self_review", False)
if self_review and output_parts:
review_event = {
**base_event,
"type": "agent_event",
"data": {"type": "think", "content": "🔄 进入自优化复盘..."},
}
event_queue.put_nowait(review_event)
review_input = (
f"你刚完成了以下任务:\n\n{output_text[:3000]}\n\n"
f"现在请以审查者的视角审视你的产出:\n"
f"1. 是否有遗漏的功能或边界情况?\n"
f"2. 代码质量是否可以改进(命名、结构、注释)?\n"
f"3. 是否与现有项目风格一致?\n"
f"4. 是否有未处理的错误场景?\n\n"
f"请优化你的产出并输出最终版本。如果已经很好,直接输出原内容。"
)
review_parts: List[str] = []
async for agent_event in runtime.run_stream(review_input):
event_queue.put_nowait({
**base_event,
"type": "agent_event",
"data": agent_event,
})
if agent_event.get("type") == "final":
content = agent_event.get("content", "")
if content:
review_parts.append(content)
if review_parts:
output_text = "\n\n".join(review_parts)
logger.info("团队编排 [%s]: 阶段 %s 流式自优化完成",
self.team_id, phase_num)
# 提取本阶段写入的文件
current_files = set(_scan_directory_files(project_path))
phase_files = sorted(current_files - prev_files)
result = {
"phase": phase_num, "name": phase_name, "role": role,
"agent_name": agent.name,
"output": output_text,
"success": True,
"iterations": last_iteration,
"tool_calls": tool_count,
"error": None,
"files": phase_files,
}
event_queue.put_nowait({
**base_event,
"type": "phase_done",
"output": output_text,
"success": True,
"iterations": last_iteration,
"tool_calls": tool_count,
"files": phase_files,
"error": None,
})
return result
except Exception as e:
logger.error("团队编排 [%s]: 阶段 %s 流式异常: %s", self.team_id, phase_num, e)
result = {
"phase": phase_num, "name": phase_name, "role": role,
"agent_name": agent.name,
"output": f"执行异常: {e}",
"success": False, "error": str(e),
"files": [],
}
event_queue.put_nowait({
**base_event,
"type": "phase_done",
"success": False,
"error": str(e),
"output": f"执行异常: {e}",
"files": [],
})
return result
async def _execute_phases_parallel(
self,
phases: List[Dict[str, Any]],
members: List[TeamMember],
context_doc: str,
project_path: Path,
prev_files: set,
pre_scan_result: str = "",
):
"""DAG 并行执行 phases。按 depends_on 拓扑分批,批内 asyncio.gather 并发。
Returns:
(phase_results, updated_context_doc, updated_prev_files, all_files)
"""
results: List[Dict[str, Any]] = []
completed: Dict[int, str] = {} # phase_num → output_text
pending = {p["phase"]: p for p in phases}
current_prev = prev_files.copy()
current_context = context_doc
all_files: List[str] = []
while pending:
# 找出所有依赖已满足的阶段
ready = [
p for p in pending.values()
if all(d in completed for d in p.get("depends_on", []))
]
if not ready:
raise RuntimeError(
f"阶段循环依赖,无法继续。已完成: {list(completed.keys())}, "
f"待处理: {list(pending.keys())}"
)
logger.info("团队编排 [%s]: 并行批次 — %d 个阶段: %s",
self.team_id, len(ready),
[(p["phase"], p.get("name", "")) for p in ready])
# 并行执行所有就绪阶段
tasks = [
self._run_single_phase(
p, members, current_context, project_path, current_prev,
all_files=list(all_files), dependency_outputs=completed.copy(),
pre_scan_result=pre_scan_result,
)
for p in ready
]
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
for p, r in zip(ready, batch_results):
if isinstance(r, Exception):
r = {
"phase": p["phase"], "name": p.get("name", ""),
"role": p.get("role", ""),
"agent_name": None,
"output": f"并行执行异常: {r}",
"success": False, "error": str(r),
"files": [],
}
# 记录完成
completed[p["phase"]] = r.get("output", "")
# 更新共享上下文(后续批次可见)
current_context += (
f"\n## 阶段 {p['phase']}: {p.get('name', '')}\n"
f"{r.get('output', '')[:2000]}\n"
)
# 更新文件追踪
for fp in r.get("files", []):
if fp not in all_files:
all_files.append(fp)
current_prev.add(fp)
results.append(r)
del pending[p["phase"]]
return results, current_context, current_prev, all_files
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: 架构分析Architect 扫描现有项目) ───
enhanced_description = project_description
pre_scan_result = "" # 预扫描源码,后续注入到各执行阶段的 prompt
architect_agent = self._get_agent_by_role(members, "architect")
if architect_agent:
target_paths = _extract_paths_from_description(project_description)
if not target_paths:
target_paths = _get_default_scan_paths()
logger.info("团队编排 [%s]: 未从描述中提取路径,使用默认扫描路径: %s",
self.team_id, [str(p) for p in target_paths])
logger.info("团队编排 [%s]: Architect 开始扫描 %d 个目标路径",
self.team_id, len(target_paths))
try:
arch_config = _build_agent_config(architect_agent, self.auto_approve_files)
arch_runtime = AgentRuntime(arch_config)
# ─── 程序化预扫描源码 ───
pre_scan_result = scan_multiple_dirs(target_paths)
if pre_scan_result:
logger.info("团队编排 [%s]: 预扫描完成,已提取源文件内容",
self.team_id)
arch_input = (
f"你是一名系统架构师。请基于以下已预扫描的源代码,"
f"输出完整的架构上下文文档供后续测试团队使用。\n\n"
f"任务背景:{project_description}\n\n"
f"{pre_scan_result}\n\n"
f"## 你的任务\n"
f"上述源码清单中已包含关键源文件的完整内容。"
f"请基于这些真实代码(不是设计文档)输出架构分析文档,包含:\n"
f"1. 技术栈清单(框架/库/版本,从实际配置文件确认)\n"
f"2. 目录结构与模块划分\n"
f"3. 数据流路径UI→ViewModel→Repository→API/DAO→DB\n"
f"4. 关键类的职责与依赖关系(引用源码中的实际类名/方法名)\n"
f"5. 从源码中发现的实际代码质量问题\n\n"
f"## 注意\n"
f"- 主要分析基于上述已提供的源码内容\n"
f"- 如需补充阅读特定文件,可以用 file_read 工具\n"
f"- 不要基于设计文档或推测进行分析\n"
f"- 在输出中列出你分析引用了哪些源文件"
)
arch_result = await arch_runtime.run(arch_input)
if arch_result.success and arch_result.content:
arch_analysis = arch_result.content.strip()
enhanced_description = (
f"{project_description}\n\n"
f"## 架构分析(由系统架构师扫描现有项目生成)\n\n"
f"{arch_analysis}"
)
results["architecture_analysis"] = arch_analysis
logger.info("团队编排 [%s]: Architect 分析完成 (%d 字符)",
self.team_id, len(arch_analysis))
except Exception as e:
logger.warning("团队编排 [%s]: Architect 分析异常(继续执行): %s",
self.team_id, e)
# ─── Phase 1: 规划阶段(由 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:
raise ValueError(
f"团队缺少 Leader 角色(需要 role='{planner_role}'),无法分解项目。"
f"可用角色: {[m.role for m in members]}"
)
available_roles = [m.role for m in members]
# 如果架构师已作为前置步骤执行,从可用角色中移除,避免 PM 再次规划架构师阶段
if enhanced_description != project_description and "architect" in available_roles:
available_roles.remove("architect")
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"你是一个团队的项目规划者。请分析以下需求,将工作分解为阶段,并分配给你团队的每个成员。\n\n"
f"需求描述:{enhanced_description}\n"
f"输出文件目录:{project_path}\n\n"
f"你的团队有以下角色:{roles_hint}\n"
f"每个阶段必须分配上述角色之一。\n\n"
f"请严格按照以下 JSON 格式输出执行计划(不要输出任何其他内容,只输出 JSON\n"
f'{{"project_name":"...","analysis":"...","phases":[\n'
f' {{"phase":1,"name":"...","role":"{roles_hint.split("/")[0] if "/" in roles_hint else roles_hint}","description":"...","expected_output":"...","depends_on":[]}},\n'
f' {{"phase":2,"name":"...","role":"...","description":"...","expected_output":"...","depends_on":[]}}\n'
f'],"acceptance_criteria":["..."]}}\n\n'
f"关键要求:\n"
f"1. phases 数组是强制性的——没有它计划无法执行\n"
f"2. 每个 phase 的 role 必须是上述角色之一\n"
f"3. depends_on 列出必须先完成的阶段编号,无依赖则用[]\n"
f"4. 4-7 个阶段最佳\n"
f"5. 用中文输出\n"
f"6. 只输出 JSON不要有任何前言或后记"
)
results["plan_input"] = enhanced_description
planner_result = await planner_runtime.run(planner_input)
plan = _parse_plan_json(planner_result.content) if planner_result.success else {}
results["plan"] = plan
# 回退方案:如果无法从 LLM 回复中解析 JSON尝试读取规划师写入的 execution_plan.json
if (not plan or not plan.get("phases")) and project_path.exists():
plan_file = project_path / "execution_plan.json"
if plan_file.exists():
try:
plan = json.loads(plan_file.read_text(encoding="utf-8"))
results["plan"] = plan
logger.info("团队编排 [%s]: 从文件回退读取计划 %s", self.team_id, plan_file)
except (json.JSONDecodeError, PermissionError, OSError) as e:
logger.warning("团队编排 [%s]: 回退读取计划文件失败: %s", self.team_id, e)
if not plan or not plan.get("phases"):
# 降级方案:规划师失败时使用默认阶段计划,避免整个流程中断
logger.warning(
"团队编排 [%s]: %s 未能生成有效计划使用默认5阶段计划降级执行",
self.team_id, planner_role
)
results["plan_error"] = f"{planner_role} 规划失败,使用默认计划"
# 构建默认计划从可用角色中选5个跳过 architect/test_planner
exec_roles = [r for r in available_roles if r not in ("architect", "test_planner")]
if not exec_roles:
exec_roles = available_roles[:]
default_phases = []
phase_names = {
"functional_tester": "功能测试与缺陷分析",
"ux_reviewer": "UX体验审查与竞品对标",
"edge_explorer": "边界条件与异常路径探索",
"performance_evaluator": "性能内存审计与优化建议",
}
for i, role in enumerate(exec_roles[:5], start=1):
default_phases.append({
"phase": i,
"name": phase_names.get(role, f"{role} 执行阶段"),
"role": role,
"description": f"基于架构文档执行 {role} 阶段",
"expected_output": f"{role} 分析报告",
"depends_on": [],
})
plan = {
"project_name": "自动化测试项目",
"analysis": "(规划师超时,使用默认阶段计划)",
"phases": default_phases,
"acceptance_criteria": ["所有阶段执行完成"],
}
results["plan"] = plan
logger.info(
"团队编排 [%s]: 降级计划生成 %d 个阶段,角色: %s",
self.team_id, len(default_phases),
[p["role"] for p in default_phases]
)
logger.info("团队编排 [%s]: PM 生成 %d 个阶段", self.team_id, len(plan["phases"]))
# ─── Phase 1..N: DAG 并行执行(含顺序回退) ───
all_outputs: List[str] = []
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
phases = plan["phases"]
# 检测是否有 phase 显式指定了 depends_on 以决定是否启用并行
has_dependencies = any(p.get("depends_on") for p in phases)
if has_dependencies:
# 执行前验证并修复依赖关系
fix_count = _validate_and_fix_dependencies(phases)
if fix_count > 0:
logger.warning("团队编排 [%s]: 自动修复了 %d 个无效依赖", self.team_id, fix_count)
# 重新检测是否有依赖(可能全部被移除)
has_dependencies = any(p.get("depends_on") for p in phases)
if has_dependencies:
logger.info("团队编排 [%s]: 使用 DAG 并行模式执行 %d 个阶段", self.team_id, len(phases))
phase_results, context_doc, prev_files, all_files = await self._execute_phases_parallel(
phases, members, context_doc, project_path, prev_files,
pre_scan_result=pre_scan_result,
)
results["phases"] = phase_results
all_outputs = [
f"## {r['name']} ({r['role']})\n{r.get('output', '')}"
for r in phase_results
]
# 更新 success任一阶段失败则整体失败
for r in phase_results:
if not r.get("success"):
results["success"] = False
else:
logger.info("团队编排 [%s]: 依赖修复后回退为顺序执行", self.team_id)
# fall through to sequential execution block below
has_dependencies = False
if not has_dependencies:
# ─── 顺序执行(向后兼容:无 depends_on 的旧版模板) ───
completed_outputs: Dict[int, str] = {}
for i, phase in enumerate(phases):
r = await self._run_single_phase(
phase, members, context_doc, project_path, prev_files,
all_files=list(all_files), dependency_outputs=completed_outputs,
pre_scan_result=pre_scan_result,
)
phase_num = r["phase"]
phase_name = r["name"]
role = r["role"]
output_text = r.get("output", "")
if r["success"]:
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:8000]}\n"
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
completed_outputs[phase_num] = output_text
for fp in r.get("files", []):
if fp not in all_files:
all_files.append(fp)
for fp in r.get("files", []):
prev_files.add(fp)
else:
all_outputs.append(f"[跳过] 阶段 {phase_num} ({phase_name}): {r.get('error', '')}")
results["success"] = False
results["phases"].append(r)
# ─── QA 审查 + 反馈闭环 ───
qa_agent = self._get_agent_by_role(members, "qa")
fix_rounds: List[Dict[str, Any]] = []
final_qa_review: Dict[str, Any] = {}
if qa_agent and all_outputs:
logger.info("团队编排 [%s]: QA 审查开始", self.team_id)
async def _run_qa_review(round_label: str = "") -> tuple:
"""运行一次 QA 审查,返回 (qa_review_dict, raw_output)"""
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
qa_runtime = AgentRuntime(qa_config)
deliverables = "\n\n---\n\n".join(all_outputs)
prefix = f"{round_label}" if round_label else ""
qa_input = (
f"请审查以下项目交付物{prefix}\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 {}
return qa_review, qa_result.content, qa_result.success
try:
qa_review, qa_output, qa_success = await _run_qa_review()
results["qa_review"] = {
"output": qa_output,
"review": qa_review,
"success": qa_success,
}
final_qa_review = qa_review
# ─── QA 反馈闭环:关键/高优先级问题自动修复 ───
# critical 问题强制执行至少一轮修复high 问题最多 2 轮
MAX_FIX_ROUNDS = 3
critical_fixed_in_previous_round = False
for fix_round in range(1, MAX_FIX_ROUNDS + 1):
if final_qa_review.get("pass", False):
break
issues = final_qa_review.get("issues", [])
fixable = [i for i in issues
if i.get("severity") in ("critical", "high")]
has_critical = any(
i.get("severity") == "critical" for i in issues
)
if not fixable:
logger.info("团队编排 [%s]: QA 无 critical/high 问题,跳过修复循环",
self.team_id)
break
# critical 问题第1轮强制执行不可跳过
if has_critical and fix_round == 1:
logger.info("团队编排 [%s]: 检测到 critical 问题,强制执行修复",
self.team_id)
dev_role = "developer"
dev_agent = self._get_agent_by_role(members, dev_role)
if not dev_agent:
dev_role = "fullstack_dev"
dev_agent = self._get_agent_by_role(members, dev_role)
if not dev_agent:
logger.warning("团队编排 [%s]: QA 修复循环需要 developer但未找到",
self.team_id)
break
logger.info("团队编排 [%s]: QA 修复第 %d 轮,%d 个待修复问题",
self.team_id, fix_round, len(fixable))
issues_text = "\n\n".join(
f"### 问题 {j+1} [{i['severity']}]\n"
f"**文件**: {i.get('file', 'unknown')}\n"
f"**描述**: {i.get('description', '')}\n"
f"**修复建议**: {i.get('suggestion', '')}"
for j, i in enumerate(fixable)
)
fix_phase = {
"phase": 100 + fix_round,
"name": f"QA反馈修复 (第{fix_round}轮)",
"role": dev_role,
"description": (
f"根据 QA 审查结果修复以下 {len(fixable)} 个问题:\n\n{issues_text}"
),
"expected_output": "修复后的代码文件",
"depends_on": [],
}
fix_result = await self._run_single_phase(
fix_phase, members, context_doc, project_path, prev_files,
pre_scan_result=pre_scan_result,
)
results["phases"].append(fix_result)
if fix_result.get("success"):
fix_output = fix_result.get("output", "")
context_doc += f"\n## QA修复 第{fix_round}\n{fix_output[:2000]}\n"
all_outputs.append(
f"## QA修复 第{fix_round}轮 ({dev_role})\n{fix_output}"
)
for fp in fix_result.get("files", []):
if fp not in all_files:
all_files.append(fp)
prev_files.add(fp)
# 重新 QA
before_score = final_qa_review.get("score", 0)
new_review, new_output, new_success = await _run_qa_review(
f"{fix_round}轮修复后复查"
)
fix_rounds.append({
"round": fix_round,
"before_score": before_score,
"after_score": new_review.get("score", 0),
"fixed_issues": len(fixable),
"qa_output": new_output,
})
final_qa_review = new_review
results["qa_review"]["output"] = new_output
results["qa_review"]["review"] = new_review
results["qa_review"]["success"] = new_success
# 检查本轮修复后是否还有 critical 问题
remaining_critical = any(
i.get("severity") == "critical"
for i in new_review.get("issues", [])
)
# 分数无提升且无 critical 残留 → 终止;有 critical 残留则继续
if new_review.get("score", 0) <= before_score and not remaining_critical:
logger.info("团队编排 [%s]: QA 修复第 %d 轮分数无提升 (%d%d),终止循环",
self.team_id, fix_round, before_score,
new_review.get("score", 0))
break
if remaining_critical:
logger.warning("团队编排 [%s]: 第 %d 轮修复后仍有 %d 个 critical 问题,继续修复",
self.team_id, fix_round,
sum(1 for i in new_review.get("issues", [])
if i.get("severity") == "critical"))
except Exception as e:
logger.error("团队编排 [%s]: QA 审查异常: %s", self.team_id, e)
results["qa_review"] = results.get("qa_review", {})
results["qa_review"]["error"] = str(e)
results["qa_review"]["fix_rounds"] = fix_rounds
results["qa_review"]["final_review"] = final_qa_review
results["qa_review"]["final_pass"] = final_qa_review.get("pass", False)
# ─── 汇总最终交付物 ───
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] = []
# ─── 架构分析Architect 扫描现有项目) ───
enhanced_description = project_description
pre_scan_result = "" # 预扫描源码,后续注入到各执行阶段的 prompt
architect_agent = self._get_agent_by_role(members, "architect")
if architect_agent:
target_paths = _extract_paths_from_description(project_description)
if not target_paths:
target_paths = _get_default_scan_paths()
logger.info("团队编排 [%s]: 未从描述中提取路径,使用默认扫描路径: %s",
self.team_id, [str(p) for p in target_paths])
logger.info("团队编排 [%s]: Architect 开始扫描 %d 个目标路径",
self.team_id, len(target_paths))
yield {"type": "architect_start", "agent": architect_agent.name,
"paths": [str(p) for p in target_paths]}
try:
arch_config = _build_agent_config(architect_agent, self.auto_approve_files)
arch_runtime = AgentRuntime(arch_config)
# ─── 程序化预扫描源码 ───
pre_scan_result = scan_multiple_dirs(target_paths)
if pre_scan_result:
logger.info("团队编排 [%s]: 预扫描完成,已提取源文件内容",
self.team_id)
arch_input = (
f"你是一名系统架构师。请基于以下已预扫描的源代码,"
f"输出完整的架构上下文文档供后续测试团队使用。\n\n"
f"任务背景:{project_description}\n\n"
f"{pre_scan_result}\n\n"
f"## 你的任务\n"
f"上述源码清单中已包含关键源文件的完整内容。"
f"请基于这些真实代码(不是设计文档)输出架构分析文档,包含:\n"
f"1. 技术栈清单(框架/库/版本,从实际配置文件确认)\n"
f"2. 目录结构与模块划分\n"
f"3. 数据流路径UI→ViewModel→Repository→API/DAO→DB\n"
f"4. 关键类的职责与依赖关系(引用源码中的实际类名/方法名)\n"
f"5. 从源码中发现的实际代码质量问题\n\n"
f"## 注意\n"
f"- 主要分析基于上述已提供的源码内容\n"
f"- 如需补充阅读特定文件,可以用 file_read 工具\n"
f"- 不要基于设计文档或推测进行分析\n"
f"- 在输出中列出你分析引用了哪些源文件"
)
arch_result = await arch_runtime.run(arch_input)
if arch_result.success and arch_result.content:
arch_analysis = arch_result.content.strip()
enhanced_description = (
f"{project_description}\n\n"
f"## 架构分析(由系统架构师扫描现有项目生成)\n\n"
f"{arch_analysis}"
)
yield {"type": "architect_done",
"analysis": arch_analysis[:2000],
"full_length": len(arch_analysis)}
logger.info("团队编排 [%s]: Architect 分析完成 (%d 字符)",
self.team_id, len(arch_analysis))
except Exception as e:
logger.warning("团队编排 [%s]: Architect 分析异常(继续执行): %s",
self.team_id, e)
yield {"type": "architect_done", "error": str(e)}
# ─── 规划阶段(由 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]
# 如果架构师已作为前置步骤执行,从可用角色中移除,避免 PM 再次规划架构师阶段
if enhanced_description != project_description and "architect" in available_roles:
available_roles.remove("architect")
roles_hint = "/".join(available_roles)
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
planner_runtime = AgentRuntime(planner_config)
planner_input = (
f"你是一个团队的项目规划者。请分析以下需求,将工作分解为阶段,并分配给你团队的每个成员。\n\n"
f"需求描述:{enhanced_description}\n"
f"输出文件目录:{project_path}\n\n"
f"你的团队有以下角色:{roles_hint}\n"
f"每个阶段必须分配上述角色之一。\n\n"
f"请严格按照以下 JSON 格式输出执行计划(不要输出任何其他内容,只输出 JSON\n"
f'{{"project_name":"...","analysis":"...","phases":[\n'
f' {{"phase":1,"name":"...","role":"{roles_hint.split("/")[0] if "/" in roles_hint else roles_hint}","description":"...","expected_output":"...","depends_on":[]}},\n'
f' {{"phase":2,"name":"...","role":"...","description":"...","expected_output":"...","depends_on":[]}}\n'
f'],"acceptance_criteria":["..."]}}\n\n'
f"关键要求:\n"
f"1. phases 数组是强制性的——没有它计划无法执行\n"
f"2. 每个 phase 的 role 必须是上述角色之一\n"
f"3. depends_on 列出必须先完成的阶段编号,无依赖则用[]\n"
f"4. 4-7 个阶段最佳\n"
f"5. 用中文输出\n"
f"6. 只输出 JSON不要有任何前言或后记"
)
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 {}
# 回退方案:尝试读取规划师写入的 execution_plan.json
if (not plan or not plan.get("phases")) and project_path.exists():
plan_file = project_path / "execution_plan.json"
if plan_file.exists():
try:
plan = json.loads(plan_file.read_text(encoding="utf-8"))
logger.info("团队编排(stream) [%s]: 从文件回退读取计划 %s", self.team_id, plan_file)
except (json.JSONDecodeError, PermissionError, OSError) as e:
logger.warning("团队编排(stream) [%s]: 回退读取计划文件失败: %s", self.team_id, e)
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"):
# 降级方案:规划师失败时使用默认阶段计划
logger.warning(
"团队编排(stream) [%s]: %s 未能生成有效计划使用默认5阶段计划降级执行",
self.team_id, planner_role
)
yield {"type": "plan_fallback", "content": f"{planner_role} 规划失败,使用默认计划降级执行"}
exec_roles = [r for r in available_roles if r not in ("architect", "test_planner")]
if not exec_roles:
exec_roles = available_roles[:]
default_phases = []
phase_names = {
"functional_tester": "功能测试与缺陷分析",
"ux_reviewer": "UX体验审查与竞品对标",
"edge_explorer": "边界条件与异常路径探索",
"performance_evaluator": "性能内存审计与优化建议",
}
for i, role in enumerate(exec_roles[:5], start=1):
default_phases.append({
"phase": i,
"name": phase_names.get(role, f"{role} 执行阶段"),
"role": role,
"description": f"基于架构文档执行 {role} 阶段",
"expected_output": f"{role} 分析报告",
"depends_on": [],
})
plan = {
"project_name": "自动化测试项目",
"analysis": "(规划师超时,使用默认阶段计划)",
"phases": default_phases,
"acceptance_criteria": ["所有阶段执行完成"],
}
logger.info(
"团队编排(stream) [%s]: 降级计划生成 %d 个阶段",
self.team_id, len(default_phases)
)
phases = plan["phases"]
context_doc = f"# 项目: {plan.get('project_name', '未命名')}\n\n## 需求分析\n{plan.get('analysis', '')}\n\n"
# 检测是否有 phase 显式指定了 depends_on
has_dependencies = any(p.get("depends_on") for p in phases)
if has_dependencies:
# 执行前验证并修复依赖关系
fix_count = _validate_and_fix_dependencies(phases)
if fix_count > 0:
logger.warning("团队编排 [%s]: 自动修复了 %d 个无效依赖", self.team_id, fix_count)
has_dependencies = any(p.get("depends_on") for p in phases)
if not has_dependencies:
logger.info("团队编排 [%s]: 依赖修复后回退为顺序执行", self.team_id)
if has_dependencies:
# ─── DAG 并行执行(流式 + 每 Agent 独立事件队列) ───
completed: Dict[int, str] = {}
pending = {p["phase"]: p for p in phases}
current_prev = prev_files.copy()
current_context = context_doc
while pending:
ready = [
p for p in pending.values()
if all(d in completed for d in p.get("depends_on", []))
]
if not ready:
yield {"type": "error", "content": "阶段循环依赖,无法继续"}
return
# 预解析 agent 信息
phase_agents = []
for p in ready:
agent = self._get_agent_by_role(members, p.get("role", ""))
phase_agents.append((p, agent))
yield {
"type": "parallel_batch_start",
"phases": [
{"phase": p["phase"], "name": p.get("name"), "role": p.get("role", ""),
"agent": a.name if a else None}
for p, a in phase_agents
],
}
for p, agent in phase_agents:
yield {
"type": "phase_start",
"phase": p["phase"],
"name": p.get("name"),
"role": p.get("role", ""),
"agent": agent.name if agent else None,
}
# 并行执行:每个 phase 通过 event_queue 实时推送 Agent 内部事件
event_queue: asyncio.Queue = asyncio.Queue()
tasks = {
asyncio.create_task(
self._run_phase_stream_and_collect(
p, members, current_context, project_path, current_prev, event_queue,
pre_scan_result=pre_scan_result,
)
): p
for p, _ in phase_agents
}
# 消费事件队列直到所有 phase 完成
done_count = 0
while done_count < len(tasks):
evt = await event_queue.get()
yield evt
if evt.get("type") == "phase_done":
done_count += 1
# 收集最终结果
for task, p in tasks.items():
try:
r = task.result()
except Exception as exc:
r = {
"phase": p["phase"], "name": p.get("name", ""),
"role": p.get("role", ""),
"agent_name": None,
"output": f"并行执行异常: {exc}",
"success": False, "error": str(exc),
"files": [],
}
output_text = r.get("output", "")
completed[p["phase"]] = output_text
current_context += f"\n## 阶段 {p['phase']}: {p.get('name', '')}\n{output_text[:8000]}\n"
all_outputs.append(f"## {r['name']} ({r['role']})\n{output_text}")
for fp in r.get("files", []):
if fp not in all_files:
all_files.append(fp)
current_prev.add(fp)
del pending[p["phase"]]
yield {
"type": "parallel_batch_end",
"completed": [p["phase"] for p, _ in phase_agents],
}
prev_files = current_prev
context_doc = current_context
else:
# ─── 顺序执行(流式:每 phase 实时推送 Agent 内部事件) ───
completed_outputs_s: Dict[int, str] = {}
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}")
agent = self._get_agent_by_role(members, role)
if not agent:
yield {"type": "phase_done", "phase": phase_num, "name": phase_name,
"role": role, "success": False, "error": f"missing_role:{role}"}
all_outputs.append(f"[跳过] 阶段 {phase_num} ({phase_name}): 无 {role} 角色")
continue
yield {
"type": "phase_start",
"phase": phase_num,
"name": phase_name,
"role": role,
"agent": agent.name,
}
event_queue = asyncio.Queue()
task = asyncio.create_task(
self._run_phase_stream_and_collect(
phase, members, context_doc, project_path, prev_files, event_queue,
pre_scan_result=pre_scan_result,
)
)
# 消费事件直到 phase_done
while True:
evt = await event_queue.get()
yield evt
if evt.get("type") == "phase_done":
break
try:
r = task.result()
except Exception as exc:
r = {
"phase": phase_num,
"name": phase_name,
"role": role,
"agent_name": agent.name,
"output": f"执行异常: {exc}",
"success": False, "error": str(exc),
"files": [],
}
output_text = r.get("output", "")
if r.get("success"):
context_doc += f"\n## 阶段 {phase_num}: {phase_name}\n{output_text[:8000]}\n"
all_outputs.append(f"## {phase_name} ({role})\n{output_text}")
completed_outputs_s[phase_num] = output_text
for fp in r.get("files", []):
if fp not in all_files:
all_files.append(fp)
prev_files.add(fp)
# ─── QA 审查 + 反馈闭环 ───
qa_agent = self._get_agent_by_role(members, "qa")
fix_rounds: List[Dict[str, Any]] = []
final_qa_review: Dict[str, Any] = {}
if qa_agent and all_outputs:
yield {"type": "qa_start", "agent": qa_agent.name}
async def _run_qa_review_stream(round_label: str = "") -> tuple:
"""运行一次 QA 审查,返回 (qa_review_dict, raw_output, success)"""
qa_config = _build_agent_config(qa_agent, self.auto_approve_files)
qa_runtime = AgentRuntime(qa_config)
deliverables = "\n\n---\n\n".join(all_outputs)
prefix = f"{round_label}" if round_label else ""
qa_input = (
f"请审查以下项目交付物{prefix}\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 {}
return qa_review_data, qa_result.content, qa_result.success
try:
qa_review_data, qa_output, qa_success = await _run_qa_review_stream()
final_qa_review = qa_review_data
yield {
"type": "qa_done",
"output": qa_output,
"review": qa_review_data,
"success": qa_success,
}
# ─── QA 反馈闭环critical 问题强制执行至少一轮修复 ───
MAX_FIX_ROUNDS = 3
for fix_round in range(1, MAX_FIX_ROUNDS + 1):
if final_qa_review.get("pass", False):
break
issues = final_qa_review.get("issues", [])
fixable = [i for i in issues
if i.get("severity") in ("critical", "high")]
has_critical = any(
i.get("severity") == "critical" for i in issues
)
if not fixable:
break
if has_critical and fix_round == 1:
logger.info("团队编排 [%s]: 检测到 critical 问题,强制执行修复",
self.team_id)
dev_role = "developer"
dev_agent = self._get_agent_by_role(members, dev_role)
if not dev_agent:
dev_role = "fullstack_dev"
dev_agent = self._get_agent_by_role(members, dev_role)
if not dev_agent:
logger.warning("团队编排: QA 修复循环需要 developer但未找到")
break
logger.info("团队编排 [%s]: QA 修复第 %d 轮(流式),%d 个问题",
self.team_id, fix_round, len(fixable))
yield {
"type": "fix_phase_start",
"round": fix_round,
"issues_count": len(fixable),
"agent": dev_agent.name,
}
issues_text = "\n\n".join(
f"### 问题 {j+1} [{i['severity']}]\n"
f"**文件**: {i.get('file', 'unknown')}\n"
f"**描述**: {i.get('description', '')}\n"
f"**修复建议**: {i.get('suggestion', '')}"
for j, i in enumerate(fixable)
)
fix_phase = {
"phase": 100 + fix_round,
"name": f"QA反馈修复 (第{fix_round}轮)",
"role": dev_role,
"description": (
f"根据 QA 审查结果修复以下 {len(fixable)} 个问题:\n\n{issues_text}"
),
"expected_output": "修复后的代码文件",
"depends_on": [],
}
# 使用流式执行修复阶段
fix_event_queue: asyncio.Queue = asyncio.Queue()
fix_task = asyncio.create_task(
self._run_phase_stream_and_collect(
fix_phase, members, context_doc, project_path,
prev_files, fix_event_queue,
pre_scan_result=pre_scan_result,
)
)
while True:
evt = await fix_event_queue.get()
evt["_fix_round"] = fix_round
yield evt
if evt.get("type") == "phase_done":
break
try:
fix_result = fix_task.result()
except Exception as exc:
fix_result = {
"phase": 100 + fix_round,
"name": f"QA反馈修复 (第{fix_round}轮)",
"role": dev_role,
"agent_name": dev_agent.name,
"output": f"修复异常: {exc}",
"success": False, "error": str(exc),
"files": [],
}
if fix_result.get("success"):
fix_output = fix_result.get("output", "")
context_doc += f"\n## QA修复 第{fix_round}\n{fix_output[:2000]}\n"
all_outputs.append(
f"## QA修复 第{fix_round}轮 ({dev_role})\n{fix_output}"
)
for fp in fix_result.get("files", []):
if fp not in all_files:
all_files.append(fp)
prev_files.add(fp)
yield {
"type": "fix_phase_done",
"round": fix_round,
"success": fix_result.get("success", False),
}
# 重新 QA
before_score = final_qa_review.get("score", 0)
yield {"type": "qa_start", "agent": qa_agent.name,
"round": fix_round}
new_review, new_output, new_success = await _run_qa_review_stream(
f"{fix_round}轮修复后复查"
)
fix_rounds.append({
"round": fix_round,
"before_score": before_score,
"after_score": new_review.get("score", 0),
"fixed_issues": len(fixable),
})
final_qa_review = new_review
yield {
"type": "qa_done",
"output": new_output,
"review": new_review,
"success": new_success,
"round": fix_round,
}
# 检查本轮修复后是否还有 critical 问题
remaining_critical = any(
i.get("severity") == "critical"
for i in new_review.get("issues", [])
)
# 分数无提升且无 critical 残留 → 终止;有 critical 残留则继续
if new_review.get("score", 0) <= before_score and not remaining_critical:
logger.info("团队编排: QA 修复第 %d 轮分数无提升 (%d%d),终止",
fix_round, before_score,
new_review.get("score", 0))
break
if remaining_critical:
logger.warning("团队编排 [%s]: 第 %d 轮修复后仍有 %d 个 critical 问题,继续修复",
self.team_id, fix_round,
sum(1 for i in new_review.get("issues", [])
if i.get("severity") == "critical"))
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),
"qa_fix_rounds": fix_rounds,
"qa_final_pass": final_qa_review.get("pass", False),
"qa_final_score": final_qa_review.get("score", 0),
}