refactor: 合并 dev_scripts 入口为单一 harness.py (#23)

原有 4 个命令行入口(check_harness、sync_wiki_docs、new_task_archive、
export_task_archives)需要在多处文档分别描述,同步核心镜像还要按顺序跑 3 条命令。

合并为 dev_scripts/harness.py,提供 check / sync / archive / export 四个子命令,
各命令实现逻辑逐字取自原脚本,行为未改变;新增 sync --verify 一次完成
导出 → check --strict → 一致性校验。wiki_docs.py 作为库保持不变。

破坏性变更:4 个旧入口已删除,不保留兼容转发。已采用 DevHarness 的业务项目
升级基线时必须同步修改自己的命令与文档。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ila
2026-08-18 21:36:15 +08:00
co-authored by Claude Opus 5
parent 65ead42f36
commit bfc0726c33
8 changed files with 295 additions and 296 deletions
+724
View File
@@ -0,0 +1,724 @@
"""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": (
"## 新项目 Wiki 初始化门禁",
"## 工单与设计证据双门禁",
"### 先判断是否需要工单",
"### 再判断设计证据",
"### 本地 HTML 审核快照",
"### 记录和重新确认",
"## 面向初级维护者的修改边界",
"## 每个任务的文档影响",
"## 需求记录与流转",
"## 稳定文档与任务归档",
"## 自然语言快捷指令",
"## 效率与范围控制",
"### 严格控制范围",
"### 渐进执行和修复",
"### 复用已验证事实",
"### 明确停止条件",
),
"docs/02-architecture-and-code-map.md": (
"## 项目定位",
"## 代码地图",
"## 两条主要执行路径",
"## 不可破坏的边界",
),
"docs/03-business-rules-and-glossary.md": (
"## 核心术语",
"## 工单状态",
"## 稳定业务规则",
"## 新项目需要补充什么",
),
"docs/04-local-development-and-verification.md": (
"## 环境要求",
"## 第一次运行",
"## 常用调试方式",
"## 完成修改前",
),
"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/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、数据、状态或流程设计 / 原设计或复现证据",
"- 可编辑设计源链接、版本或事实来源:",
"- 本地 HTML 审核快照路径和版本(不适用时说明原因):",
"- 本地浏览方式和资源完整性检查:",
"- 版本、revision 或确认日期:",
"- 状态:无 / 草稿 / 已确认 / 已废弃",
"- 确认人、确认时间和覆盖范围:",
"- 无需 UI 原型或无需任何原型的原因:",
"## 文档影响",
"- [ ] 不影响长期文档,原因:",
"- [ ] 更新架构与代码地图",
"- [ ] 更新业务规则与术语",
"- [ ] 更新常见修改或故障排查",
"## 交付文档影响",
"- [ ] 无交付文档影响,原因:",
"- [ ] 更新已有交付文档,受众与页面:",
"- [ ] 新增交付文档,受众与页面:",
"- [ ] 需要目标岗位或客户代表验证:是 / 否;验证方式:",
)
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",
"### 新项目 Wiki 初始化门禁",
"`Home` 不存在时必须先创建 `Home`",
"不得把模板自带的本地 `docs/` 当作新项目 Wiki 已初始化的证据",
"提交只包含当前工单相关文件",
"### 工单与设计证据双门禁",
"新页面、独立用户功能、重大交互或导航变化",
"`prototypes/<工单号>/<版本>/index.html`",
"已确认的 HTML 快照不得原位覆盖",
"代码组件名、类名、变量、国际化键、API 字段和数据库字段不是显示文案",
"### 自然语言快捷指令",
"`只分析`",
"`建工单`",
"`执行工单 #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",
)
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<number>\d+)-(?P<title>.+)$")
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:
steps = (
("同步", lambda: run_sync(
argparse.Namespace(check=False, verify=False, config=args.config))),
("结构检查", lambda: run_check(argparse.Namespace(strict=True))),
("一致性校验", lambda: run_sync(
argparse.Namespace(check=True, verify=False, config=args.config))),
)
for name, step in steps:
code = step()
if code != 0:
print(f"错误:{name}未通过,已停止")
return code
return 0
try:
config = load_config(Path(args.config).resolve())
messages = sync_all(config, WikiClient(config), check=args.check)
except WikiDocsError as exc:
print(f"错误:{exc}")
return 1
for message in messages:
print(message)
print("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 镜像")
p_sync.add_argument(
"--check", action="store_true", help="只检查 Wiki 与镜像是否一致,不写文件"
)
p_sync.add_argument(
"--verify",
action="store_true",
help="依次执行导出、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())