#!/usr/bin/env python3
"""Build final DOCX/TXT files from confirmed Markdown drafts."""
from __future__ import annotations
import argparse
import html
import re
import shutil
import subprocess
import tempfile
import zipfile
from pathlib import Path
from typing import Any
from common import ensure_dir, read_json, safe_filename
try:
from docx import Document
from docx.enum.section import WD_SECTION
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.shared import Cm, Inches, Pt, RGBColor
DOCX_AVAILABLE = True
except Exception:
DOCX_AVAILABLE = False
BLACK_RGB = "000000"
def strip_markdown_links(text: str) -> str:
text = re.sub(r"(?]+)>", r"\1", text)
return text
def parse_application_lines(md_path: Path) -> tuple[list[str], list[str]]:
lines = md_path.read_text(encoding="utf-8").splitlines()
fields = [line.strip() for line in lines if line.strip().startswith("➤")]
warnings = [line for line in fields if "待用户确认" in line]
return fields, warnings
def parse_application_field(md_path: Path, field_name: str) -> str:
if not md_path.exists():
return ""
prefix = f"➤{field_name}:"
for line in md_path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if stripped.startswith(prefix):
return stripped[len(prefix) :].strip()
return ""
def application_version(draft_dir: Path) -> str:
version = parse_application_field(draft_dir / "申请表信息.md", "版本号")
if "待用户确认" in version:
return ""
return version
def application_software_name(draft_dir: Path) -> str:
name = parse_application_field(draft_dir / "申请表信息.md", "软件全称")
if "待用户确认" in name:
return ""
return name
def write_application_txt(draft_dir: Path, out_dir: Path) -> tuple[Path | None, list[str]]:
md_path = draft_dir / "申请表信息.md"
if not md_path.exists():
return None, ["缺少草稿/申请表信息.md"]
fields, warnings = parse_application_lines(md_path)
out_path = out_dir / "申请表信息.txt"
out_path.write_text("\n".join(fields) + "\n", encoding="utf-8")
return out_path, warnings
def read_json_if_exists(path: Path) -> dict[str, Any]:
if not path.exists():
return {}
return read_json(path)
def confirmation_issues(workdir: Path) -> list[str]:
draft_dir = workdir / "草稿"
issues: list[str] = []
business = read_json_if_exists(draft_dir / "业务理解.json")
if not business or not business.get("user_confirmed"):
issues.append("业务理解尚未确认:请确认 草稿/业务理解.md 后记录 `business` 门禁")
selection = read_json_if_exists(draft_dir / "代码文件选择.json")
if not selection or not selection.get("user_confirmed"):
issues.append("代码文件选择尚未确认:请确认 草稿/代码文件选择.json 后记录 `code-selection` 门禁")
screenshot = read_json_if_exists(workdir / "截图方式确认.json")
if not screenshot.get("screenshot_method_confirmed"):
issues.append("截图方式尚未确认:请选择截图方式后记录 `screenshot-method` 门禁")
app_md = draft_dir / "申请表信息.md"
if app_md.exists():
_, warnings = parse_application_lines(app_md)
if warnings:
issues.append("申请表信息仍包含“待用户确认”字段")
else:
issues.append("缺少 草稿/申请表信息.md")
app_confirmation = read_json_if_exists(draft_dir / "申请表字段确认.json")
if not app_confirmation.get("application_fields_confirmed"):
issues.append("申请表字段尚未确认:请补全字段后记录 `application-fields` 门禁")
markdown_confirmation = read_json_if_exists(draft_dir / "最终生成确认.json")
if not markdown_confirmation.get("markdown_confirmed"):
issues.append("Markdown 草稿尚未最终确认:请确认全部草稿后记录 `markdown` 门禁")
return issues
def parse_code_pages(md_path: Path) -> list[tuple[int, list[str]]]:
pages: list[tuple[int, list[str]]] = []
current_no: int | None = None
current_lines: list[str] = []
in_fence = False
for raw in md_path.read_text(encoding="utf-8").splitlines():
page_match = re.match(r"^##\s+第\s*(\d+)\s*页", raw.strip())
if page_match:
if current_no is not None:
pages.append((current_no, current_lines))
current_no = int(page_match.group(1))
current_lines = []
in_fence = False
continue
if raw.strip().startswith("```"):
in_fence = not in_fence
continue
if current_no is not None and in_fence:
current_lines.append(raw)
if current_no is not None:
pages.append((current_no, current_lines))
return pages
def set_run_font(run: Any, name: str, size_pt: float) -> None:
run.font.name = name
run.font.size = Pt(size_pt)
try:
run.font.color.rgb = RGBColor(0, 0, 0)
except Exception:
pass
try:
run._element.rPr.rFonts.set(qn("w:eastAsia"), name)
except Exception:
pass
def set_normal_font(document: Any, name: str = "SimSun", size_pt: float = 10.5) -> None:
style = document.styles["Normal"]
style.font.name = name
style.font.size = Pt(size_pt)
try:
style.font.color.rgb = RGBColor(0, 0, 0)
except Exception:
pass
try:
style._element.rPr.rFonts.set(qn("w:eastAsia"), name)
except Exception:
pass
def set_style_black(document: Any) -> None:
for style_name in ("Normal", "Heading 1", "Heading 2", "Heading 3", "List Bullet", "List Number"):
try:
document.styles[style_name].font.color.rgb = RGBColor(0, 0, 0)
except Exception:
pass
def force_black_document(document: Any) -> None:
set_style_black(document)
containers = [document]
for section in document.sections:
containers.extend([section.header, section.footer])
for container in containers:
for paragraph in container.paragraphs:
for run in paragraph.runs:
try:
run.font.color.rgb = RGBColor(0, 0, 0)
except Exception:
pass
for table in container.tables:
for row in table.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
for run in paragraph.runs:
try:
run.font.color.rgb = RGBColor(0, 0, 0)
except Exception:
pass
def configure_a4(document: Any) -> None:
section = document.sections[0]
section.page_width = Cm(21)
section.page_height = Cm(29.7)
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(2.54)
def configure_code_a4(document: Any) -> None:
section = document.sections[0]
section.page_width = Cm(21)
section.page_height = Cm(29.7)
section.top_margin = Cm(2.0)
section.bottom_margin = Cm(2.0)
section.left_margin = Cm(2.5)
section.right_margin = Cm(2.0)
def add_page_field(paragraph: Any) -> None:
begin = OxmlElement("w:fldChar")
begin.set(qn("w:fldCharType"), "begin")
instr = OxmlElement("w:instrText")
instr.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
instr.text = " PAGE "
separate = OxmlElement("w:fldChar")
separate.set(qn("w:fldCharType"), "separate")
result = OxmlElement("w:t")
result.text = "1"
end = OxmlElement("w:fldChar")
end.set(qn("w:fldCharType"), "end")
for element in (begin, instr, separate, result, end):
run = paragraph.add_run()
run._r.append(element)
set_run_font(run, "SimSun", 8)
def set_code_header(document: Any, software_name: str, version: str) -> None:
section = document.sections[0]
section.header.is_linked_to_previous = False
header = section.header
header.paragraphs[0].text = "" if header.paragraphs else None
# Build a two-column header: software name on the left, page number on the right.
table = header.add_table(rows=1, cols=2, width=Cm(17.5))
table.autofit = True
left_cell = table.rows[0].cells[0]
right_cell = table.rows[0].cells[1]
left_para = left_cell.paragraphs[0]
left_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
left_para.paragraph_format.space_before = Pt(0)
left_para.paragraph_format.space_after = Pt(0)
left_para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
left_para.paragraph_format.line_spacing = Pt(12)
left_run = left_para.add_run(f"{software_name} {version}")
set_run_font(left_run, "SimSun", 8)
right_para = right_cell.paragraphs[0]
right_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT
right_para.paragraph_format.space_before = Pt(0)
right_para.paragraph_format.space_after = Pt(0)
right_para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
right_para.paragraph_format.line_spacing = Pt(12)
prefix = right_para.add_run("第 ")
set_run_font(prefix, "SimSun", 8)
add_page_field(right_para)
suffix = right_para.add_run(" 页")
set_run_font(suffix, "SimSun", 8)
# Remove borders from the header table
for cell in table.rows[0].cells:
tc_pr = cell._tc.get_or_add_tcPr()
tc_borders = OxmlElement("w:tcBorders")
for border_name in ("top", "left", "bottom", "right"):
border = OxmlElement(f"w:{border_name}")
border.set(qn("w:val"), "nil")
tc_borders.append(border)
tc_pr.append(tc_borders)
def build_code_docx_python(md_path: Path, out_path: Path, software_name: str, version: str) -> None:
pages = parse_code_pages(md_path)
if not pages:
raise RuntimeError(f"No code pages parsed from {md_path}")
document = Document()
configure_code_a4(document)
set_normal_font(document, "Consolas", 7.2)
set_style_black(document)
set_code_header(document, software_name, version)
# 后30页文档页码从31开始,实现前后文档连续编号1-60
start_page_no = pages[0][0] if pages else 1
if start_page_no != 1:
pg_num_type = OxmlElement("w:pgNumType")
pg_num_type.set(qn("w:start"), str(start_page_no))
document.sections[0]._sectPr.append(pg_num_type)
for index, (page_no, lines) in enumerate(pages):
for line in lines:
p = document.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
p.paragraph_format.line_spacing = Pt(14)
run = p.add_run(line if line else " ")
set_run_font(run, "Consolas", 7.2)
if index != len(pages) - 1:
# 嵌入式分页符:避免 add_page_break() 产生多余空段落导致空白页
run = p.add_run()
br = OxmlElement('w:br')
br.set(qn('w:type'), 'page')
run._r.append(br)
force_black_document(document)
document.save(out_path)
def paragraph_xml(text: str, font: str = "SimSun", size_half_points: int = 21, align: str | None = None, line_twips: int = 240) -> str:
align_xml = f'' if align else ""
escaped = html.escape(text)
return (
""
f"{align_xml}"
""
f""
f""
f""
f"{escaped}"
""
""
)
def page_break_xml() -> str:
return ''
def page_field_runs_xml() -> str:
return (
''
f''
''
''
f''
' PAGE '
''
f''
''
''
f''
'1'
''
f''
''
)
def header_xml(header_text: str) -> str:
"""Build a two-column header: software name left, page number right."""
escaped = html.escape(header_text)
# Use a borderless table for left/right alignment in header
return f"""
{escaped}
第
{page_field_runs_xml()}
页
"""
def minimal_docx(out_path: Path, body_xml: str, header_text: str | None = None, start_page: int = 1) -> None:
content_types = """
"""
rels = """
"""
header_rel = (
''
if header_text
else ""
)
doc_rels = f"""
{header_rel}"""
styles = f"""
"""
document = f"""
{body_xml}
{'' if header_text else ''}
{'' if start_page != 1 else ''}
"""
with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("[Content_Types].xml", content_types)
zf.writestr("_rels/.rels", rels)
zf.writestr("word/_rels/document.xml.rels", doc_rels)
zf.writestr("word/styles.xml", styles)
zf.writestr("word/document.xml", document)
if header_text:
zf.writestr("word/header1.xml", header_xml(header_text))
def force_black_xml(xml: str) -> str:
xml = re.sub(r"]*>", "", xml)
xml = xml.replace("", "")
xml = re.sub(r"]*/>", f'', xml)
def ensure_rpr_color(match: re.Match[str]) -> str:
value = match.group(0)
if "", f'')
xml = re.sub(r"]*>.*?", ensure_rpr_color, xml, flags=re.S)
xml = re.sub(r"(?!)", f'', xml)
return xml
def normalize_docx_text_color(docx_path: Path) -> None:
tmp_path = docx_path.with_suffix(docx_path.suffix + ".tmp")
color_xml_parts = (
"word/document.xml",
"word/styles.xml",
"word/numbering.xml",
"word/header",
"word/footer",
)
with zipfile.ZipFile(docx_path, "r") as src, zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as dst:
for item in src.infolist():
data = src.read(item.filename)
if item.filename.endswith(".xml") and item.filename.startswith(color_xml_parts):
text = data.decode("utf-8")
data = force_black_xml(text).encode("utf-8")
elif item.filename.endswith(".rels"):
text = data.decode("utf-8", errors="ignore")
if "hyperlink" in text:
text = re.sub(r'\s*]*Type="[^"]*/hyperlink"[^>]*/>', "", text)
data = text.encode("utf-8")
dst.writestr(item, data)
tmp_path.replace(docx_path)
def next_header_part(names: set[str]) -> tuple[str, str]:
index = 1
while f"word/header{index}.xml" in names:
index += 1
return f"word/header{index}.xml", f"header{index}.xml"
def unique_relationship_id(rels_xml: str, base: str = "rIdManualHeader") -> str:
if f'Id="{base}"' not in rels_xml:
return base
index = 2
while f'Id="{base}{index}"' in rels_xml:
index += 1
return f"{base}{index}"
def add_header_to_existing_docx(docx_path: Path, header_text: str) -> None:
"""Add the same two-column header used by code materials to an existing DOCX."""
tmp_path = docx_path.with_suffix(docx_path.suffix + ".tmp")
with zipfile.ZipFile(docx_path, "r") as src:
names = set(src.namelist())
header_part, header_target = next_header_part(names)
rels_xml = src.read("word/_rels/document.xml.rels").decode("utf-8")
rel_id = unique_relationship_id(rels_xml)
with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as dst:
for item in src.infolist():
data = src.read(item.filename)
if item.filename == "[Content_Types].xml":
text = data.decode("utf-8")
override = (
f''
)
if f'PartName="/{header_part}"' not in text:
text = text.replace("", f"{override}")
data = text.encode("utf-8")
elif item.filename == "word/_rels/document.xml.rels":
text = data.decode("utf-8")
relationship = (
f''
)
text = text.replace("", f"{relationship}")
data = text.encode("utf-8")
elif item.filename == "word/document.xml":
text = data.decode("utf-8")
if "xmlns:r=" not in text:
text = text.replace(
"'
if "]*/>", header_ref, text, count=1)
else:
text = re.sub(r"(]*>)", rf"\1{header_ref}", text, count=1)
data = text.encode("utf-8")
dst.writestr(item, data)
dst.writestr(header_part, header_xml(header_text))
tmp_path.replace(docx_path)
def build_code_docx_ooxml(md_path: Path, out_path: Path, software_name: str, version: str) -> None:
pages = parse_code_pages(md_path)
if not pages:
raise RuntimeError(f"No code pages parsed from {md_path}")
start_page_no = pages[0][0] if pages else 1
body: list[str] = []
for index, (page_no, lines) in enumerate(pages):
for line in lines:
body.append(paragraph_xml(line if line else " ", font="Consolas", size_half_points=14, line_twips=280))
if index != len(pages) - 1:
# 嵌入式分页符:嵌入最后一段的 run 避免多余空段落
last = body.pop()
last = last.replace('', '')
body.append(last)
minimal_docx(out_path, "\n".join(body), header_text=f"{software_name} {version}", start_page=start_page_no)
def add_markdown_table(document: Any, rows: list[list[str]]) -> None:
if not rows:
return
table = document.add_table(rows=1, cols=len(rows[0]))
table.style = "Table Grid"
for idx, text in enumerate(rows[0]):
table.rows[0].cells[idx].text = strip_markdown_links(text)
for row in rows[1:]:
cells = table.add_row().cells
for idx, text in enumerate(row[: len(cells)]):
cells[idx].text = strip_markdown_links(text)
def parse_table_line(line: str) -> list[str]:
return [cell.strip() for cell in line.strip().strip("|").split("|")]
def add_image(document: Any, image_path: Path) -> None:
if not image_path.exists():
p = document.add_paragraph()
run = p.add_run(f"[截图缺失:{image_path}]")
set_run_font(run, "SimSun", 10.5)
return
try:
document.add_picture(str(image_path), width=Inches(5.8))
except Exception:
p = document.add_paragraph()
run = p.add_run(f"[截图无法插入:{image_path}]")
set_run_font(run, "SimSun", 10.5)
def build_manual_docx_python(md_path: Path, out_path: Path, base_dir: Path, software_name: str, version: str) -> None:
document = Document()
configure_a4(document)
set_normal_font(document, "SimSun", 10.5)
set_style_black(document)
set_code_header(document, software_name, version)
lines = md_path.read_text(encoding="utf-8").splitlines()
table_buf: list[list[str]] = []
in_fence = False
def flush_table() -> None:
nonlocal table_buf
if table_buf:
data = [row for row in table_buf if not all(re.fullmatch(r":?-{3,}:?", cell) for cell in row)]
add_markdown_table(document, data)
table_buf = []
for line in lines:
stripped = line.strip()
stripped = strip_markdown_links(stripped)
if stripped.startswith("```"):
flush_table()
in_fence = not in_fence
continue
if in_fence:
continue
if stripped.startswith("", "【截图预留:请在此处插入当前功能页面或操作结果截图。】", text)
text = strip_markdown_links(text)
if code_mode:
text = re.sub(r"(?=^##\s+第\s*\d+\s*页)", r"\n\\newpage\n", text, flags=re.M)
if code_mode or "STOP_FOR_USER" in original_text:
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as tmp:
tmp.write(text)
tmp_name = tmp.name
source = Path(tmp_name)
try:
subprocess.run(["pandoc", "-f", "markdown", "-t", "docx", str(source), "-o", str(out_path)], check=True)
finally:
if tmp_name:
Path(tmp_name).unlink(missing_ok=True)
def build_code_docx(md_path: Path, out_path: Path, software_name: str, version: str) -> None:
if DOCX_AVAILABLE:
build_code_docx_python(md_path, out_path, software_name, version)
else:
build_code_docx_ooxml(md_path, out_path, software_name, version)
normalize_docx_text_color(out_path)
def build_manual_docx(md_path: Path, out_path: Path, base_dir: Path, software_name: str, version: str) -> None:
if DOCX_AVAILABLE:
build_manual_docx_python(md_path, out_path, base_dir, software_name, version)
else:
build_with_pandoc(md_path, out_path, code_mode=False)
add_header_to_existing_docx(out_path, f"{software_name} {version}")
normalize_docx_text_color(out_path)
def run_command(command: list[str], cwd: Path | None = None, timeout: int = 60) -> tuple[int, str]:
try:
completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True, timeout=timeout)
return completed.returncode, (completed.stdout + completed.stderr).strip()
except Exception as exc:
return 99, str(exc)
def docx_checks(skill_dir: Path, outputs: list[Path]) -> list[str]:
notes: list[str] = []
env_script = skill_dir / "vendor/docx-toolkit/scripts/env_check.sh"
preview_script = skill_dir / "vendor/docx-toolkit/scripts/docx_preview.sh"
if env_script.exists():
code, output = run_command(["bash", str(env_script)], cwd=env_script.parent.parent, timeout=30)
status = "READY" if code == 0 else "NOT READY"
first_lines = "\n".join(output.splitlines()[:12])
notes.append(f"DOCX env: {status}\n\n```text\n{first_lines}\n```")
else:
notes.append("DOCX env: vendor script missing")
if preview_script.exists():
for out in outputs:
code, output = run_command(["bash", str(preview_script), str(out)], timeout=45)
first_lines = "\n".join(output.splitlines()[:8])
notes.append(f"Preview {out.name}: exit={code}\n\n```text\n{first_lines}\n```")
return notes
def build_all(workdir: Path, software_name: str, version: str, skip_preview: bool) -> dict[str, Any]:
workdir = ensure_dir(workdir)
draft_dir = workdir / "草稿"
final_dir = ensure_dir(workdir / "正式资料")
app_name = application_software_name(draft_dir)
app_version = application_version(draft_dir)
final_software_name = app_name or software_name
final_version = app_version or version
safe_name = safe_filename(final_software_name)
outputs: list[Path] = []
warnings: list[str] = []
if app_name and app_name != software_name:
warnings.append(f"命令参数软件名称为 {software_name},正式资料已按申请表信息软件名称 {app_name} 生成")
if app_version and app_version != version:
warnings.append(f"命令参数版本号为 {version},正式资料已按申请表信息版本号 {app_version} 生成")
screenshot_confirmation = read_json_if_exists(workdir / "截图方式确认.json")
screenshot_method = screenshot_confirmation.get("screenshot_method")
screenshot_manifest = workdir / "截图/截图清单.json"
if screenshot_method == "skip":
warnings.append("用户选择暂不截图;操作手册已保留截图预留位置")
elif screenshot_method and not screenshot_manifest.exists():
warnings.append("操作手册截图未生成或未插入;操作手册应保留截图预留位置")
elif screenshot_manifest.exists():
screenshots = read_json_if_exists(screenshot_manifest).get("screenshots") or []
if not screenshots:
warnings.append("操作手册截图清单为空;操作手册应保留截图预留位置")
app_txt, app_warnings = write_application_txt(draft_dir, final_dir)
if app_txt:
outputs.append(app_txt)
warnings.extend(app_warnings)
code_specs = [
("代码-前30页.md", f"{safe_name}-代码(前30页).docx"),
("代码-后30页.md", f"{safe_name}-代码(后30页).docx"),
("代码-全部.md", f"{safe_name}-代码(全部).docx"),
]
for md_name, docx_name in code_specs:
md_path = draft_dir / md_name
if md_path.exists():
out_path = final_dir / docx_name
build_code_docx(md_path, out_path, final_software_name, final_version)
outputs.append(out_path)
manual_md = draft_dir / "操作手册.md"
if manual_md.exists():
manual_out = final_dir / f"{safe_name}_操作手册.docx"
manual_source = manual_md
tmp_manual: Path | None = None
if app_name and app_name != software_name:
text = manual_md.read_text(encoding="utf-8").replace(software_name, app_name)
with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as tmp:
tmp.write(text)
tmp_manual = Path(tmp.name)
manual_source = tmp_manual
try:
build_manual_docx(manual_source, manual_out, draft_dir, final_software_name, final_version)
finally:
if tmp_manual:
tmp_manual.unlink(missing_ok=True)
outputs.append(manual_out)
else:
warnings.append("缺少草稿/操作手册.md")
skill_dir = Path(__file__).resolve().parents[1]
notes = [] if skip_preview else docx_checks(skill_dir, [p for p in outputs if p.suffix.lower() == ".docx"])
report = write_report(final_dir, outputs, warnings, notes)
return {"outputs": [str(p) for p in outputs], "warnings": warnings, "report": str(report)}
def write_report(workdir: Path, outputs: list[Path], warnings: list[str], notes: list[str]) -> Path:
report = workdir / "生成报告.md"
lines = ["# 生成报告", "", "## 输出文件", ""]
for path in outputs:
size = path.stat().st_size if path.exists() else 0
lines.append(f"- `{path.name}` ({size} bytes)")
lines.extend(["", "## 警告", ""])
if warnings:
lines.extend(f"- {warning}" for warning in warnings)
else:
lines.append("- 无")
lines.extend(["", "## DOCX 校验", ""])
if notes:
lines.extend(notes)
else:
lines.append("- 已跳过预览校验")
report.write_text("\n".join(lines) + "\n", encoding="utf-8")
return report
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--workdir", default="软件著作权申请资料")
parser.add_argument("--software-name", required=True)
parser.add_argument("--version", default="V1.0")
parser.add_argument("--skip-preview", action="store_true")
args = parser.parse_args()
workdir = Path(args.workdir)
issues = confirmation_issues(workdir)
if issues:
print("STOP_FOR_USER")
print("NEXT_ACTION: 正式 Word/TXT 生成前必须完成以下确认:")
for issue in issues:
print(f"- {issue}")
raise SystemExit(2)
result = build_all(workdir, args.software_name, args.version, args.skip_preview)
print(f"OK final materials: {Path(args.workdir) / '正式资料'}")
for output in result["outputs"]:
print(output)
if result["warnings"]:
print("Warnings:")
for warning in result["warnings"]:
print(f"- {warning}")
print(f"Report: {result['report']}")
if __name__ == "__main__":
main()