Files

287 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 <file|-> | import-achievements --achievements-json <file|->")
print(" | import-expenses --expenses-json <file|-> # 兼容别名: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))