feat: 结题报告初稿助手首版 v1.0.1(draft-closing-report-default)
All checks were successful
技能自动化发布 / release (push) Successful in 6s

This commit is contained in:
chenfuqiang
2026-09-13 16:04:01 +08:00
commit 7b24521b43
206 changed files with 29892 additions and 0 deletions

240
scripts/cli/app.py Normal file
View File

@@ -0,0 +1,240 @@
"""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_init_db,
cmd_list,
cmd_list_drafts,
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 _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 "",
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:
report_id = getattr(args, "report_id", None)
return cmd_render_html(report_id=int(report_id) if report_id else None)
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(" [--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-json F [--achievements-json F] [--expenses-json F]")
print(" python main.py list [projects|achievements|expenses] [--project-no X] [--status 待结题]")
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("--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("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("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))