- 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>
1806 lines
80 KiB
Python
1806 lines
80 KiB
Python
"""
|
||
团队编排引擎 — 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),
|
||
}
|