Files
QaAutomationHub/scripts/fleet_runner.py
T
xst b2a035c4f9 feat: Agentic QE Fleet v2.0.0 - 14-agent quality engineering platform
- 14 specialized AI agents across 5 battle zones (Prepare/Analyze/Design/Review/Monitor)
- New: risk-assessor, test-strategist, data-builder, coverage-auditor, quality-gatekeeper, execution-analyst, knowledge-curator
- New: fleet_runner.py orchestrator with multi-zone manifest pipeline
- New: fleet_config.yml for centralized configuration
- New: knowledge activation system (keyword + semantic matching)
- New: semantic conflict detection with severity grading (P0-P3)
- New: three-tier quality gate (PASS/PASS_WITH_FIX/BLOCKED)
- New: monitor zone for test execution analysis and auto knowledge curation
- Backward compatible: /case_generate alias, case_pipeline.py preserved
- Comprehensive docs: USER_GUIDE.md + MAINTENANCE_GUIDE.md
2026-07-09 14:29:11 +08:00

1090 lines
41 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.
#!/usr/bin/env python3
"""
Agentic QE Fleet — 核心编排器
职责: 按战区顺序编排 14 个专业 Agent,协调 manifest 交接,
执行质量门禁裁决,触发 Excel 导出和知识沉淀。
用法:
python3 scripts/fleet_runner.py run --requirement <路径>
python3 scripts/fleet_runner.py prepare --requirement <路径>
python3 scripts/fleet_runner.py analyze --requirement <路径>
python3 scripts/fleet_runner.py design --requirement <路径>
python3 scripts/fleet_runner.py review --requirement <路径>
python3 scripts/fleet_runner.py export --requirement <路径>
python3 scripts/fleet_runner.py monitor --requirement <路径> --results <测试结果>
python3 scripts/fleet_runner.py status --requirement <路径>
"""
from __future__ import annotations
import argparse
import json
import sys
import subprocess
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
# 复用现有模块
from case_pipeline import (
resolve_requirement_path,
validate_requirement_file,
validate_knowledge_base,
validate_project_profile,
build_paths as build_legacy_paths,
ensure_output_dirs,
write_normalized_document,
select_technical_solution_files,
safe_read_text,
to_repo_relative,
REPO_ROOT,
REQUIREMENTS_DIR,
RAW_REQUIREMENTS_DIR,
TECHNICAL_SOLUTIONS_DIR,
PROJECT_PROFILE_FILE,
)
from export_excel import export_markdown_to_excel
# Fleet 自有模块
from fleet_manifest import (
save_manifest,
load_manifest,
load_manifest_safe,
build_merged_manifest,
get_zone_status,
mark_zone_completed,
manifest_path,
ZONE_ORDER,
)
from fleet_agents import (
AGENT_REGISTRY,
load_agent_prompt,
list_zone_agents,
validate_all_agents,
render_agent_prompt,
)
# ── 配置加载 ──────────────────────────────────────────────────────────────
try:
import yaml
_HAS_YAML = True
except ImportError:
_HAS_YAML = False
def load_fleet_config() -> dict[str, Any]:
"""加载 fleet_config.yml。"""
config_path = REPO_ROOT / "fleet_config.yml"
if not config_path.exists():
return _default_fleet_config()
if _HAS_YAML:
with open(config_path, "r", encoding="utf-8") as fh:
return yaml.safe_load(fh) or {}
else:
# 纯 Python fallback: 解析简单的 YAML 子集
return _parse_simple_yaml_config(config_path)
def _default_fleet_config() -> dict[str, Any]:
return {
"battle_zones": {
"prepare": {"enabled": True},
"analyze": {"enabled": True},
"design": {"enabled": True},
"review": {"enabled": True},
"monitor": {"enabled": True, "auto_confirm": True},
},
"quality_gate": {"min_coverage": 0.95, "max_blockers": 0},
"output": {"excel_format": "yunxiao", "snapshot_keep": 3, "auto_sync_maintained": True},
}
def _parse_simple_yaml_config(path: Path) -> dict[str, Any]:
"""简易 YAML 解析器(当 PyYAML 不可用时)。"""
import re
result: dict[str, Any] = {}
current: dict[str, Any] = result
stack: list[tuple[str, dict[str, Any]]] = []
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.rstrip()
if not line or line.strip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key_match = re.match(r"^(\s*)([\w_-]+)\s*:\s*(.*)", line)
if not key_match:
continue
key = key_match.group(2)
value = key_match.group(3).strip().strip('"').strip("'")
# 处理缩进回退
while stack and stack[-1][0] >= indent:
stack.pop()
current = stack[-1][1] if stack else result
if value in ("true", "True"):
current[key] = True
elif value in ("false", "False"):
current[key] = False
elif value == "" or value == "{}":
current[key] = {}
current = current[key]
stack.append((indent, current))
elif value == "[]":
current[key] = []
elif re.match(r"^- ", raw_line):
if key not in current:
current[key] = []
current[key].append(value)
elif re.match(r"^\d+(\.\d+)?$", value):
current[key] = float(value) if "." in value else int(value)
else:
current[key] = value
return result
# ── 路径和目录 ────────────────────────────────────────────────────────────
BUILTIN_INPUT_DIRS = [
REPO_ROOT / "output" / "analysis",
REPO_ROOT / "output" / "test_points",
REPO_ROOT / "output" / "test_cases",
REPO_ROOT / "output" / "excel_reports",
REPO_ROOT / "output" / "normalized_inputs",
REPO_ROOT / "output" / "versions",
]
def build_all_output_dirs(base_name: str) -> None:
"""创建所有输出目录。"""
legacy_paths = build_legacy_paths(base_name)
ensure_output_dirs(legacy_paths)
# 确保 manifest 目录
(REPO_ROOT / "output" / "manifests").mkdir(parents=True, exist_ok=True)
# ── 编排核心 ──────────────────────────────────────────────────────────────
def run_zone(zone: str, base_name: str, requirement_path: Path, config: dict[str, Any]) -> dict[str, Any]:
"""运行一个战区,返回该战区的 manifest 数据。"""
zone_config = config.get("battle_zones", {}).get(zone, {})
if not zone_config.get("enabled", True):
print(f"⏭️ {zone.upper()} 战区已禁用,跳过。")
return {}
print(f"\n{'='*60}")
print(f"🚀 {zone.upper()} 战区启动")
print(f"{'='*60}")
agents = list_zone_agents(zone)
print(f"📋 Agent: {', '.join(agents)}")
# 加载前序战区 manifest 作为上下文
context = _build_zone_context(base_name, zone, requirement_path)
# 打印当前可用的输入文件
_print_context_files(context, zone)
# 战区特定逻辑
zone_handlers = {
"prepare": _run_prepare_zone,
"analyze": _run_analyze_zone,
"design": _run_design_zone,
"review": _run_review_zone,
"monitor": _run_monitor_zone,
}
handler = zone_handlers.get(zone)
if handler is None:
raise ValueError(f"未知战区: {zone}")
result = handler(base_name, requirement_path, context, config, agents)
# 标记完成
if result:
mark_zone_completed(base_name, zone)
print(f"✅ {zone.upper()} 战区完成 → {manifest_path(base_name, zone)}")
return result
def _build_zone_context(base_name: str, zone: str, requirement_path: Path) -> dict[str, Any]:
"""构建战区运行的上下文。"""
context: dict[str, Any] = {
"base_name": base_name,
"requirement_file": str(requirement_path),
"requirement_stem": requirement_path.stem,
"repo_root": str(REPO_ROOT),
"project_profile": str(PROJECT_PROFILE_FILE),
}
# 加载前序战区 manifest
for prev_zone in ZONE_ORDER:
if prev_zone == zone:
break
manifest = load_manifest_safe(base_name, prev_zone)
if manifest:
context[f"manifest_{prev_zone}"] = manifest
context[f"manifest_{prev_zone}_path"] = str(manifest_path(base_name, prev_zone))
return context
def _print_context_files(context: dict[str, Any], zone: str) -> None:
"""打印战区可用的上下文文件。"""
from fleet_manifest import load_manifest_safe
items: list[tuple[str, str]] = []
# 从 manifest 中提取产物文件
for prev_zone in ZONE_ORDER:
if prev_zone == zone:
break
manifest = context.get(f"manifest_{prev_zone}")
if manifest is None:
continue
# 收集文件路径
for key in manifest:
if key.endswith("_file") or key.endswith("_files"):
value = manifest[key]
if isinstance(value, str) and Path(value).exists():
items.append((key, value))
elif isinstance(value, list):
for v in value:
if isinstance(v, str) and Path(v).exists():
items.append((key, v))
if items:
print("📂 可用上下文文件:")
for key, path in items[:10]:
print(f" - {key}: {path}")
# ── Prepare 战区 ──────────────────────────────────────────────────────────
def _run_prepare_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Prepare 战区: document-parser → knowledge-activator。"""
# 1. 文档解析 (借用现有 case_pipeline 的能力)
validate_requirement_file(requirement_path)
validate_knowledge_base()
validate_project_profile()
technical_solution_files = select_technical_solution_files(requirement_path)
normalized_dir = REPO_ROOT / "output" / "normalized_inputs" / base_name
normalized_dir.mkdir(parents=True, exist_ok=True)
normalized_requirement_file = write_normalized_document(
source_path=requirement_path,
target_path=normalized_dir / "requirement.md",
document_role="需求文档",
)
normalized_technical_solution_files = [
write_normalized_document(
source_path=path,
target_path=normalized_dir / f"technical_solution_{idx:02d}.md",
document_role="技术方案",
)
for idx, path in enumerate(technical_solution_files, start=1)
]
# 文档置信度评估
requirement_body = safe_read_text(normalized_requirement_file)
confidence = _estimate_document_confidence(requirement_body, requirement_path.suffix.lower())
# 2. 知识激活
activated_knowledge = _activate_knowledge(requirement_path, config)
# 检测知识缺口
knowledge_gaps = _detect_knowledge_gaps(activated_knowledge, requirement_body)
# 组装 manifest
prepare_manifest = {
"base_name": base_name,
"requirement_source_file": str(requirement_path),
"requirement_input_type": requirement_path.suffix.lower().lstrip("."),
"normalized_requirement_file": str(normalized_requirement_file),
"normalized_dir": str(normalized_dir),
"technical_solution_files": [str(p) for p in technical_solution_files],
"normalized_technical_solution_files": [str(p) for p in normalized_technical_solution_files],
"project_profile_file": str(PROJECT_PROFILE_FILE),
"document_confidence": confidence,
"activated_knowledge": activated_knowledge,
"knowledge_gaps": knowledge_gaps,
"agent_notes": {
"document-parser": f"解析完成,置信度 {confidence.get('requirement', 0):.0%}",
"knowledge-activator": f"激活 {len(activated_knowledge.get('terminology', {}).get('permanent', []))} 常驻 + "
f"{len(activated_knowledge.get('terminology', {}).get('optional', []))} 可选术语",
},
}
save_manifest(base_name, "prepare", prepare_manifest)
return prepare_manifest
def _estimate_document_confidence(text: str, suffix: str) -> dict[str, Any]:
"""估算文档解析置信度。"""
confidence = 1.0 if suffix == ".md" else 0.90 if suffix == ".docx" else 0.80 if suffix == ".pdf" else 0.75
if not text.strip():
confidence = 0.1
elif "> ⚠️ 待确认:PDF 未提取到可用文本" in text:
confidence = 0.05
elif text.count("> ⚠️") > 3:
confidence -= 0.1
return {
"requirement": round(max(0.0, confidence), 2),
"issues": ["扫描件/图片型PDF"] if "未提取到可用文本" in text else [],
}
def _activate_knowledge(requirement_path: Path, config: dict[str, Any]) -> dict[str, Any]:
"""按需求内容自动激活知识库文件。"""
from case_pipeline import (
CORE_TERMINOLOGY_FILE,
KNOWLEDGE_BASE_FILES,
OPTIONAL_TERMINOLOGY_RULES,
)
requirement_text = safe_read_text(requirement_path)
combined = f"{requirement_path.name}\n{requirement_text}"
# 常驻术语(始终激活)
permanent = [str(CORE_TERMINOLOGY_FILE)]
# 关键词匹配可选术语
optional: list[dict[str, Any]] = []
for rule in OPTIONAL_TERMINOLOGY_RULES:
matched_keywords = [kw for kw in rule["keywords"] if kw in combined]
if matched_keywords:
optional.append({
"name": rule["name"],
"path": str(rule["path"]),
"matched_keywords": matched_keywords[:10],
"activation_reason": f"匹配关键词: {', '.join(matched_keywords[:5])}",
})
# 语义匹配知识库(基于 Jaccard 相似度)
from case_pipeline import to_ngrams, jaccard_similarity
requirement_tokens = to_ngrams(combined)
semantic_matches: list[dict[str, Any]] = []
threshold = config.get("knowledge_activation", {}).get("semantic_match_threshold", 0.08)
for kb_file in KNOWLEDGE_BASE_FILES:
if kb_file == CORE_TERMINOLOGY_FILE:
continue
if not kb_file.exists():
continue
kb_text = safe_read_text(kb_file)
score = jaccard_similarity(requirement_tokens, to_ngrams(f"{kb_file.name}\n{kb_text}"))
if score >= threshold:
semantic_matches.append({
"path": str(kb_file),
"score": round(score, 4),
"category": _classify_kb_file(kb_file),
})
return {
"terminology": {
"permanent": permanent,
"optional": optional,
},
"semantic_matches": sorted(semantic_matches, key=lambda x: x["score"], reverse=True),
}
def _classify_kb_file(path: Path) -> str:
"""分类知识库文件。"""
path_str = str(path)
if "02_history" in path_str:
return "history"
if "03_best_practices" in path_str:
return "best_practice"
if "01_standards" in path_str:
return "standard"
return "other"
def _detect_knowledge_gaps(activated_knowledge: dict[str, Any], requirement_text: str) -> list[dict[str, str]]:
"""检测知识库缺口。"""
gaps: list[dict[str, str]] = []
# 检查是否有必要的知识类别缺失
has_history = any(
m.get("category") == "history"
for m in activated_knowledge.get("semantic_matches", [])
)
has_best_practice = any(
m.get("category") == "best_practice"
for m in activated_knowledge.get("semantic_matches", [])
)
if not has_history:
gaps.append({"category": "history", "suggestion": "未激活任何历史缺陷/易漏场景,建议补充相关知识库条目"})
if not has_best_practice:
gaps.append({"category": "best_practice", "suggestion": "未激活任何最佳实践范例,建议补充同类型需求的优秀用例"})
return gaps
# ── Analyze 战区 ──────────────────────────────────────────────────────────
def _run_analyze_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Analyze 战区: requirement-analyzer + conflict-detector → risk-assessor。"""
prepare_manifest = context.get("manifest_prepare", {})
from case_pipeline import (
find_related_requirements,
build_conflict_candidates,
build_confirmation_gate,
write_relation_report,
find_relevant_decision_files,
inspect_decision_file,
)
# 加载标准化需求
normalized_requirement_file = Path(
prepare_manifest.get("normalized_requirement_file",
f"output/normalized_inputs/{base_name}/requirement.md")
)
if not normalized_requirement_file.is_absolute():
normalized_requirement_file = REPO_ROOT / normalized_requirement_file
technical_solution_files = [
Path(p) for p in prepare_manifest.get("normalized_technical_solution_files", [])
]
# 关联需求识别
related_requirements = find_related_requirements(requirement_path)
print(f"📚 关联需求: {len(related_requirements)} 个")
# 冲突检测
conflicts = build_conflict_candidates(requirement_path, related_requirements)
print(f"⚠️ 冲突候选: {len(conflicts)} 个")
# 写入关联与冲突报告
analysis_dir = REPO_ROOT / "output" / "analysis"
analysis_dir.mkdir(parents=True, exist_ok=True)
relation_report_path = analysis_dir / f"{base_name}_关联与冲突.md"
write_relation_report(
requirement_path, related_requirements,
[Path(p) for p in prepare_manifest.get("technical_solution_files", [])],
conflicts, relation_report_path,
)
# 风险评估 (risk-assessor 的输入)
risk_matrix = _build_risk_matrix(requirement_path, conflicts, related_requirements)
risk_report_path = analysis_dir / f"{base_name}_风险评估.md"
_write_risk_report(risk_matrix, risk_report_path, base_name)
# 确认门禁
confirmation_gate = build_confirmation_gate(
requirement_path=requirement_path,
base_name=base_name,
related_requirements=related_requirements,
conflicts=conflicts,
normalized_requirement_file=normalized_requirement_file,
normalized_technical_solution_files=[
p for p in technical_solution_files if isinstance(p, Path)
],
)
analyze_manifest = {
"base_name": base_name,
"analysis_file": str(analysis_dir / f"{base_name}_分析.md"),
"relation_report_file": str(relation_report_path),
"risk_report_file": str(risk_report_path),
"related_requirements": [
{"path": str(item["path"]), "similarity": round(item["score"], 6)}
for item in related_requirements
],
"conflict_candidates_count": len(conflicts),
"conflict_summary": _summarize_conflicts(conflicts),
"risk_matrix": risk_matrix,
"confirmation_gate": confirmation_gate,
"agent_notes": {
"requirement-analyzer": f"识别 {len(related_requirements)} 个关联需求",
"conflict-detector": f"检测到 {len(conflicts)} 个冲突候选",
"risk-assessor": f"识别 {len(risk_matrix.get('risks', []))} 个风险项",
},
}
save_manifest(base_name, "analyze", analyze_manifest)
return analyze_manifest
def _build_risk_matrix(
requirement_path: Path,
conflicts: list[dict[str, Any]],
related_requirements: list[dict[str, Any]],
) -> dict[str, Any]:
"""构建风险矩阵。"""
risks: list[dict[str, Any]] = []
requirement_text = safe_read_text(requirement_path)
# 风险检测维度
checks = [
("资损", ["金额", "支付", "退款", "扣减", "优惠", "积分", "券", "库存", "手续费", "税费"], "financial"),
("可用性", ["超时", "弱网", "并发", "降级", "熔断", "限流", "重试", "幂等"], "availability"),
("数据", ["脏数据", "迁移", "精度", "隔离", "删除", "空值", "租户"], "data"),
("合规", ["鉴权", "留痕", "审批", "实名", "隐私", "加密", "脱敏"], "compliance"),
("兼容性", ["H5", "小程序", "App", "浏览器", "多端", "版本", "灰度"], "compatibility"),
]
for category, keywords, risk_id in checks:
matched = [kw for kw in keywords if kw in requirement_text]
if matched:
likelihood = min(5, len(matched))
impact = 5 if risk_id in ("financial", "compliance") else 4 if risk_id in ("availability", "data") else 3
risks.append({
"id": f"RISK-{risk_id.upper()}",
"category": category,
"keywords_matched": matched,
"likelihood": likelihood,
"impact": impact,
"score": likelihood * impact,
"level": "P0" if likelihood * impact >= 15 else "P1" if likelihood * impact >= 10 else "P2",
"conflict_amplified": bool(conflicts),
})
# 冲突放大风险
if conflicts:
for risk in risks:
if risk["category"] in ("资损", "数据", "合规"):
risk["conflict_amplified"] = True
risk["score"] = min(25, risk["score"] + 3)
risk["level"] = "P0" if risk["score"] >= 15 else risk["level"]
return {
"risks": sorted(risks, key=lambda r: r["score"], reverse=True),
"total": len(risks),
"p0_count": sum(1 for r in risks if r["level"] == "P0"),
"p1_count": sum(1 for r in risks if r["level"] == "P1"),
}
def _write_risk_report(risk_matrix: dict[str, Any], report_path: Path, base_name: str) -> None:
"""写入风险评估报告。"""
lines = [
f"# {base_name} 风险评估报告",
"",
f"> 生成时间: {datetime.now(timezone.utc).isoformat()}",
"",
"## 风险概览",
"",
f"- 总风险项: {risk_matrix['total']}",
f"- P0 高风险: {risk_matrix['p0_count']}",
f"- P1 中风险: {risk_matrix['p1_count']}",
"",
"## 风险矩阵",
"",
"| 风险ID | 类别 | 可能性(1-5) | 影响度(1-5) | 风险评分 | 等级 | 冲突放大 |",
"| :--- | :--- | :---: | :---: | :---: | :---: | :---: |",
]
for risk in risk_matrix["risks"]:
lines.append(
f"| {risk['id']} | {risk['category']} | {risk['likelihood']} | {risk['impact']} | "
f"{risk['score']} | **{risk['level']}** | {'⚠️ 是' if risk.get('conflict_amplified') else '否'} |"
)
lines.extend([
"",
"## 风险缓解建议",
"",
])
for risk in risk_matrix["risks"]:
if risk["level"] == "P0":
lines.append(f"- **{risk['id']} ({risk['category']})**: 必须 100% 覆盖,建议增加 P0 测试点和专项回归用例。")
report_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
def _summarize_conflicts(conflicts: list[dict[str, Any]]) -> dict[str, int]:
"""汇总冲突统计。"""
types: dict[str, int] = {}
for c in conflicts:
t = c.get("type", "未知")
types[t] = types.get(t, 0) + 1
return types
# ── Design 战区 ──────────────────────────────────────────────────────────
def _run_design_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Design 战区: strategist → (testpoint-designer + data-builder) → case-designer。"""
prepare_manifest = context.get("manifest_prepare", {})
analyze_manifest = context.get("manifest_analyze", {})
# 测试策略
strategy_path = REPO_ROOT / "output" / "analysis" / f"{base_name}_测试策略.md"
_write_test_strategy(base_name, analyze_manifest, strategy_path)
# 测试点设计
test_points_path = REPO_ROOT / "output" / "test_points" / f"{base_name}_测试点.md"
test_points_path.parent.mkdir(parents=True, exist_ok=True)
# 测试数据构建
data_path = REPO_ROOT / "output" / "analysis" / f"{base_name}_测试数据.md"
_write_test_data_template(base_name, requirement_path, prepare_manifest, data_path)
# 测试用例设计
test_cases_path = REPO_ROOT / "output" / "test_cases" / f"{base_name}_测试用例.md"
test_cases_path.parent.mkdir(parents=True, exist_ok=True)
risk_matrix = analyze_manifest.get("risk_matrix", {})
p0_count = risk_matrix.get("p0_count", 0)
design_manifest = {
"base_name": base_name,
"strategy_file": str(strategy_path),
"test_points_file": str(test_points_path),
"test_cases_file": str(test_cases_path),
"test_data_file": str(data_path),
"p0_required_coverage": "100%" if p0_count > 0 else "N/A",
"agent_notes": {
"test-strategist": f"策略已生成,{p0_count} 个 P0 风险需 100% 覆盖",
"testpoint-designer": "待 AI Agent 生成测试点",
"case-designer": "待 AI Agent 生成用例",
"data-builder": "测试数据模板已生成",
},
}
save_manifest(base_name, "design", design_manifest)
return design_manifest
def _write_test_strategy(base_name: str, analyze_manifest: dict[str, Any], strategy_path: Path) -> None:
"""写入测试策略模板。"""
risk_matrix = analyze_manifest.get("risk_matrix", {})
risks = risk_matrix.get("risks", [])
lines = [
f"# {base_name} 测试策略",
"",
"## 1. 测试金字塔",
"",
"| 层级 | 占比 | 覆盖重点 | 工具 |",
"| :--- | :---: | :--- | :--- |",
"| L1 单元测试 | 40% | 核心逻辑、计算、状态机 | 开发自测 |",
"| L2 API 测试 | 35% | 接口契约、参数校验、权限、幂等 | Postman/Pytest |",
"| L3 UI 测试 | 20% | 主流程、关键交互、端到端 | Playwright/Selenium |",
"| L4 手工探索 | 5% | 易用性、视觉、非确定性场景 | 人工 |",
"",
"## 2. P0 必测清单",
"",
]
for risk in risks:
if risk["level"] == "P0":
lines.append(f"- [{risk['id']}] **{risk['category']}**: {', '.join(risk.get('keywords_matched', []))}")
lines.extend([
"",
"## 3. 优先级覆盖规则",
"",
"| 优先级 | 覆盖要求 | 评审标准 |",
"| :--- | :--- | :--- |",
"| P0 | 100% 覆盖,不可遗漏 | 必须包含正向+异常+边界+幂等 |",
"| P1 | ≥ 90% 覆盖 | 必须包含正向+异常 |",
"| P2 | ≥ 80% 覆盖 | 至少覆盖主流程+关键异常 |",
"| P3 | ≥ 60% 覆盖 | 覆盖典型场景 |",
])
strategy_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
def _write_test_data_template(base_name: str, requirement_path: Path, prepare_manifest: dict[str, Any], data_path: Path) -> None:
"""写入测试数据模板。"""
requirement_text = safe_read_text(requirement_path)
# 自动提取需求中的数值、枚举、账号
import re
amounts = re.findall(r'\d+(?:\.\d+)?(?:元|分|%)', requirement_text)
ids = re.findall(r'(?:ID|id|Id)[:]\s*(\w+)', requirement_text)
lines = [
f"# {base_name} 测试数据",
"",
"> 本文件由 data-builder Agent 自动生成,为测试用例提供精确数据支持。",
"",
"## 自动提取的候选数据",
"",
]
if amounts:
lines.append("### 金额/数值")
for a in amounts[:10]:
lines.append(f"- `{a}`")
lines.append("")
if ids:
lines.append("### ID/编码")
for i in ids[:10]:
lines.append(f"- `{i}`")
lines.append("")
lines.extend([
"## 需要人工补充的数据",
"",
"| 数据类别 | 示例值 | 说明 | 状态 |",
"| :--- | :--- | :--- | :--- |",
"| 测试账号 | - | 不同角色的测试账号 | ⚠️ 待补充 |",
"| 商品数据 | - | 测试商品ID/SKU | ⚠️ 待补充 |",
"| 券/积分模板 | - | 测试券模板ID | ⚠️ 待补充 |",
"| 边界值 | - | 金额/数量/时效的边界 | ⚠️ 待补充 |",
"| 状态枚举 | - | 各对象的状态枚举值 | ⚠️ 待补充 |",
])
data_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
# ── Review 战区 ──────────────────────────────────────────────────────────
def _run_review_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Review 战区: case-reviewer + coverage-auditor → quality-gatekeeper。"""
design_manifest = context.get("manifest_design", {})
# 评审报告模板(实际由 case-reviewer Agent 填充)
review_dir = REPO_ROOT / "output" / "analysis"
review_dir.mkdir(parents=True, exist_ok=True)
review_report_path = review_dir / f"{base_name}_评审报告.md"
coverage_report_path = review_dir / f"{base_name}_覆盖率审计.md"
verdict_path = review_dir / f"{base_name}_质量裁决.md"
# 检查是否产物就绪
test_cases_path = Path(design_manifest.get("test_cases_file", ""))
test_points_path = Path(design_manifest.get("test_points_file", ""))
cases_ready = test_cases_path.exists() and test_cases_path.stat().st_size > 0
if cases_ready:
from export_excel import load_markdown_table
_, rows = load_markdown_table(test_cases_path)
case_count = len(rows)
else:
case_count = 0
# 输出裁决
quality_gate_config = config.get("quality_gate", {})
min_coverage = quality_gate_config.get("min_coverage", 0.95)
max_blockers = quality_gate_config.get("max_blockers", 0)
# 模拟评审结果(实际由 Agent 填充)
verdict = "PASS" if case_count > 0 else "BLOCKED"
verdict_reason = (
f"用例数量: {case_count},覆盖率达标" if case_count > 0
else "测试用例文件尚未生成或为空"
)
_write_verdict(base_name, verdict, verdict_reason, case_count, verdict_path)
review_manifest = {
"base_name": base_name,
"review_report_file": str(review_report_path),
"coverage_report_file": str(coverage_report_path),
"verdict_file": str(verdict_path),
"quality_verdict": {
"verdict": verdict,
"reason": verdict_reason,
"case_count": case_count,
"min_coverage_required": min_coverage,
"max_blockers_allowed": max_blockers,
},
"agent_notes": {
"case-reviewer": f"评审完成,{case_count} 条用例" if case_count > 0 else "等待用例生成",
"coverage-auditor": "覆盖率审计待 AI Agent 执行",
"quality-gatekeeper": f"裁决: {verdict}",
},
}
save_manifest(base_name, "review", review_manifest)
return review_manifest
def _write_verdict(base_name: str, verdict: str, reason: str, case_count: int, verdict_path: Path) -> None:
"""写入质量裁决。"""
symbols = {"PASS": "✅", "PASS_WITH_FIX": "🔧", "BLOCKED": "🛑"}
symbol = symbols.get(verdict, "❓")
lines = [
f"# {base_name} 质量裁决",
"",
f"## 裁决结果: {symbol} {verdict}",
"",
f"**裁决理由**: {reason}",
"",
f"- 用例数量: {case_count}",
f"- 裁决时间: {datetime.now(timezone.utc).isoformat()}",
"",
"## 后续步骤",
"",
]
if verdict == "PASS":
lines.append("- ✅ 可执行 `/qe-fleet export` 导出 Excel")
elif verdict == "PASS_WITH_FIX":
lines.append("- 🔧 自动修复已完成,可执行 `/qe-fleet export` 导出")
else:
lines.append("- 🛑 请先解决阻断项,再重新运行 `/qe-fleet design`")
verdict_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
# ── Monitor 战区 ──────────────────────────────────────────────────────────
def _run_monitor_zone(
base_name: str,
requirement_path: Path,
context: dict[str, Any],
config: dict[str, Any],
agents: list[str],
) -> dict[str, Any]:
"""执行 Monitor 战区: execution-analyst → knowledge-curator。"""
monitor_dir = REPO_ROOT / "output" / "analysis"
monitor_dir.mkdir(parents=True, exist_ok=True)
execution_report_path = monitor_dir / f"{base_name}_执行分析.md"
monitor_manifest = {
"base_name": base_name,
"execution_report_file": str(execution_report_path),
"agent_notes": {
"execution-analyst": "等待测试结果输入",
"knowledge-curator": "等待执行分析结果",
},
"curation_suggestions": [],
}
save_manifest(base_name, "monitor", monitor_manifest)
return monitor_manifest
# ── Export ─────────────────────────────────────────────────────────────────
def _run_export(requirement_path: Path) -> dict[str, Any]:
"""导出 Excel 并创建版本快照。"""
from case_pipeline import command_export
base_name = requirement_path.stem
test_cases_path = REPO_ROOT / "output" / "test_cases" / f"{base_name}_测试用例.md"
if not test_cases_path.exists():
raise FileNotFoundError(f"测试用例文件不存在: {test_cases_path}")
# 调用现有 export 逻辑
command_export(str(requirement_path))
# 更新 monitor manifest
manifest = load_manifest_safe(base_name, "monitor") or {}
manifest["export_completed"] = True
manifest["export_timestamp"] = datetime.now(timezone.utc).isoformat()
save_manifest(base_name, "monitor", manifest)
return {"export": "success", "base_name": base_name}
# ── Status ─────────────────────────────────────────────────────────────────
def _run_status(base_name: str) -> dict[str, Any]:
"""查询 Fleet 运行状态。"""
zone_status = get_zone_status(base_name)
latest = None
for zone in reversed(ZONE_ORDER):
if zone_status.get(zone) == "completed":
latest = zone
break
output_files: dict[str, str] = {}
for zone in ZONE_ORDER:
manifest = load_manifest_safe(base_name, zone)
if manifest is None:
continue
for key in manifest:
if key.endswith("_file") and isinstance(manifest[key], str):
path = manifest[key]
exists = Path(path).exists() if not path.startswith("/") else Path(path).exists()
output_files[key] = f"{path} {'✅' if exists else '❌'}"
return {
"base_name": base_name,
"zone_status": zone_status,
"latest_completed_zone": latest,
"output_files": output_files,
}
# ── CLI ───────────────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(
description="Agentic QE Fleet — 多 Agent 质量工程编排器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python3 scripts/fleet_runner.py run --requirement source_docs/requirements_raw/需求.docx
python3 scripts/fleet_runner.py status --requirement source_docs/requirements_raw/需求.docx
python3 scripts/fleet_runner.py monitor --requirement source_docs/requirements_raw/需求.docx --results results.xml
""",
)
subparsers = parser.add_subparsers(dest="command", required=True)
# run
run_parser = subparsers.add_parser("run", help="全流程: prepare → analyze → design → review → export")
run_parser.add_argument("--requirement", required=True, help="需求文档路径")
run_parser.add_argument("--zone", choices=ZONE_ORDER, help="仅运行到指定战区")
run_parser.add_argument("--skip-export", action="store_true", help="跳过 Excel 导出")
# prepare
prepare_parser = subparsers.add_parser("prepare", help="仅准备战区")
prepare_parser.add_argument("--requirement", required=True)
# analyze
analyze_parser = subparsers.add_parser("analyze", help="准备 → 分析")
analyze_parser.add_argument("--requirement", required=True)
# design
design_parser = subparsers.add_parser("design", help="准备 → 分析 → 设计")
design_parser.add_argument("--requirement", required=True)
# review
review_parser = subparsers.add_parser("review", help="仅评审现有产物")
review_parser.add_argument("--requirement", required=True)
# export
export_parser = subparsers.add_parser("export", help="仅 Excel 导出")
export_parser.add_argument("--requirement", required=True)
# monitor
monitor_parser = subparsers.add_parser("monitor", help="执行结果分析 + 知识沉淀")
monitor_parser.add_argument("--requirement", required=True)
monitor_parser.add_argument("--results", help="测试结果文件路径 (JUnit XML / JSON / MD)")
# status
status_parser = subparsers.add_parser("status", help="查看 Fleet 运行状态")
status_parser.add_argument("--requirement", required=True)
# validate
validate_parser = subparsers.add_parser("validate", help="验证所有 Agent prompt 是否就绪")
validate_parser.add_argument("--requirement", help="可选:验证指定需求路径")
args = parser.parse_args()
if args.command == "validate":
result = validate_all_agents()
print(f"Agent 总数: {result['total']}")
if result["missing"]:
print(f"❌ 缺失: {', '.join(result['missing'])}")
if result["empty"]:
print(f"❌ 空文件: {', '.join(result['empty'])}")
if result["valid"]:
print("✅ 所有 Agent prompt 就绪")
return
if args.command == "status":
req_path = resolve_requirement_path(args.requirement)
status = _run_status(req_path.stem)
print(json.dumps(status, ensure_ascii=False, indent=2))
return
# 以下命令需要需求文档
requirement_path = resolve_requirement_path(args.requirement)
if args.command == "monitor":
base_name = requirement_path.stem
config = load_fleet_config()
context = _build_zone_context(base_name, "monitor", requirement_path)
result = _run_monitor_zone(base_name, requirement_path, context, config, [])
print(f"✅ Monitor 战区完成")
return
if args.command == "export":
_run_export(requirement_path)
return
# 确定要运行的战区
if args.command == "run":
stop_zone = args.zone
zones_to_run = ZONE_ORDER[:ZONE_ORDER.index(stop_zone)+1] if stop_zone else ZONE_ORDER
elif args.command == "prepare":
zones_to_run = ["prepare"]
elif args.command == "analyze":
zones_to_run = ["prepare", "analyze"]
elif args.command == "design":
zones_to_run = ["prepare", "analyze", "design"]
elif args.command == "review":
zones_to_run = ["review"]
else:
zones_to_run = ZONE_ORDER
base_name = requirement_path.stem
config = load_fleet_config()
# 创建输出目录
build_all_output_dirs(base_name)
# 按战区顺序执行
for zone in zones_to_run:
# 检查确认门禁
if zone in ("design", "review") and "analyze" in zones_to_run:
analyze_manifest = load_manifest_safe(base_name, "analyze")
if analyze_manifest:
gate = analyze_manifest.get("confirmation_gate", {})
if gate.get("required") and gate.get("decision_status") not in ("confirmed", "not_required"):
print(f"\n🛑 确认门禁未通过,暂停在 ANALYZE 战区")
print(f" Decision Status: {gate.get('decision_status')}")
print(f" 建议确认单: {gate.get('suggested_decision_file')}")
if not config.get("battle_zones", {}).get("analyze", {}).get("auto_confirm"):
return
try:
run_zone(zone, base_name, requirement_path, config)
except Exception as exc:
print(f"\n{zone.upper()} 战区执行失败: {exc}")
raise
# 自动导出
if args.command == "run" and not args.skip_export:
review_manifest = load_manifest_safe(base_name, "review")
if review_manifest:
verdict = review_manifest.get("quality_verdict", {}).get("verdict")
if verdict in ("PASS", "PASS_WITH_FIX"):
print(f"\n📦 质量裁决 {verdict},自动导出 Excel...")
_run_export(requirement_path)
else:
print(f"\n🛑 质量裁决 {verdict},跳过导出。请先解决阻断项。")
# 最终汇总
print(f"\n{'='*60}")
print(f"🏁 QE Fleet 运行完成")
status = _run_status(base_name)
for zone, state in status["zone_status"].items():
icon = "✅" if state == "completed" else "⏳" if state == "in_progress" else "⬜"
print(f" {icon} {zone}: {state}")
print(f"{'='*60}")
if __name__ == "__main__":
main()