"""DevHarness 单一命令行入口。 子命令: check 检查必需文件、核心文档和已有任务快照结构 sync 从 Gitea Wiki 单向导出或校验核心 docs 镜像 archive 显式在 Gitea Wiki 创建可选任务快照 export 人工按需把已有 Wiki 任务快照导出到 docs/task 各子命令的实现逻辑取自原来的 check_harness.py、sync_wiki_docs.py、 new_task_archive.py 和 export_task_archives.py,行为未改变。 """ from __future__ import annotations import argparse import re from datetime import date from pathlib import Path from typing import Any from wiki_docs import ( DEFAULT_CONFIG, WikiClient, WikiDocsError, dirty_paths, load_config, parse_mirror, sync_all, write_mirror, ) # ---------------------------------------------------------------- 结构检查 ROOT = Path(__file__).resolve().parents[1] CORE_PAGE_PATHS = { "Home": "docs/README.md", "Project-Profile": "docs/00-project-profile.md", "Development-Workflow": "docs/01-workflow.md", "Architecture-and-Code-Map": "docs/02-architecture-and-code-map.md", "Business-Rules-and-Glossary": "docs/03-business-rules-and-glossary.md", "Local-Development-and-Verification": ( "docs/04-local-development-and-verification.md" ), "Common-Changes": "docs/05-common-changes.md", "Troubleshooting": "docs/06-troubleshooting.md", "New-Project-Documentation-Setup": ( "docs/07-new-project-documentation-setup.md" ), "Existing-Project-Adoption-Guide": ( "docs/08-existing-project-adoption.md" ), "Product-Requirements-Overview": ( "docs/09-product-requirements-overview.md" ), "Delivery-Documentation-Guide": "docs/delivery/README.md", "Audience-Document-Template": ( "docs/delivery/audience-document-template.md" ), "Task-Archive-Template": "docs/templates/task-archive.md", } CORE_DOCUMENT_REQUIREMENTS = { "docs/README.md": ( "## 第一次阅读", "## 五分钟开始", "## 简单修改从哪里开始", "## 事实来源", ), "docs/00-project-profile.md": ( "## 基本信息", "## 项目治理模式", "## DevHarness 来源与基线", "## 子项目与交付单元", "## 技术栈与运行环境", "## 阅读入口", "## 常用命令", "## 环境、配置与凭据", ), "docs/01-workflow.md": ( "## 语言与术语", "## Gitea 交互与工单最小读取", "## 新项目 Wiki 初始化门禁", "## 按治理模式选择最小门禁", "### 不可裁剪底线", "### 先判断是否需要工单", "### 再判断设计证据", "### 线上原型审核与按需导出", "## 面向初级维护者的修改边界", "## 每个任务的文档影响", "## 需求记录与流转", "## 稳定文档与可选历史快照", "## 自然语言快捷指令", "## 效率与范围控制", "### 严格控制范围", "### 渐进执行和修复", "### 复用已验证事实", "### 明确停止条件", ), "docs/02-architecture-and-code-map.md": ( "## 项目定位", "## 代码地图", "## 两条主要执行路径", "## 不可破坏的边界", ), "docs/03-business-rules-and-glossary.md": ( "## 核心术语", "## 工单状态", "## 稳定业务规则", "## 新项目需要补充什么", ), "docs/04-local-development-and-verification.md": ( "## 环境要求", "## Windows PowerShell 与 UTF-8", "### PowerShell 语法与外部命令", "## 第一次运行", "## 常用调试方式", "## 完成修改前", ), "docs/05-common-changes.md": ( "## 风险分级", "## 修改 Wiki 文案", "## 调整 Harness 检查", "## 看懂 Agent 的修改", ), "docs/06-troubleshooting.md": ( "## 排查顺序", "## 必须停止的情况", ), "docs/07-new-project-documentation-setup.md": ( "## 初始化顺序", "#### 选择治理模式", "#### 需求总览启用条件", "### 2. 选择建设基线", "#### 工程基线裁剪", "#### 判断案例", "### 3. 识别子项目与交付单元", "### 7. 确定交付对象和文档", "#### 在线创建与回读门禁", "## 完成标准", ), "docs/08-existing-project-adoption.md": ( "## 与新项目初始化的区别", "## 接入前只读盘点", "## 已有内容保护原则", "## 多应用单仓库判断", "### 适合继续单仓库", "### 可以考虑拆仓", "### 保持单仓库时的最小规则", "## 增量接入顺序", "## 后续升级", "### 升级步骤", "### 可复制升级指令", "## 冲突处理和停止条件", "## 可复制 Agent 指令", "### 只分析", "### 方案确认后实施", "## 最小验收清单", "## 回退原则", ), "docs/09-product-requirements-overview.md": ( "## 本页用途", "## 事实来源边界", "## 当前需求索引", "## 登记规则", "## 原型与设计资产", "### 原型门禁", "### 线上原型与按需 HTML 快照", "### 原型确认记录", "## 状态规则", "## 更新时机", "## 最小验收清单", ), "docs/delivery/README.md": ( "## 什么时候需要交付文档", "## 受众与文档选择", "## 内部文档与交付文档边界", "## 编写和维护流程", "## 最小验收清单", ), "docs/delivery/audience-document-template.md": ( "## 文档信息", "## 目的与适用范围", "## 前置条件", "## 操作步骤", "## 常见错误与恢复", "## 安全与权限", "## 已知限制", "## 支持与升级处理", "## 版本记录", "## 交付前检查", ), } REQUIRED_FILES = ( "AGENTS.md", "CLAUDE.md", "README.md", "docs/00-project-profile.md", "docs/01-workflow.md", "docs/templates/deployment.md", "docs/templates/task-archive.md", *CORE_DOCUMENT_REQUIREMENTS, "wiki-docs.json", "dev_scripts/wiki_docs.py", "dev_scripts/harness.py", ".gitea/issue_template/epic.md", ".gitea/issue_template/mvp.md", ".gitea/issue_template/task.md", ) ARCHIVE_HEADINGS = ( "## 背景与目标", "## 最终方案", "## 修改文件", "## 验收结果", "## 测试", "## 相关提交", ) def check_required_files(errors: list[str]) -> None: for relative_path in REQUIRED_FILES: if not (ROOT / relative_path).is_file(): errors.append(f"缺少必需文件:{relative_path}") def check_project_profile(errors: list[str], warnings: list[str], strict: bool) -> None: profile = ROOT / "docs" / "00-project-profile.md" if not profile.is_file(): return if "<填写" in profile.read_text(encoding="utf-8"): message = "项目档案仍有未填写内容" (errors if strict else warnings).append(message) def check_archives(errors: list[str]) -> None: task_dir = ROOT / "docs" / "task" for path in task_dir.glob("*.md"): if not re.match(r"^\d+-.+\.md$", path.name): errors.append(f"归档文件名不符合 <编号>-<标题>.md:{path.name}") content = path.read_text(encoding="utf-8") try: metadata, _ = parse_mirror(content) except WikiDocsError as exc: errors.append(f"{path.name} 的任务镜像无效:{exc}") continue if re.fullmatch(r"Task-\d+-.+", metadata.get("wiki_page", "")) is None: errors.append(f"{path.name} 的 wiki_page 不是任务归档页面") if re.fullmatch(r"[0-9a-f]{40,64}", metadata.get("wiki_revision", "")) is None: errors.append(f"{path.name} 的 wiki_revision 无效") for heading in ARCHIVE_HEADINGS: if heading not in content: errors.append(f"{path.name} 缺少章节:{heading}") if "**未验证部分**:" not in content: errors.append(f"{path.name} 没有记录未验证部分") def missing_sections(content: str, required: tuple[str, ...]) -> list[str]: return [section for section in required if section not in content] def check_core_documents(errors: list[str], root: Path = ROOT) -> None: """检查初级维护者所需主题页的固定结构。""" for relative_path, required in CORE_DOCUMENT_REQUIREMENTS.items(): path = root / relative_path if not path.is_file(): continue try: _, body = parse_mirror(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, WikiDocsError): continue for section in missing_sections(body, required): errors.append(f"{relative_path} 缺少核心章节:{section}") def check_task_template(errors: list[str], root: Path = ROOT) -> None: path = root / ".gitea" / "issue_template" / "task.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") required = ( "## 依赖与并行", "- 前置工单:无 / #编号", "- 是否允许与前置工单并行:是 / 否", "- 原因:", "## 子项目影响", "- 仅影响的子项目 / 交付单元:", "- 是否跨子项目:是 / 否", "- 是否修改共享接口或契约:是 / 否;唯一事实来源:", "- 各子项目需要执行的验证:", "## 原始需求", "- 来源:用户对话 / Gitea / 其他", "- 提出时间:", "- 关键原话或脱敏摘要:", "## 需求变化记录", "| 日期 | 变化内容 | 原因 | 用户确认 |", "## 设计与原型门禁", "- 修改类型:纯显示文案 / 小范围 UI / 新组件 / 新页面或独立用户功能 / 重大交互或导航 / 非 UI / 恢复既有行为的 Bug", "- 所需设计证据:无 / 标注截图 / 低保真图 / 已确认原型 / 架构、API、数据、状态或流程设计 / 原设计或复现证据", "- 可编辑设计源、线上原型链接和访问检查:", "- 审核版本、revision、复制版本或确认日期及识别方式:", "- 本地 HTML 导出:未要求 / 用户明确要求 / 项目规则要求", "- 本地 HTML 路径、版本和资源检查(仅显式导出时填写):", "- 状态:无 / 草稿 / 已确认 / 已废弃", "- 确认人、确认时间和覆盖范围:", "- 无需 UI 原型或无需任何原型的原因:", "## 文档影响", "- [ ] 不影响长期文档,原因:", "- [ ] 更新架构与代码地图", "- [ ] 更新业务规则与术语", "- [ ] 更新常见修改或故障排查", "## 交付文档影响", "- [ ] 无交付文档影响,原因:", "- [ ] 更新已有交付文档,受众与页面:", "- [ ] 新增交付文档,受众与页面:", "- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:", "## 任务记录与可选快照", "- 单次任务事实来源:当前 Gitea 工单正文与评论", "- [ ] 默认不创建任务快照", "- [ ] 用户明确要求专项快照;用途和范围:", "- [ ] 项目专用规则要求任务快照;规则入口:", ) for section in missing_sections(content, required): errors.append(f"单元任务模板缺少:{section}") def check_agent_efficiency_rules(errors: list[str], root: Path = ROOT) -> None: path = root / "AGENTS.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") required = ( "### 效率与范围控制", "#### 严格控制范围", "#### 渐进执行和修复", "#### 复用已验证事实", "#### 明确停止条件", "需要工单时,单元任务是正式实施单位", "高风险修改必须停止", "用户没有明确验收通过前不得关闭", "只有长期事实变化时才修改 Wiki", "需要工单的任务以 Gitea 工单作为需求、变化、实现、测试、提交和验收的事实来源", "默认不创建任务归档", "### Gitea 交互与工单最小读取", "查询、创建、更新、评论、状态变更及关闭操作", "优先关注当前状态、最新评论和首个未完成步骤", "连接器不支持评论分页或增量读取时允许读取完整工单", "不得为规避完整读取而新增本地工单、缓存或第二事实来源", "### 新项目 Wiki 初始化门禁", "`Home` 不存在时必须先创建 `Home`", "不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据", "提交只包含当前任务相关文件", "不得仅为设置编码重复启动一层 PowerShell", "不得默认套用 Bash", "复杂正则优先使用变量或 `rg -e`", "单引号 PowerShell here-string", "`foreach`、`if` 等语句块", "使用真实目录配合 `-g/--glob`", "文件解码和控制台输出分别处理", "不得默认使用 `-ExecutionPolicy Bypass`", "### 按治理模式选择最小设计证据", "轻量模式的小 Bug、局部样式或布局", "明确为虚构的测试数据无需形式化脱敏", "用户可以使用中文、英文或合理的中英混合语言交流", "默认使用中文分析、回复、编写工单和维护内部项目文档", "日志和错误原文保持原样", "按该次要求执行", "完整独立需求、新页面、重大交互或导航变化", "`prototypes/<工单号>/<版本>/index.html`", "默认直接通过 Quant-UX 或其他设计工具的线上链接审核", "已确认的本地快照不得原位覆盖", "`导出原型 #N`", "`导出全部原型`", "### 自然语言快捷指令", "`只分析`", "`建工单`", "`执行工单 #N`", "`建工单并做`", "`继续工单 #N`", "`检查工单 #N`", "`同步文档`", "`导出原型 #N`", "`导出全部原型`", "`导出任务归档`", "`导出全部任务归档`", "`#N 验收通过`", "### 需求记录与流转", "不得臆造用户原话", "不复制完整聊天", "Gitea 工单全文不导出到仓库", ) for section in missing_sections(content, required): errors.append(f"AGENTS.md 缺少:{section}") def check_repository_readme(errors: list[str], root: Path = ROOT) -> None: """检查快速开始包含线上 Wiki 初始化顺序和产品编码门禁。""" path = root / "README.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") required = ( "创建 Gitea 远端仓库并推送当前引导提交,启用工单和 Wiki", "优先使用已配置的 Gitea MCP", "`Home` 不存在时先创建并回读 `Home`", "本地 `docs/` 的存在不能证明线上 Wiki 已初始化", "python dev_scripts/harness.py sync --verify", "需要工单的任务以 Gitea 工单为单次任务事实来源", "默认不创建任务归档", ) for section in missing_sections(content, required): errors.append(f"README.md 缺少:{section}") remote_index = content.find("创建 Gitea 远端仓库") wiki_index = content.find("`Home` 不存在时先创建") if remote_index < 0 or wiki_index < 0 or remote_index > wiki_index: errors.append("README.md 必须先创建 Gitea 远端,再创建 Wiki Home") def check_claude_code_entry(errors: list[str], root: Path = ROOT) -> None: """检查 Claude Code 入口直接复用共同 Agent 规则。""" path = root / "CLAUDE.md" if not path.is_file(): return content = path.read_text(encoding="utf-8") lines = {line.strip() for line in content.splitlines()} if "@AGENTS.md" not in lines: errors.append("CLAUDE.md 缺少独立的 @AGENTS.md 导入") required = ( "共同规则事实来源", "只记录 Claude Code 特有", "只修改 `AGENTS.md`", "## 模型路由", "## Agent 交接", "## Haiku 只读约束", "当前模型足以完成任务时不升级模型", "Opus 输出方案后必须等待用户确认", "不让 Haiku 决定最终根因", "只读必须通过子 Agent 工具权限实现", ) for section in missing_sections(content, required): errors.append(f"CLAUDE.md 缺少:{section}") def core_mapping_errors(configured_mappings: dict[str, str]) -> list[str]: errors: list[str] = [] for page, expected_path in CORE_PAGE_PATHS.items(): if configured_mappings.get(page) != expected_path: errors.append( f"核心 Wiki 页面映射缺失或路径错误:{page} -> {expected_path}" ) return errors def check_wiki_mirrors(errors: list[str]) -> None: """检查核心映射与镜像头;任务快照由 check_archives 单独检查。""" try: config = load_config() except WikiDocsError as exc: errors.append(str(exc)) return configured_mappings = {mapping.page: mapping.path for mapping in config.mappings} errors.extend(core_mapping_errors(configured_mappings)) mapped_paths = {mapping.path for mapping in config.mappings} actual_paths = { path.relative_to(ROOT).as_posix() for path in (ROOT / "docs").rglob("*.md") if path.parent != ROOT / "docs" / "task" } for path in sorted(actual_paths - mapped_paths): errors.append(f"docs 中存在未登记的 Wiki 镜像:{path}") for mapping in config.mappings: path = ROOT / mapping.path if not path.is_file(): errors.append(f"缺少 Wiki 镜像:{mapping.path}") continue try: metadata, _ = parse_mirror(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, WikiDocsError) as exc: errors.append(f"Wiki 镜像无效 {mapping.path}:{exc}") continue if metadata.get("generated") != "true (请先修改 Gitea Wiki,禁止直接编辑本文件)": errors.append(f"{mapping.path} 没有只读镜像标记") if metadata.get("wiki_page") != mapping.page: errors.append(f"{mapping.path} 的 wiki_page 与映射不一致") revision = metadata.get("wiki_revision", "") if re.fullmatch(r"[0-9a-f]{40,64}", revision) is None: errors.append(f"{mapping.path} 的 wiki_revision 无效") if not metadata.get("synchronized_at"): errors.append(f"{mapping.path} 缺少 synchronized_at") # ---------------------------------------------------------------- 任务归档 def safe_title(title: str) -> str: """把标题转换为适合 Wiki 页面名和 Windows 文件名的短文本。""" cleaned = re.sub(r'[<>:"/\\|?*]', "-", title.strip()) cleaned = re.sub(r"\s+", "-", cleaned) cleaned = re.sub(r"-+", "-", cleaned) return cleaned.strip(".-") def build_archive( template: str, issue_number: str, title: str, page_name: str, issue_url: str, ) -> str: content = template.replace("<工单号>", issue_number, 1) content = content.replace("<标题>", title.strip(), 1) content = content.replace("YYYY-MM-DD", date.today().isoformat(), 1) content = content.replace("<链接>", issue_url, 1) return content.replace("<页面名>", page_name, 1) # ---------------------------------------------------------------- 归档导出 TASK_PAGE_PATTERN = re.compile(r"^Task-(?P\d+)-(?P.+)$") def task_revision(metadata: dict[str, Any], page_name: str) -> str: last_commit = metadata.get("last_commit") revision = last_commit.get("sha") if isinstance(last_commit, dict) else None if not isinstance(revision, str) or not revision: raise WikiDocsError(f"Wiki 页面缺少 revision:{page_name}") return revision def existing_task_mirrors(root: Path = ROOT) -> dict[str, Path]: """按镜像头匹配已有文件,兼容历史自定义文件名。""" mirrors: dict[str, Path] = {} task_dir = root / "docs" / "task" if not task_dir.is_dir(): return mirrors for path in task_dir.glob("*.md"): try: metadata, _ = parse_mirror(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, WikiDocsError) as exc: raise WikiDocsError(f"已有任务镜像无效 {path.name}:{exc}") from exc page_name = metadata.get("wiki_page", "") if not TASK_PAGE_PATTERN.fullmatch(page_name): raise WikiDocsError(f"已有任务镜像页面名无效 {path.name}:{page_name}") if page_name in mirrors: raise WikiDocsError(f"任务页面存在重复本地镜像:{page_name}") mirrors[page_name] = path return mirrors def task_target(page_name: str, root: Path = ROOT) -> Path: match = TASK_PAGE_PATTERN.fullmatch(page_name) if match is None: raise WikiDocsError(f"不是任务归档页面:{page_name}") title = safe_title(match.group("title")) if not title: raise WikiDocsError(f"任务归档标题无效:{page_name}") return root / "docs" / "task" / f"{match.group('number')}-{title}.md" def export_task_archives( client: WikiClient, *, export_all: bool = False, root: Path = ROOT ) -> list[str]: """增量或全量读取任务归档;绝不删除本地文件。""" dirty = dirty_paths(["docs/task"], root) if dirty: raise WikiDocsError( "本地任务镜像存在未提交改动,已停止以防覆盖:\n" + "\n".join(dirty) ) existing = existing_task_mirrors(root) pages = [] for metadata in client.list_pages(): title = metadata.get("title") if isinstance(title, str) and TASK_PAGE_PATTERN.fullmatch(title): pages.append((int(title.split("-", 2)[1]), title, metadata)) pages.sort(key=lambda item: (item[0], item[1])) messages: list[str] = [] targets: set[Path] = set() for _, page_name, metadata in pages: target = existing.get(page_name, task_target(page_name, root)) if target in targets: raise WikiDocsError(f"多个任务页面映射到同一本地路径:{target.name}") targets.add(target) revision = task_revision(metadata, page_name) if not export_all and target.is_file(): local_metadata, _ = parse_mirror(target.read_text(encoding="utf-8")) if ( local_metadata.get("wiki_page") == page_name and local_metadata.get("wiki_revision") == revision ): messages.append(f"跳过:{target.relative_to(root)} <- {page_name}@{revision[:12]}") continue page = client.get_page_from_metadata(metadata, page_name) changed = write_mirror(target, page) action = "已导出" if changed else "无变化" messages.append(f"{action}:{target.relative_to(root)} <- {page_name}@{revision[:12]}") return messages # ---------------------------------------------------------------- 子命令入口 def run_check(args: argparse.Namespace) -> int: errors: list[str] = [] warnings: list[str] = [] check_required_files(errors) check_project_profile(errors, warnings, args.strict) check_wiki_mirrors(errors) check_core_documents(errors) check_task_template(errors) check_agent_efficiency_rules(errors) check_repository_readme(errors) check_claude_code_entry(errors) check_archives(errors) for warning in warnings: print(f"警告:{warning}") for error in errors: print(f"错误:{error}") if errors: print(f"检查失败:{len(errors)} 个问题") return 1 print("DevHarness 检查通过") return 0 def run_sync(args: argparse.Namespace) -> int: """同步核心镜像;--verify 复用本轮完整读取完成初始化验证。""" if args.verify: try: config = load_config(Path(args.config).resolve()) messages = sync_all( config, WikiClient(config), check=False, deep_check=True ) except WikiDocsError as exc: print(f"错误:{exc}") return 1 for message in messages: print(message) code = run_check(argparse.Namespace(strict=True)) if code != 0: print("错误:结构检查未通过,已停止") return code print("Wiki 镜像初始化验证通过") return 0 try: config = load_config(Path(args.config).resolve()) deep_check = getattr(args, "deep_check", False) messages = sync_all( config, WikiClient(config), check=args.check or deep_check, deep_check=deep_check, ) except WikiDocsError as exc: print(f"错误:{exc}") return 1 for message in messages: print(message) print( "Wiki 镜像深度检查通过" if deep_check else "Wiki 镜像检查通过" if args.check else "Wiki 镜像同步完成" ) return 0 def run_archive(args: argparse.Namespace) -> int: short_title = safe_title(args.title) if not args.issue_number.isdigit(): print("错误:工单号必须是数字") return 1 if not short_title: print("错误:标题不能为空") return 1 try: config = load_config(Path(args.config).resolve()) page_name = f"Task-{args.issue_number}-{short_title}" client = WikiClient(config) if any(item.get("title") == page_name for item in client.list_pages()): raise WikiDocsError(f"任务归档已经存在:{page_name}") template = client.get_page("Task-Archive-Template").text issue_url = ( f"{config.gitea_url}/{config.owner}/{config.repository}/issues/" f"{args.issue_number}" ) content = build_archive( template, args.issue_number, args.title, page_name, issue_url ) page = client.create_page( page_name, content, f"docs: 创建任务 #{args.issue_number} 归档草稿", ) except WikiDocsError as exc: print(f"错误:{exc}") return 1 print(f"已创建 Wiki:{page.html_url}") print("未导出本地任务归档;需要时运行 harness.py export") return 0 def run_export(args: argparse.Namespace) -> int: try: config = load_config(Path(args.config).resolve()) messages = export_task_archives(WikiClient(config), export_all=args.all) except WikiDocsError as exc: print(f"错误:{exc}") return 1 for message in messages: print(message) print("任务归档全量导出完成" if args.all else "任务归档增量导出完成") return 0 def main() -> int: parser = argparse.ArgumentParser(description="DevHarness 检查、同步与归档工具") sub = parser.add_subparsers(dest="command", required=True) p_check = sub.add_parser("check", help="检查 DevHarness 项目结构") p_check.add_argument( "--strict", action="store_true", help="项目档案有占位内容时返回失败" ) p_check.set_defaults(func=run_check) p_sync = sub.add_parser("sync", help="从 Gitea Wiki 单向同步核心 docs 镜像") sync_mode = p_sync.add_mutually_exclusive_group() sync_mode.add_argument( "--check", action="store_true", help="按 revision 快速检查 Wiki 与镜像,不写文件", ) sync_mode.add_argument( "--deep-check", action="store_true", help="下载全部 Wiki 正文并逐页检查镜像,不写文件", ) sync_mode.add_argument( "--verify", action="store_true", help="完整读取并导出 Wiki,再执行 check --strict 初始化验证", ) p_sync.add_argument( "--config", default=str(DEFAULT_CONFIG), help="Wiki 页面映射 JSON 文件" ) p_sync.set_defaults(func=run_sync) p_archive = sub.add_parser("archive", help="显式在 Gitea Wiki 创建可选任务快照") p_archive.add_argument("issue_number", help="Gitea 工单号,例如 123") p_archive.add_argument("title", help="简短任务标题") p_archive.add_argument( "--config", default=str(DEFAULT_CONFIG), help="Wiki 映射配置" ) p_archive.set_defaults(func=run_archive) p_export = sub.add_parser("export", help="人工按需导出 Gitea Wiki 任务归档") p_export.add_argument( "--all", action="store_true", help="全量读取全部线上任务归档;默认按 revision 增量", ) p_export.add_argument( "--config", default=str(DEFAULT_CONFIG), help="核心 Wiki 映射配置" ) p_export.set_defaults(func=run_export) args = parser.parse_args() return args.func(args) if __name__ == "__main__": raise SystemExit(main())