feat: virtual company M2 control + tier-3/4 capabilities + per-dept session fix
- M2-a/b/c: stop/pause/resume/skip dept, plan/rework approval gates, budget & timeout circuit breakers, per-dept cancel and on-demand rework - tier-3: batched event persistence (_EventBuffer), project/dept concurrency semaphores, company_ws event streaming - tier-4: cross-dept deliverable handoff, CEO memory feedback loop, per-dept model override, one-click Markdown report export (RFC5987) - fix: per-department independent SQLAlchemy session in parallel waves (shared Session race across asyncio tasks in company_orchestrator) - fix: rename process_open_fds gauge to app_process_open_fds (collides with prometheus_client ProcessCollector on Linux) - fix: dept role resolution ladder, real token usage & cost tracking, deliverable file capture (mtime scan of dept workspace) - tests: test_company_orchestrator.py 22 cases with mocked LLM - frontend: CompanyControlRoom, companyExecution store, EChart, dept model config dialog, ws proxy, build/typecheck decoupling - docs: virtual company design/usage docs, M2 control plan, deepseek4pro handover docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,9 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import difflib
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, AsyncGenerator, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
@@ -39,8 +41,34 @@ from app.services.project_scanner import (
|
||||
)
|
||||
|
||||
|
||||
def _build_agent_config(agent: Agent, auto_approve_files: bool = True) -> AgentConfig:
|
||||
"""从 Agent 的 workflow_config 构建 AgentConfig,供 AgentRuntime 使用。"""
|
||||
def _extract_model_override(team_config: Any) -> Optional[dict]:
|
||||
"""四档-③:从 Team.config 解析部门级模型覆盖 {provider?, model?}(无则 None)。
|
||||
|
||||
约定存于 `config["model_override"] = {"provider": "...", "model": "..."}`;
|
||||
provider/model 任一为空即视为未设该项,两者皆空 → None(回归各成员 Agent 自身配置)。
|
||||
"""
|
||||
if not isinstance(team_config, dict):
|
||||
return None
|
||||
mo = team_config.get("model_override")
|
||||
if not isinstance(mo, dict):
|
||||
return None
|
||||
model = str(mo.get("model") or "").strip()
|
||||
provider = str(mo.get("provider") or "").strip()
|
||||
if not model and not provider:
|
||||
return None
|
||||
return {"model": model or None, "provider": provider or None}
|
||||
|
||||
|
||||
def _build_agent_config(
|
||||
agent: Agent,
|
||||
auto_approve_files: bool = True,
|
||||
model_override: Optional[dict] = None,
|
||||
) -> AgentConfig:
|
||||
"""从 Agent 的 workflow_config 构建 AgentConfig,供 AgentRuntime 使用。
|
||||
|
||||
model_override(四档-③ 部门级配模型)不为空时覆盖 provider/model,
|
||||
让同一部门内所有成员统一走部门指定的模型(成本/质量分层调优)。
|
||||
"""
|
||||
wf = agent.workflow_config or {}
|
||||
nodes = wf.get("nodes", [])
|
||||
llm_node = None
|
||||
@@ -58,8 +86,16 @@ def _build_agent_config(agent: Agent, auto_approve_files: bool = True) -> AgentC
|
||||
temperature = float(llm_data.get("temperature", 0.4))
|
||||
max_iterations = int(llm_data.get("max_iterations", 15))
|
||||
|
||||
# 四档-③:部门级模型覆盖(优先于成员 Agent 自身配置)
|
||||
if model_override:
|
||||
if model_override.get("model"):
|
||||
model = model_override["model"]
|
||||
if model_override.get("provider"):
|
||||
provider = model_override["provider"]
|
||||
|
||||
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)
|
||||
logger.info("_build_agent_config: agent=%s tools=%d permission_level=%s model=%s%s",
|
||||
agent.name, len(tools), perm, model, " (dept-override)" if model_override else "")
|
||||
return AgentConfig(
|
||||
name=agent.name,
|
||||
system_prompt=system_prompt,
|
||||
@@ -232,6 +268,48 @@ def _parse_depends_on(raw: str) -> List[int]:
|
||||
return []
|
||||
|
||||
|
||||
def _norm_role(s: str) -> str:
|
||||
"""角色名归一化:小写、去空白、下划线/空格统一为连字符。"""
|
||||
return (s or "").strip().lower().replace("_", "-").replace(" ", "-")
|
||||
|
||||
|
||||
# LLM 常见叫法 → 预设 slug(归一化后的键)。其余靠归一化+模糊兜底。
|
||||
_ROLE_ALIASES: Dict[str, str] = {
|
||||
"backend": "dev-backend",
|
||||
"back-end": "dev-backend",
|
||||
"backend-dev": "dev-backend",
|
||||
"backend-developer": "dev-backend",
|
||||
"developer": "dev-backend",
|
||||
"engineer": "dev-backend",
|
||||
"程序员": "dev-backend",
|
||||
"后端": "dev-backend",
|
||||
"后端工程师": "dev-backend",
|
||||
"frontend": "dev-frontend",
|
||||
"front-end": "dev-frontend",
|
||||
"frontend-dev": "dev-frontend",
|
||||
"frontend-developer": "dev-frontend",
|
||||
"前端": "dev-frontend",
|
||||
"前端工程师": "dev-frontend",
|
||||
"产品经理": "pm",
|
||||
"product-manager": "pm",
|
||||
"pm-lead": "pm",
|
||||
"设计师": "designer",
|
||||
"ui": "designer",
|
||||
"ui-designer": "designer",
|
||||
"测试": "qa",
|
||||
"测试工程师": "qa",
|
||||
"qa-engineer": "qa",
|
||||
"运维": "devops",
|
||||
"文档": "tech-writer",
|
||||
"技术文档": "tech-writer",
|
||||
"tech-writer": "tech-writer",
|
||||
"techwriter": "tech-writer",
|
||||
"首席执行官": "ceo",
|
||||
"技术总监": "cto",
|
||||
"首席技术官": "cto",
|
||||
}
|
||||
|
||||
|
||||
def _parse_phases_from_markdown(text: str) -> List[Dict[str, Any]]:
|
||||
"""从 Markdown 表格中提取 phase 信息。支持格式:
|
||||
| **phase-1** | `functional_tester` | 核心任务 | 工时 |
|
||||
@@ -240,10 +318,10 @@ def _parse_phases_from_markdown(text: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
phases = []
|
||||
# 匹配表格行: | (可选**的) phase-N (可选**) | role | description | [可选 depends_on] |
|
||||
# role 可能带反引号 `role` 或直接是文本
|
||||
# role 可能带反引号 `role` 或直接是文本;支持连字符 slug(如 dev-backend)
|
||||
table_row = re.compile(
|
||||
r'^\|\s*\*{0,2}(?:phase[-–]?\s*)?(\d+)\*{0,2}\s*\|'
|
||||
r'\s*(?:`)?([a-z_]+)(?:`)?\s*\|'
|
||||
r'\s*(?:`)?([a-z0-9_-]+)(?:`)?\s*\|'
|
||||
r'\s*([^|]+?)\s*\|'
|
||||
r'(?:\s*([^|]*?)\s*\|)?',
|
||||
re.MULTILINE | re.IGNORECASE
|
||||
@@ -253,16 +331,8 @@ def _parse_phases_from_markdown(text: str) -> List[Dict[str, Any]]:
|
||||
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"):
|
||||
# 接受任何非空 role slug;不存在的角色交由 _resolve_phase_agent 归一化/兜底
|
||||
if not role:
|
||||
continue
|
||||
phases.append({
|
||||
"phase": phase_num,
|
||||
@@ -311,6 +381,27 @@ def _scan_directory_files(directory: Path) -> List[str]:
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def _scan_recent_files(root: Path, since_ts: float) -> List[str]:
|
||||
"""递归扫描 root,返回本次运行期间(mtime >= since_ts)修改过的文件绝对路径。
|
||||
|
||||
用于精确捕获部门本轮真实产物:Agent 有时把产物写到 project_path 以外的
|
||||
兄弟目录,或用脚本(code_execute)生成文件——这些都不在 file_write 追踪里,
|
||||
但只要落在部门工作区且是本轮修改的,就应计入交付物。按 mtime 过滤可避免
|
||||
把历史运行的陈旧文件误纳入。
|
||||
"""
|
||||
import os
|
||||
hits: List[str] = []
|
||||
if not root.exists():
|
||||
return hits
|
||||
for p in root.rglob("*"):
|
||||
try:
|
||||
if p.is_file() and p.stat().st_mtime >= since_ts:
|
||||
hits.append(str(p))
|
||||
except OSError:
|
||||
continue
|
||||
return hits
|
||||
|
||||
|
||||
def _extract_written_files(result: AgentResult) -> List[str]:
|
||||
"""从 AgentResult.steps 中提取 file_write 成功产出的绝对路径列表。"""
|
||||
files: List[str] = []
|
||||
@@ -357,6 +448,11 @@ class TeamOrchestrator:
|
||||
self.team_id = team_id
|
||||
self.user_id = user_id
|
||||
self.auto_approve_files = auto_approve_files
|
||||
self._model_override: Optional[dict] = None # 四档-③:部门级模型覆盖,execute[_stream] 载入 team 后设置
|
||||
|
||||
def _acfg(self, agent: Agent) -> AgentConfig:
|
||||
"""构建成员 AgentConfig,自动应用部门级模型覆盖(四档-③)。"""
|
||||
return _build_agent_config(agent, self.auto_approve_files, self._model_override)
|
||||
|
||||
def _load_team(self) -> Team:
|
||||
team = self.db.query(Team).filter(Team.id == self.team_id).first()
|
||||
@@ -373,12 +469,66 @@ class TeamOrchestrator:
|
||||
)
|
||||
|
||||
def _get_agent_by_role(self, members: List[TeamMember], role: str) -> Optional[Agent]:
|
||||
"""按角色查找 Agent。"""
|
||||
"""按角色查找 Agent(严格精确匹配)。规划者角色解析依赖其 None 回退,勿放宽。"""
|
||||
for m in members:
|
||||
if m.role == role:
|
||||
return m.agent
|
||||
return None
|
||||
|
||||
def _resolve_phase_agent(
|
||||
self, members: List[TeamMember], role: str
|
||||
) -> tuple[Optional[Agent], str, bool]:
|
||||
"""健壮地把阶段 role 解析到成员 Agent,避免「部门空转」。
|
||||
|
||||
解析阶梯:精确 → 归一化相等 → 复合拆分(a/b、a,b) → alias 表 → 模糊 → 兜底到 Leader。
|
||||
Returns: (agent, matched_role, is_fallback)。members 为空才返回 (None, role, False)。
|
||||
"""
|
||||
if not members:
|
||||
return None, role, False
|
||||
|
||||
# a. 精确
|
||||
for m in members:
|
||||
if m.role == role:
|
||||
return m.agent, m.role, False
|
||||
|
||||
norm_to_member = {_norm_role(m.role): m for m in members}
|
||||
nrole = _norm_role(role)
|
||||
|
||||
# b. 归一化相等
|
||||
if nrole in norm_to_member:
|
||||
m = norm_to_member[nrole]
|
||||
return m.agent, m.role, False
|
||||
|
||||
# c. 复合拆分("ceo/cto"、"a, b"、"a and b"、"a、b")
|
||||
tokens = [t.strip().strip("-") for t in re.split(r"[/,&|、]|\band\b", nrole)]
|
||||
tokens = [t for t in tokens if t]
|
||||
if len(tokens) > 1:
|
||||
for tok in tokens:
|
||||
if tok in norm_to_member:
|
||||
m = norm_to_member[tok]
|
||||
return m.agent, m.role, False
|
||||
alias = _ROLE_ALIASES.get(tok)
|
||||
if alias and _norm_role(alias) in norm_to_member:
|
||||
m = norm_to_member[_norm_role(alias)]
|
||||
return m.agent, m.role, False
|
||||
|
||||
# d. alias 表
|
||||
alias = _ROLE_ALIASES.get(nrole)
|
||||
if alias and _norm_role(alias) in norm_to_member:
|
||||
m = norm_to_member[_norm_role(alias)]
|
||||
return m.agent, m.role, False
|
||||
|
||||
# e. 模糊
|
||||
close = difflib.get_close_matches(nrole, list(norm_to_member.keys()), n=1, cutoff=0.6)
|
||||
if close:
|
||||
m = norm_to_member[close[0]]
|
||||
return m.agent, m.role, False
|
||||
|
||||
# f. 兜底到 Leader(无则第一个成员)
|
||||
lead = next((m for m in sorted(members, key=lambda x: x.position) if m.is_lead), None)
|
||||
m = lead or sorted(members, key=lambda x: x.position)[0]
|
||||
return m.agent, m.role, True
|
||||
|
||||
def _resolve_planner_role(self, members: List[TeamMember], workflow: str) -> str:
|
||||
"""根据团队 workflow 类型解析规划者角色。
|
||||
|
||||
@@ -491,14 +641,10 @@ class TeamOrchestrator:
|
||||
self_review = phase.get("self_review", False)
|
||||
|
||||
# DB 查询在协程内完成(members 已预加载,此处仅内存迭代)
|
||||
agent = None
|
||||
for m in members:
|
||||
if m.role == role:
|
||||
agent = m.agent
|
||||
break
|
||||
agent, matched_role, role_fallback = self._resolve_phase_agent(members, role)
|
||||
|
||||
if not agent:
|
||||
logger.warning("团队编排 [%s]: 阶段 %s 缺少角色 '%s',跳过",
|
||||
logger.warning("团队编排 [%s]: 阶段 %s 无可用成员(角色 '%s'),跳过",
|
||||
self.team_id, phase_num, role)
|
||||
return {
|
||||
"phase": phase_num, "name": phase_name, "role": role,
|
||||
@@ -508,11 +654,16 @@ class TeamOrchestrator:
|
||||
"files": [],
|
||||
}
|
||||
|
||||
if role_fallback or matched_role != role:
|
||||
logger.info("团队编排 [%s]: 阶段 %s 角色 '%s' 解析为 '%s'%s",
|
||||
self.team_id, phase_num, role, matched_role,
|
||||
"(兜底 Leader)" if role_fallback else "")
|
||||
|
||||
logger.info("团队编排 [%s]: 执行阶段 %s — %s (角色: %s, Agent: %s)",
|
||||
self.team_id, phase_num, phase_name, role, agent.name)
|
||||
self.team_id, phase_num, phase_name, matched_role, agent.name)
|
||||
|
||||
try:
|
||||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||||
agent_config = self._acfg(agent)
|
||||
runtime = AgentRuntime(agent_config)
|
||||
|
||||
phase_input = self._build_phase_input(
|
||||
@@ -555,6 +706,7 @@ class TeamOrchestrator:
|
||||
|
||||
return {
|
||||
"phase": phase_num, "name": phase_name, "role": role,
|
||||
"actual_role": matched_role, "role_fallback": role_fallback,
|
||||
"agent_name": agent.name,
|
||||
"output": output_text,
|
||||
"success": phase_result.success,
|
||||
@@ -597,15 +749,13 @@ class TeamOrchestrator:
|
||||
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
|
||||
agent, matched_role, role_fallback = self._resolve_phase_agent(members, role)
|
||||
|
||||
base_event = {
|
||||
"phase": phase_num,
|
||||
"role": role,
|
||||
"actual_role": matched_role,
|
||||
"role_fallback": role_fallback,
|
||||
"name": phase_name,
|
||||
"agent": agent.name if agent else None,
|
||||
}
|
||||
@@ -627,8 +777,13 @@ class TeamOrchestrator:
|
||||
"files": [],
|
||||
}
|
||||
|
||||
if role_fallback or matched_role != role:
|
||||
logger.info("团队编排 [%s]: 阶段 %s 角色 '%s' 解析为 '%s'%s",
|
||||
self.team_id, phase_num, role, matched_role,
|
||||
"(兜底 Leader)" if role_fallback else "")
|
||||
|
||||
try:
|
||||
agent_config = _build_agent_config(agent, self.auto_approve_files)
|
||||
agent_config = self._acfg(agent)
|
||||
runtime = AgentRuntime(agent_config)
|
||||
|
||||
phase_input = self._build_phase_input(
|
||||
@@ -838,7 +993,7 @@ class TeamOrchestrator:
|
||||
members = self._load_members()
|
||||
if not members:
|
||||
raise ValueError("团队没有成员,请先为角色分配 Agent")
|
||||
|
||||
self._model_override = _extract_model_override(team.config) # 四档-③ 部门级配模型
|
||||
# ─── 创建项目目录 ───
|
||||
workspace_root = _resolve_workspace_root()
|
||||
safe_name = _safe_project_name(project_description)
|
||||
@@ -846,6 +1001,7 @@ class TeamOrchestrator:
|
||||
project_path.mkdir(parents=True, exist_ok=True)
|
||||
logger.info("团队编排 [%s]: 项目目录 %s", self.team_id, project_path)
|
||||
|
||||
run_start_ts = time.time() # 交付物捕获基准:本轮之后修改的文件才算产物
|
||||
prev_files = set(_scan_directory_files(project_path))
|
||||
all_files: List[str] = []
|
||||
|
||||
@@ -877,7 +1033,7 @@ class TeamOrchestrator:
|
||||
logger.info("团队编排 [%s]: Architect 开始扫描 %d 个目标路径",
|
||||
self.team_id, len(target_paths))
|
||||
try:
|
||||
arch_config = _build_agent_config(architect_agent, self.auto_approve_files)
|
||||
arch_config = self._acfg(architect_agent)
|
||||
arch_runtime = AgentRuntime(arch_config)
|
||||
# ─── 程序化预扫描源码 ───
|
||||
pre_scan_result = scan_multiple_dirs(target_paths)
|
||||
@@ -932,27 +1088,29 @@ class TeamOrchestrator:
|
||||
# 如果架构师已作为前置步骤执行,从可用角色中移除,避免 PM 再次规划架构师阶段
|
||||
if enhanced_description != project_description and "architect" in available_roles:
|
||||
available_roles.remove("architect")
|
||||
roles_hint = "/".join(available_roles)
|
||||
roles_hint = ", ".join(available_roles)
|
||||
first_role = available_roles[0] if available_roles else "developer"
|
||||
|
||||
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_config = self._acfg(planner_agent)
|
||||
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"你的团队有以下角色 slug(必须逐字使用,禁止合并/翻译/自造):{roles_hint}\n"
|
||||
f'每个阶段的 role 字段必须精确等于上述其中一个 slug(例如 "{first_role}");'
|
||||
f'严禁把多个角色写成 "a/b" 这种复合形式。\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":1,"name":"...","role":"{first_role}","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"2. 每个 phase 的 role 必须逐字复制上述角色 slug 之一(区分大小写、含连字符),禁止组合/翻译/自造\n"
|
||||
f"3. depends_on 列出必须先完成的阶段编号,无依赖则用[]\n"
|
||||
f"4. 4-7 个阶段最佳\n"
|
||||
f"5. 用中文输出\n"
|
||||
@@ -1090,7 +1248,7 @@ class TeamOrchestrator:
|
||||
|
||||
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_config = self._acfg(qa_agent)
|
||||
qa_runtime = AgentRuntime(qa_config)
|
||||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||||
prefix = f"({round_label})" if round_label else ""
|
||||
@@ -1238,6 +1396,9 @@ class TeamOrchestrator:
|
||||
final_scan = set(_scan_directory_files(project_path))
|
||||
for fp in all_files:
|
||||
final_scan.add(fp)
|
||||
# 兜底:捕获 Agent 写到 project_path 以外兄弟目录 / 脚本生成的本轮产物
|
||||
for fp in _scan_recent_files(project_path.parent, run_start_ts):
|
||||
final_scan.add(fp)
|
||||
results["files"] = sorted(final_scan)
|
||||
results["completed_at"] = datetime.now().isoformat()
|
||||
return results
|
||||
@@ -1256,6 +1417,7 @@ class TeamOrchestrator:
|
||||
try:
|
||||
team = self._load_team()
|
||||
members = self._load_members()
|
||||
self._model_override = _extract_model_override(team.config) # 四档-③ 部门级配模型
|
||||
except Exception as e:
|
||||
yield {"type": "error", "content": str(e)}
|
||||
return
|
||||
@@ -1269,6 +1431,7 @@ class TeamOrchestrator:
|
||||
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)
|
||||
run_start_ts = time.time() # 交付物捕获基准:本轮之后修改的文件才算产物
|
||||
prev_files = set(_scan_directory_files(project_path))
|
||||
all_files: List[str] = []
|
||||
|
||||
@@ -1296,7 +1459,7 @@ class TeamOrchestrator:
|
||||
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_config = self._acfg(architect_agent)
|
||||
arch_runtime = AgentRuntime(arch_config)
|
||||
# ─── 程序化预扫描源码 ───
|
||||
pre_scan_result = scan_multiple_dirs(target_paths)
|
||||
@@ -1353,24 +1516,26 @@ class TeamOrchestrator:
|
||||
# 如果架构师已作为前置步骤执行,从可用角色中移除,避免 PM 再次规划架构师阶段
|
||||
if enhanced_description != project_description and "architect" in available_roles:
|
||||
available_roles.remove("architect")
|
||||
roles_hint = "/".join(available_roles)
|
||||
roles_hint = ", ".join(available_roles)
|
||||
first_role = available_roles[0] if available_roles else "developer"
|
||||
|
||||
planner_config = _build_agent_config(planner_agent, self.auto_approve_files)
|
||||
planner_config = self._acfg(planner_agent)
|
||||
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"你的团队有以下角色 slug(必须逐字使用,禁止合并/翻译/自造):{roles_hint}\n"
|
||||
f'每个阶段的 role 字段必须精确等于上述其中一个 slug(例如 "{first_role}");'
|
||||
f'严禁把多个角色写成 "a/b" 这种复合形式。\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":1,"name":"...","role":"{first_role}","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"2. 每个 phase 的 role 必须逐字复制上述角色 slug 之一(区分大小写、含连字符),禁止组合/翻译/自造\n"
|
||||
f"3. depends_on 列出必须先完成的阶段编号,无依赖则用[]\n"
|
||||
f"4. 4-7 个阶段最佳\n"
|
||||
f"5. 用中文输出\n"
|
||||
@@ -1552,7 +1717,7 @@ class TeamOrchestrator:
|
||||
role = phase.get("role", "developer")
|
||||
phase_name = phase.get("name", f"阶段 {phase_num}")
|
||||
|
||||
agent = self._get_agent_by_role(members, role)
|
||||
agent, matched_role, role_fallback = self._resolve_phase_agent(members, role)
|
||||
if not agent:
|
||||
yield {"type": "phase_done", "phase": phase_num, "name": phase_name,
|
||||
"role": role, "success": False, "error": f"missing_role:{role}"}
|
||||
@@ -1564,6 +1729,8 @@ class TeamOrchestrator:
|
||||
"phase": phase_num,
|
||||
"name": phase_name,
|
||||
"role": role,
|
||||
"actual_role": matched_role,
|
||||
"role_fallback": role_fallback,
|
||||
"agent": agent.name,
|
||||
}
|
||||
|
||||
@@ -1615,7 +1782,7 @@ class TeamOrchestrator:
|
||||
|
||||
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_config = self._acfg(qa_agent)
|
||||
qa_runtime = AgentRuntime(qa_config)
|
||||
deliverables = "\n\n---\n\n".join(all_outputs)
|
||||
prefix = f"({round_label})" if round_label else ""
|
||||
@@ -1792,6 +1959,9 @@ class TeamOrchestrator:
|
||||
final_scan = set(_scan_directory_files(project_path))
|
||||
for fp in all_files:
|
||||
final_scan.add(fp)
|
||||
# 兜底:捕获 Agent 写到 project_path 以外兄弟目录 / 脚本生成的本轮产物
|
||||
for fp in _scan_recent_files(project_path.parent, run_start_ts):
|
||||
final_scan.add(fp)
|
||||
yield {
|
||||
"type": "final",
|
||||
"deliverable": final,
|
||||
|
||||
Reference in New Issue
Block a user