"""CLI:argparse 与分发(结题报告初稿助手)。 只负责参数解析并转交 service;业务内核在 draft_service / ledger_service / docx_service, 勿在此按入口复制逻辑。 """ from __future__ import annotations import argparse import sys from typing import List, Optional from service.task_service import ( cmd_add_achievement, cmd_add_expense, cmd_add_project, cmd_config_path, cmd_draft_batch_pending, cmd_draft_report, cmd_health, cmd_import, cmd_import_achievements, cmd_import_expenses, cmd_import_projects, cmd_init_db, cmd_list, cmd_list_achievements, cmd_list_drafts, cmd_list_expenses, cmd_list_projects, cmd_render_html, cmd_run, cmd_show_draft, cmd_stats, cmd_version, ) from util.config_bootstrap import bootstrap_skill_config from util.argparse_zh import ZhArgumentParser from util.constants import LOG_LOGGER_NAME, SKILL_SLUG from util.logging_config import get_skill_logger, setup_skill_logging def _cli_str_or_none(raw: Optional[str]) -> Optional[str]: if raw is None: return None v = str(raw).strip() return v or None def _cli_float_or_none(raw: Optional[str]) -> Optional[float]: raw = _cli_str_or_none(raw) if raw is None: return None try: return float(raw) except ValueError: return None def _cli_int_or_none(raw: Optional[str]) -> Optional[int]: raw = _cli_str_or_none(raw) if raw is None: return None try: return int(float(raw)) except ValueError: return None def _handle_run(args: argparse.Namespace) -> int: tail = [str(x).strip() for x in (args.run_tail or []) if str(x).strip()] if len(tail) > 2: print("参数过多。") print("用法:python main.py run [target [input_id]] | run [-t target] [-i input_id]") return 1 t_target: Optional[str] = None t_input: Optional[str] = None if len(tail) == 2: t_target, t_input = tail[0], tail[1] elif len(tail) == 1: if tail[0].isdigit(): t_input = tail[0] else: t_target = tail[0] target = _cli_str_or_none(getattr(args, "target", None)) or t_target input_id = _cli_str_or_none(getattr(args, "input_id", None)) or t_input return cmd_run(target=target, input_id=input_id) def _handle_add_project(args: argparse.Namespace) -> int: return cmd_add_project( project_no=str(args.project_no or "").strip(), name=_cli_str_or_none(args.name) or "", pi=_cli_str_or_none(args.pi) or "", department=_cli_str_or_none(args.department) or "", program=_cli_str_or_none(args.program) or "", start=_cli_str_or_none(args.start) or "", end=_cli_str_or_none(args.end) or "", approved_budget=_cli_str_or_none(args.approved_budget) or "", expected_json=_cli_str_or_none(args.expected_json) or "", excerpt=_cli_str_or_none(args.excerpt) or "", status=_cli_str_or_none(args.status) or "", ) def _handle_import(args: argparse.Namespace) -> int: return cmd_import( projects_json=_cli_str_or_none(args.projects_json) or "", achievements_json=_cli_str_or_none(args.achievements_json) or "", expenses_json=_cli_str_or_none(args.expenses_json) or "", ) def _handle_list(args: argparse.Namespace) -> int: return cmd_list( kind=_cli_str_or_none(args.kind) or "projects", project_no=_cli_str_or_none(args.project_no) or "", status=_cli_str_or_none(args.status) or "", ) def _handle_render_html(args: argparse.Namespace) -> int: # 宿主行上下文可能给空串或未解析占位符(如 {{id}}):解析失败一律回退 # 缺省行为(重建最近 10 份),不让 args 变体把渲染打崩。 report_id = _cli_int_or_none(getattr(args, "report_id", None)) return cmd_render_html(report_id=report_id) def _print_full_usage() -> None: print("结题报告初稿助手(main.py)可用命令:") print(" python main.py add-project --project-no X --name Y [--pi 张三] [--program 国自然青年] [--start D] [--end D]") print(" [--approved-budget 500000] [--expected-json '[{\"type\":\"论文\",\"target\":3,\"unit\":\"篇\"}]'] [--excerpt 摘句] [--status 待结题]") print(" python main.py add-achievement --project-no X --type 论文 --title T [--venue 期刊] [--date D] [--representative 1]") print(" python main.py add-expense --project-no X --category 差旅费 --amount 23000 [--date D] [--description 摘要]") print(" python main.py import-projects --projects-json | import-achievements --achievements-json ") print(" | import-expenses --expenses-json # 兼容别名:import --projects-json ...") print(" python main.py list-projects [--status 待结题] | list-achievements --project-no X | list-expenses --project-no X") print(" # 兼容别名:list [projects|achievements|expenses]") print(" python main.py draft-report --project-no X # 主命令:生成 Word 初稿 + HTML 预览") print(" python main.py draft-batch-pending # 批量生成全部待结题初稿") print(" python main.py list-drafts [--limit N] | show-draft --id N | render-html [--report-id N]") print(" python main.py stats") print(" python main.py run [target [input_id]] [-t target] [-i input_id]") print(" python main.py health | config-path | version | init-db") def build_parser() -> ZhArgumentParser: p = ZhArgumentParser( prog="main.py", description="结题报告初稿助手:对比预期指标与实际成果生成完成情况章节,决算明细转规范描述,组装可修改的 Word 版《结题报告(初稿)》。", formatter_class=argparse.RawDescriptionHelpFormatter, ) sub = p.add_subparsers(dest="cmd", required=True, parser_class=ZhArgumentParser) sp = sub.add_parser("add-project", help="录入/更新结题项目(按项目编号幂等)") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.add_argument("--name", default=None, metavar="项目名称") sp.add_argument("--pi", default=None, metavar="负责人") sp.add_argument("--department", default=None, metavar="所属学院") sp.add_argument("--program", default=None, metavar="项目类型") sp.add_argument("--start", default=None, metavar="执行期开始") sp.add_argument("--end", default=None, metavar="执行期结束") sp.add_argument("--approved-budget", default=None, metavar="立项经费(元)", dest="approved_budget") sp.add_argument("--expected-json", default=None, metavar="预期指标JSON", dest="expected_json") sp.add_argument("--excerpt", default=None, metavar="年度报告摘句") sp.add_argument("--status", default=None, metavar="状态(待结题/初稿已生成/已提交)") sp.set_defaults(handler=_handle_add_project) sp = sub.add_parser("add-achievement", help="录入成果") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.add_argument("--type", required=True, metavar="类型(论文/专利/人才培养/其他)", dest="ach_type") sp.add_argument("--title", required=True, metavar="成果名称") sp.add_argument("--venue", default=None, metavar="期刊/专利号") sp.add_argument("--date", default=None, metavar="日期") sp.add_argument("--representative", default=None, metavar="代表性(1/0)") sp.add_argument("--note", default=None, metavar="备注") sp.set_defaults( handler=lambda a: cmd_add_achievement( project_no=str(a.project_no), ach_type=str(a.ach_type), title=str(a.title), venue=_cli_str_or_none(a.venue) or "", date=_cli_str_or_none(a.date) or "", representative=_cli_str_or_none(a.representative) or "", note=_cli_str_or_none(a.note) or "", ) ) sp = sub.add_parser("add-expense", help="录入决算明细") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.add_argument("--category", required=True, metavar="科目") sp.add_argument("--amount", required=True, type=float, metavar="金额(元)") sp.add_argument("--date", default=None, metavar="发生日期") sp.add_argument("--description", default=None, metavar="原始摘要") sp.set_defaults( handler=lambda a: cmd_add_expense( project_no=str(a.project_no), category=str(a.category), amount=float(a.amount), date=_cli_str_or_none(a.date) or "", description=_cli_str_or_none(a.description) or "", ) ) sp = sub.add_parser("import", help="批量导入(兼容别名:projects/achievements/expenses JSON)") sp.add_argument("--projects-json", default=None, metavar="项目JSON(file|-)", dest="projects_json") sp.add_argument("--achievements-json", default=None, metavar="成果JSON(file|-)", dest="achievements_json") sp.add_argument("--expenses-json", default=None, metavar="决算JSON(file|-)", dest="expenses_json") sp.set_defaults(handler=_handle_import) sp = sub.add_parser("import-projects", help="批量导入结题项目") sp.add_argument("--projects-json", required=True, metavar="JSON(file|-)", dest="projects_json") sp.set_defaults(handler=lambda a: cmd_import_projects(str(a.projects_json))) sp = sub.add_parser("import-achievements", help="批量导入成果清单") sp.add_argument("--achievements-json", required=True, metavar="JSON(file|-)", dest="achievements_json") sp.set_defaults(handler=lambda a: cmd_import_achievements(str(a.achievements_json))) sp = sub.add_parser("import-expenses", help="批量导入决算明细") sp.add_argument("--expenses-json", required=True, metavar="JSON(file|-)", dest="expenses_json") sp.set_defaults(handler=lambda a: cmd_import_expenses(str(a.expenses_json))) sp = sub.add_parser("list", help="查询台账(兼容别名:projects/achievements/expenses)") sp.add_argument("--kind", default="projects", metavar="projects|achievements|expenses", dest="kind") sp.add_argument("--project-no", default=None, metavar="项目编号", dest="project_no") sp.add_argument("--status", default=None, metavar="状态筛选") sp.set_defaults(handler=_handle_list) sp = sub.add_parser("list-projects", help="查询结题项目") sp.add_argument("--status", default=None, metavar="状态筛选") sp.set_defaults(handler=lambda a: cmd_list_projects(status=_cli_str_or_none(a.status) or "")) sp = sub.add_parser("list-achievements", help="查询成果清单") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.set_defaults(handler=lambda a: cmd_list_achievements(project_no=str(a.project_no))) sp = sub.add_parser("list-expenses", help="查询决算明细") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.set_defaults(handler=lambda a: cmd_list_expenses(project_no=str(a.project_no))) sp = sub.add_parser("draft-report", help="生成结题报告初稿(主命令)") sp.add_argument("--project-no", required=True, metavar="项目编号", dest="project_no") sp.set_defaults(handler=lambda a: cmd_draft_report(project_no=str(a.project_no))) sp = sub.add_parser("draft-batch-pending", help="批量生成全部待结题项目初稿") sp.set_defaults(handler=lambda _a: cmd_draft_batch_pending()) sp = sub.add_parser("list-drafts", help="查询初稿记录") sp.add_argument("--limit", default=20, metavar="条数") sp.set_defaults(handler=lambda a: cmd_list_drafts(limit=int(a.limit or 20))) sp = sub.add_parser("show-draft", help="查看初稿记录详情") sp.add_argument("--id", required=True, type=int, metavar="记录编号", dest="draft_id") sp.set_defaults(handler=lambda a: cmd_show_draft(draft_id=int(a.draft_id))) sp = sub.add_parser("render-html", help="重建 HTML 预览(缺省最近 10 份)") sp.add_argument("--report-id", default=None, metavar="记录编号", dest="report_id") sp.set_defaults(handler=_handle_render_html) sp = sub.add_parser("stats", help="统计") sp.set_defaults(handler=lambda _a: cmd_stats()) sp = sub.add_parser("run", help="任务执行命令骨架") sp.add_argument("--target", "-t", default=None, metavar="目标") sp.add_argument("--input-id", "-i", default=None, metavar="输入id", dest="input_id") sp.add_argument("run_tail", nargs="*", metavar="位置参数") sp.set_defaults(handler=_handle_run) sp = sub.add_parser("health", help="健康检查") sp.set_defaults(handler=lambda _a: cmd_health()) sp = sub.add_parser("config-path", help="输出用户 .env 与模板路径") sp.set_defaults(handler=lambda _a: cmd_config_path()) sp = sub.add_parser("version", help="版本信息(JSON)") sp.set_defaults(handler=lambda _a: cmd_version()) sp = sub.add_parser("init-db", help="幂等初始化本地数据库与数据管理元数据") sp.set_defaults(handler=lambda _a: cmd_init_db()) return p def main(argv: Optional[List[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] bootstrap_skill_config() setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME) get_skill_logger().info("cli_start argv=%s", sys.argv) if not argv: _print_full_usage() return 1 parser = build_parser() args = parser.parse_args(argv) return int(args.handler(args))