From d4c3d66efc7fd51f4f296f4de7b4a143f3d0578c Mon Sep 17 00:00:00 2001 From: xst Date: Mon, 13 Jul 2026 09:56:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9B=86=E6=88=90=20XMind=20=E6=80=9D?= =?UTF-8?q?=E7=BB=B4=E5=AF=BC=E5=9B=BE=E5=AF=BC=E5=87=BA=E5=88=B0=20QE=20F?= =?UTF-8?q?leet=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fleet_runner.py: _run_export() 在 Excel 导出后自动生成 XMind - fleet_runner.py: 新增 --skip-xmind 参数(run/export 命令均支持) - export_XMind.py: 按模块分组 → 用例编号/标题/优先级/前置条件/步骤/预期/备注树形结构 - XMind 输出目录: output/xmind_reports/ 用法: python scripts/fleet_runner.py export --requirement <需求> # Excel + XMind python scripts/fleet_runner.py export --requirement <需求> --skip-xmind # 仅 Excel python scripts/export_XMind.py --base-name <名称> # 独立 XMind 导出 --- output/xmind_reports/安徽运八需求.xmind | Bin 0 -> 17238 bytes scripts/export_XMind.py | 271 ++++++++++++++++++++++++ scripts/fleet_runner.py | 32 ++- scripts/模板.xmind | Bin 0 -> 9281 bytes 4 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 output/xmind_reports/安徽运八需求.xmind create mode 100644 scripts/export_XMind.py create mode 100644 scripts/模板.xmind diff --git a/output/xmind_reports/安徽运八需求.xmind b/output/xmind_reports/安徽运八需求.xmind new file mode 100644 index 0000000000000000000000000000000000000000..a07c110ffc420ee6699bc0f0e9fac0fcd93ba542 GIT binary patch literal 17238 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 list[str]: + stripped = line.strip() + if stripped.startswith("|"): + stripped = stripped[1:] + if stripped.endswith("|"): + stripped = stripped[:-1] + cells: list[str] = [] + current: list[str] = [] + escaped = False + for char in stripped: + if escaped: + current.append(char) + escaped = False + continue + if char == "\\": + escaped = True + continue + if char == "|": + cells.append("".join(current).strip()) + current = [] + continue + current.append(char) + if escaped: + current.append("\\") + cells.append("".join(current).strip()) + return cells + + +def load_markdown_table(source_file: Path) -> tuple[list[str], list[list[str]]]: + content = source_file.read_text(encoding="utf-8") + all_table_lines = [line.rstrip() for line in content.splitlines() if line.strip().startswith("|")] + + if len(all_table_lines) < 3: + raise ValueError(f"未在 {source_file} 中找到有效的 Markdown 表格。") + + separator_indices = [i for i, line in enumerate(all_table_lines) if SEPARATOR_RE.match(line.strip())] + if not separator_indices: + raise ValueError(f"{source_file} 中没有找到合法的 Markdown 表头分隔线。") + + headers = split_markdown_row(all_table_lines[0]) + header_values = split_markdown_row(all_table_lines[0]) + + rows: list[list[str]] = [] + for i, sep_idx in enumerate(separator_indices): + table_start = sep_idx - 1 + if table_start < 0: + continue + table_end = (separator_indices[i + 1] - 1) if i + 1 < len(separator_indices) else len(all_table_lines) + for line in all_table_lines[sep_idx + 1:table_end]: + row = split_markdown_row(line) + if len(row) != len(headers): + continue + if i > 0 and row == header_values: + continue + rows.append(row) + if not rows: + raise ValueError(f"{source_file} 的 Markdown 表格没有数据行。") + return headers, rows + + +def normalize_text(value: str) -> str: + return re.sub(r"", "\n", value or "").replace("\\n", "\n").strip() + + +def build_xmind_content(headers: list[str], rows: list[list[str]], topic_name: str) -> bytes: + ts = _now_ms() + nsmap = {None: XMAP_NS, "fo": FO_NS, "svg": SVG_NS, "xhtml": XHTML_NS, "xlink": XLINK_NS} + + def _el(tag, parent=None, **attrs): + elem = etree.Element(f"{{{XMAP_NS}}}{tag}", nsmap=nsmap, **attrs) + if parent is not None: + parent.append(elem) + return elem + + root = _el("xmap-content", version="2.0", timestamp=ts) + sheet = _el("sheet", parent=root, id=_new_id(), timestamp=ts) + _el("title", parent=sheet).text = "画布 1" + + root_topic = _el("topic", parent=sheet, id=_new_id(), timestamp=ts, + **{"structure-class": "org.xmind.ui.logic.right"}) + _el("title", parent=root_topic).text = topic_name + rc = _el("children", parent=root_topic) + rt = _el("topics", parent=rc, type="attached") + + # 按模块路径分组 + grouped: dict[str, list[dict[str, str]]] = {} + for row in rows: + case = {headers[i]: (row[i] or "").strip() for i in range(len(headers))} + module = case.get("模块", "") or "未分类" + grouped.setdefault(module, []).append(case) + + for module_name, cases in grouped.items(): + mtopic = _el("topic", parent=rt, id=_new_id(), timestamp=ts) + _el("title", parent=mtopic).text = module_name + mc = _el("children", parent=mtopic) + mtopics = _el("topics", parent=mc, type="attached") + + for case in cases: + case_id = case.get("用例编号", "") + case_title = case.get("用例标题", "") + priority = case.get("优先级", "") + case_type = case.get("类型", "") + + label_parts = ["tc", priority, case_type] + label = "-".join([p for p in label_parts if p]) + label = f"{label}:{case_title}" if case_title else label + + c_topic = _el("topic", parent=mtopics, id=_new_id(), timestamp=ts) + _el("title", parent=c_topic).text = label + cc = _el("children", parent=c_topic) + ct = _el("topics", parent=cc, type="attached") + + # 前置条件 + precond = normalize_text(case.get("前置条件", "")) + if precond: + pt = _el("topic", parent=ct, id=_new_id(), timestamp=ts) + _el("title", parent=pt).text = f"pc:{precond}" + + # 测试数据 + test_data = normalize_text(case.get("测试数据", "")) + if test_data: + dt = _el("topic", parent=ct, id=_new_id(), timestamp=ts) + _el("title", parent=dt).text = f"测试数据:{test_data}" + + # 步骤与预期结果 + steps_text = normalize_text(case.get("测试步骤", "")) + expected_text = normalize_text(case.get("预期结果", "")) + step_lines = [s.strip() for s in steps_text.split("\n") if s.strip()] if steps_text else [] + exp_lines = [e.strip() for e in expected_text.split("\n") if e.strip()] if expected_text else [] + max_len = max(len(step_lines), len(exp_lines), 1) + + for i in range(max_len): + step = step_lines[i] if i < len(step_lines) else "" + expected = exp_lines[i] if i < len(exp_lines) else "" + if step or expected: + st = _el("topic", parent=ct, id=_new_id(), timestamp=ts) + _el("title", parent=st).text = f"步骤:{step}" if step else "预期结果" + if expected: + sc = _el("children", parent=st) + sst = _el("topics", parent=sc, type="attached") + rst = _el("topic", parent=sst, id=_new_id(), timestamp=ts) + _el("title", parent=rst).text = f"预期:{expected}" + + # 备注 + remark = case.get("备注", "").strip() + if remark: + rmk = _el("topic", parent=ct, id=_new_id(), timestamp=ts) + _el("title", parent=rmk).text = f"备注:{remark}" + + return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True) + + +def build_manifest() -> bytes: + nsmap = {None: MANIFEST_NS} + + def _el(tag, parent=None, **attrs): + elem = etree.Element(f"{{{MANIFEST_NS}}}{tag}", nsmap=nsmap, **attrs) + if parent is not None: + parent.append(elem) + return elem + + root = _el("manifest", version="1.0") + _el("file-entry", parent=root, **{"full-path": "content.xml", "media-type": "text/xml"}) + return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True) + + +def build_meta() -> bytes: + nsmap = {None: XMAP_NS} + root = etree.Element(f"{{{XMAP_NS}}}meta", nsmap=nsmap, version="2.0") + return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True) + + +def export_markdown_to_xmind(source_path: Path, output_dir: Path, topic_name: str = "") -> Path: + headers, rows = load_markdown_table(source_path) + if not topic_name: + topic_name = source_path.stem.replace("_测试用例", "").replace("_分析", "") + + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / f"{topic_name}.xmind" + + content_xml = build_xmind_content(headers, rows, topic_name) + manifest_xml = build_manifest() + meta_xml = build_meta() + + with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: + zf.writestr("content.xml", content_xml) + zf.writestr("meta.xml", meta_xml) + zf.writestr("META-INF/manifest.xml", manifest_xml) + + return output_path + + +def resolve_source_file(source_file: str | None, base_name: str | None, input_dir: Path) -> Path: + if source_file: + path = Path(source_file) + return path if path.is_absolute() else REPO_ROOT / path + if base_name: + return input_dir / f"{base_name}_测试用例.md" + raise ValueError("必须指定 --source-file 或 --base-name") + + +def main() -> None: + parser = argparse.ArgumentParser(description="将 Markdown 测试用例导出为 XMind 思维导图。") + parser.add_argument("--source-file", help="测试用例 Markdown 文件路径") + parser.add_argument("--base-name", help="按 BASE_NAME 查找 output/test_cases/{BASE_NAME}_测试用例.md") + parser.add_argument("--topic-name", help="XMind 根主题名称(默认从文件名推断)", default="") + parser.add_argument("--input-dir", default=str(DEFAULT_INPUT_DIR), help="Markdown 所在目录") + parser.add_argument("--output-dir", default=str(DEFAULT_OUTPUT_DIR), help="XMind 输出目录") + args = parser.parse_args() + + if not args.source_file and not args.base_name: + parser.error("必须指定 --source-file 或 --base-name") + + input_dir = Path(args.input_dir) + if not input_dir.is_absolute(): + input_dir = REPO_ROOT / input_dir + output_dir = Path(args.output_dir) + if not output_dir.is_absolute(): + output_dir = REPO_ROOT / output_dir + + source_path = resolve_source_file(args.source_file, args.base_name, input_dir) + if not source_path.exists(): + raise FileNotFoundError(f"测试用例文件不存在:{source_path}") + + output_path = export_markdown_to_xmind(source_path, output_dir, args.topic_name) + headers, rows = load_markdown_table(source_path) + print(f"来源文件: {source_path}") + print(f"用例数量: {len(rows)}") + print(f"根主题: {args.topic_name or source_path.stem}") + print(f"XMind 已生成: {output_path}") + + +if __name__ == "__main__": + main() diff --git a/scripts/fleet_runner.py b/scripts/fleet_runner.py index 7bd63c2..4639df8 100644 --- a/scripts/fleet_runner.py +++ b/scripts/fleet_runner.py @@ -51,6 +51,7 @@ from case_pipeline import ( PROJECT_PROFILE_FILE, ) from export_excel import export_markdown_to_excel +from export_XMind import export_markdown_to_xmind # Fleet 自有模块 from fleet_manifest import ( @@ -1494,8 +1495,8 @@ def _run_monitor_zone( # ── Export ───────────────────────────────────────────────────────────────── -def _run_export(requirement_path: Path) -> dict[str, Any]: - """导出 Excel 并创建版本快照。""" +def _run_export(requirement_path: Path, skip_xmind: bool = False) -> dict[str, Any]: + """导出 Excel + XMind 并创建版本快照。""" from case_pipeline import command_export base_name = requirement_path.stem @@ -1504,16 +1505,29 @@ def _run_export(requirement_path: Path) -> dict[str, Any]: if not test_cases_path.exists(): raise FileNotFoundError(f"测试用例文件不存在: {test_cases_path}") - # 调用现有 export 逻辑 + # Excel 导出 + 版本快照 command_export(str(requirement_path)) + # XMind 思维导图导出 + result: dict[str, Any] = {"export": "success", "base_name": base_name, "excel": True} + if not skip_xmind: + try: + xmind_dir = REPO_ROOT / "output" / "xmind_reports" + xmind_path = export_markdown_to_xmind(test_cases_path, xmind_dir, base_name) + result["xmind"] = str(xmind_path) + print(f"🧠 XMind 已生成: {to_repo_relative(xmind_path)}") + except Exception as exc: + print(f"⚠️ XMind 导出失败(不影响 Excel): {exc}") + result["xmind"] = None + result["xmind_error"] = str(exc) + # 更新 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} + return result # ── Status ───────────────────────────────────────────────────────────────── @@ -1566,6 +1580,7 @@ def main() -> None: 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 导出") + run_parser.add_argument("--skip-xmind", action="store_true", help="跳过 XMind 导出") # prepare prepare_parser = subparsers.add_parser("prepare", help="仅准备战区") @@ -1584,8 +1599,9 @@ def main() -> None: review_parser.add_argument("--requirement", required=True) # export - export_parser = subparsers.add_parser("export", help="仅 Excel 导出") + export_parser = subparsers.add_parser("export", help="导出 Excel + XMind 思维导图") export_parser.add_argument("--requirement", required=True) + export_parser.add_argument("--skip-xmind", action="store_true", help="跳过 XMind 导出") # monitor monitor_parser = subparsers.add_parser("monitor", help="执行结果分析 + 知识沉淀") @@ -1631,7 +1647,7 @@ def main() -> None: return if args.command == "export": - _run_export(requirement_path) + _run_export(requirement_path, skip_xmind=getattr(args, "skip_xmind", False)) return # 确定要运行的战区 @@ -1685,8 +1701,8 @@ def main() -> None: 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) + print(f"\n📦 质量裁决 {verdict},自动导出 Excel + XMind...") + _run_export(requirement_path, skip_xmind=getattr(args, "skip_xmind", False)) else: print(f"\n🛑 质量裁决 {verdict},跳过导出。请先解决阻断项。") diff --git a/scripts/模板.xmind b/scripts/模板.xmind new file mode 100644 index 0000000000000000000000000000000000000000..69cb409ff89134b0052bb0982b8cf34820efc0e3 GIT binary patch literal 9281 zcmeHN2|SeR_kU?qZl%ypnv$Y1W-w!!p(sKjOWcZL3`VmsGiGE@*S@t|l7AcyfYe|@h7L?`x%xEH|+wFJ%|KIQT`TXDcyz|ccEay4zIp6b~^PV%7W)mjL zKoSxX(1VDaZP2Jg0{kWu7*47_WRh5vnD-!7AN*2a5@>WHm4bsIRn=e$1PY$&LZrCi zV74|!a4nbuo#8}raUxME1RRV)h3V^I1vC`EObQ(b^QKXBe8@zK3;1^O1Ye+4M=(GK zC7|}1w%|?=yT+U0PNnH#&7H^uJ(N09O9P7#{>H8&I}u3;J*)-I4SXX+0}W{eCk8V1Z&#sVSm*_s)Jv@0$xR( z22S{$ZZxVl#l?_H0tA*CsT*k+X=4#%I{?!oq!9vP=~`KMK+!+i{9Rz~KVyUv!RjEL8 zNNsIxguV3!gb|fSc4EMWsp`;~08U?zMyaW(AwZvKn2$SyO#0S9Az5MX=bCP%-i3n+(FRjs03ZQGlRW|4F&J&67E)7NT}@N-E2z@l2?T}$(FF(7 z@X~a4L%M2XywF-+8XhzxlTOkgY7)K2Tf&_{2DYN<Pf^a;7Lw&U|qr+*f19iXD1C0XC&GKMWVVoX^}j%w8%(T50dk@AC%6Z zdE*)0Gy?n+EubF&7+?>o-b7Ur)s2W(1*mohN*Y%tfEyx%M9_O#vbQ<(EEX|*2d)Ah zMV&1NInDWd6p$m`LeT-k7>zF>!aGy%e#jJupfqrfkML;#;dI6>5`iuRI9lxH&u&yg z28Ky}7vI7m|9j{jvk8)&=nMi4KFpUMR(KVv_?3j6>aF+SiCG|?cTYGC4Z|I*&`nVJHb;6ikQ z4?mofrzeSscVYlu5KI8qVbBUe1yi69d>C*r4^HtW0~>=W&FN`nu}ROlmMpBq0b>lj>MqRXbnv@6b6ma5C~2{=hG9Y zk1@YdBL({XH}R|ic>c6GqBS+f@~kn2=TRetm=v0wh;zEo^7~is!7Bl)hke)bv3~=G z-;%4?8FAZx1du=E6-7^|BaEB=YuqB(asJO-+b1+@s$(#pJoh*eU=|t1zY-jZkn)8|1P1Pp`s*lh9`F^PU!&*3+1|1Bp5mVd!gXf8u8k{2!3dY%LF<-rJ zOTTS*>XbR=sPW7T_Nz9}z1@6mU1@shB$xJAQ;M?v-hHUMb@Dx*UuJZ2NfB|?oU>1E z6S!quMMcUx7&ohWi~PKKd!H?y6K$KAzhERW|rj412qi^g(cF4H zNVZwL0oCAqVJV`1=yhX`TTp@==iR%CLAHJ#+9GMOL%*T_QRrcqUwPirT@^3x!|IEs z$2pwMeOmRVnIjc@VrtKzwf}bL;r`oZoj01^H#H9`JKFPKya*g@yB)61D(ewn{Ys9L zf4K;2#~wI{W=2Pw4Z4KSz=X9t;^slYvr6AA4EHVRf7#Jr5o(jYGtX{-==&k8Z<<{2 z{@7;$wezs8h8Md}%jq+Rd_Pc!+ND&plsc)A^=N31dG0d~W@}1`(>?cdZ0hxxy0ecv z!@}Y|TCn|f#elzTaaaq|6yzYozT>5dQp>c z8}#zxKzL{2E_$LYr?xgaX+l9r+(36?Z;@DVORCJ<7ad2ldA=X74teR=S#~X)5G>cv zQr>pXu_!Q6omrBV)t491(U{%qAa-R&M@7;Q@?iK(JKFV*2hA!;hCpR1fo9NorJ=-4 z?+2D~Sp27M_CqSt{UkXkH!SPYfKn}Azp4658I5fY1Xj``DM{`g93#uw((2MT8 zx!)X8FsyINHD_NKI^FUt`%Qh+D}B15ptgs+KLKXu&pV)ZuI<^5 z3MpOq{>iYn&lM45-ok;y4Q$mIXs0rF@ zFw?%K$B4dSf-ZAM9V}ZdHh*d<9C|2DC17Lhmz)^IV+`fU&(}> zjN;; zkQ&>`rP5yQ5lL&fS*C}NHGEt?PdEAZRnB&bziikSu#|aymhQvt59ZC{oKaw&)^`r~ za-Jd?A$L8}0X``<<(gvtLJy1G4G|90Csic>&|T8ypI9H)a#ljP z+Z4`E4w)jeJ%qQlQd%bHX6;&jQ)t|+;se&NgB#2|s48BtCL?~+qzV(v-)p+)_d0@h}jaF3Xc!EClY{S|$qBbv>qP4>d^sQtW z2N=^gUH-5PKGW@t^;3tZ2ck&P1g<&z;RI>cs@#p=%6j~=1C;_A>{+=Ln@-+F9 z^;3LmPU0nD(}F{5>8IT;?V+fZy^Gimy8E4YR!KGsVFqDtc;B@WX{6Gn_N* z-fElR0vEXIzLb}CFnOuDtWL@YaF|cGs^mw;v+j}G3St;`;_)@DvR>;=7h=1*R9~4l zIvN$K>^A1VjnZ01Fi(2!Go{WoO7cd4VkUb^BiB5~UfQbr^3*uiz08v-MbFF9ishwr zi&8hNf8Zp!xFyvs#t*kZe*2L`mqpoCXM25`j^^#RyUCLsxa`%lz2DXj57rN9+=qXV zw)qH8wkORCk$F)vC$gs1)&<=id*q=qnca|l@xfeIDKPy8sp&AX*0$2IOvj;6Ql+nZ z!xE;i?EDyi_2v1dHK*+}%mT;+$K;g)ntS$9?@Y97i&YLxrKwsp!Qeo6vd5ypP|kx_ zR#$TlzFt(u&Uqh^p5)l$J(tnuUDnxOuO38VSuQ*_T9kXFQ}BwY1Rw88EnZ+}W(?ZOc=W)#Ob~CiMaIhp_!5b2gPMY&nt;Uo%x@ zFx5^=-3zgy`j5;fid|bz&v8~__eK{7s@!@JpGxbT6 zx#rO@b~nY)D)i7tbXm%A{!Zv4j2R{4f2d_<%uA#xIW$|J`#G(Z4J+Z6JKf_KkuXAE~$DgHl zQ+E4(2op{ii9>pP$c@kIck-_qcO4ZA#>EbFKR!_!YkqFCd@cx5-n}K9`N=>B-_$4F7%Vt&uBH#cD*z!>Y-%rD>C1N^S`vkiF@A>H1)=P5Y9_imLn+ErC8dO`E#u{V$ELQi@-O9O{W<7?A-N=#mf zk{`d}LT@aH+w{CXtj^_~4RvlgmQrv}`_Z2-6sR50ia%*hsSgKug#-8=-V5vc%E8vH z4J-+$^11$CYgH3Ub_uic>C?M|$4luu8!%I^T0acs z+mqh=hqzjM(*S&^aXAR114^bYl+twz2&fvGXKGQeRffR*@UPQ?w)V8QZAnaT zM>EaO=JGA>&HG)e+7}VXgEn}jx62)QyJSn@7G3BF3=5^ zgd&e!E@_746lr%Xb#>3BU9Tj9NW4d-KmK`_Ef$(pkd}X*)ERTTvNT+!&lCi)o%#dk zyTJPgkKJh5c^?GMVJ#SLkHqTq*r&b?&J97uX*rW8PX-|>JyA7?|19Xk>}L;(Hs|zJ zPx3zV7_+M);5yUp4i5)?q@D0An3uej-m3MGqrOrV1>O&|i`UPEGv(NhB}YpN&No`W z>hN}Xy9(kzA4>g@d^@zPS@C1%%*45zxE92-eAX^JzOnWuKHL5rB_TS}EK1Vs;OerN zy7z6lTW(uhXQcN;l{wo|3mEyqjG=`)_Gk0zipzH{g!V4!(A7FpBNjZHJ0p1N&zwV3 zn7eh?mn_s;3En{=lXd26vkZ2G2=eqnr3q`ulD*P0Woz>YqY% z1;s33D!yofLH|#d0Y!Aan}-k$8qVo>g6b5DN^^m`6DbT(me9kx5=jI&s7caxDY$x* zNN`UlhC2@SDSI)T^B$e!XMi+5s8x^z`QmR+{rs4qyfvyg@Btp3cZqVo8ci~cpY zR-dZ@U(Ot#-H{pJDYyKi#6Ys;TS@bOe2s79AOGn!h6`0+D|mcy50}Kgp7GbJ9hGGI z(Z?7y<%_)3*YiIcu`n6@y5w}ywFwYZ1WrpcF>x7aiszaZPR< XuVHC62`B}EB*4c7q(#e@2=4v|OIy|N literal 0 HcmV?d00001