"""CLI 命令分发:argparse 子命令 → 调用 service 层。 9 个子命令(与 assets/actions.json 对齐): - health - version - config-path - init-db - scan-expiring-contracts - generate-renewal-task - dispatch-task-notice - mark-contract-renewed - render-html """ from __future__ import annotations import argparse import json import sys from typing import Callable, List, Optional from db.connection import get_connection, init_db from service import dispatcher, html_renderer, scanner, task_generator from util.constants import DEFAULT_THRESHOLDS, SKILL_VERSION from util.logging_setup import get_skill_logger, setup_skill_logging from util.runtime_paths import ( get_db_path, get_dispatch_dir, get_env_path, get_output_dir, ) logger = get_skill_logger() # ---------- 命令实现 ---------- def cmd_health(_args: argparse.Namespace) -> int: """健康检查。""" setup_skill_logging() db_path = get_db_path() db_exists = db_path.exists() db_writable = False error_msg = None try: conn = get_connection() try: conn.execute("SELECT 1").fetchone() db_writable = True finally: conn.close() except Exception as e: error_msg = str(e) output_dir = get_output_dir() dispatch_dir = get_dispatch_dir() checks = [ ("python_version", f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", True), ("db_path", str(db_path), True), ("db_exists", str(db_exists), db_exists), ("db_writable", str(db_writable), db_writable), ("output_dir", str(output_dir), True), ("dispatch_dir", str(dispatch_dir), True), ] if db_exists and db_writable: try: conn = get_connection() try: counts = { "contracts": conn.execute("SELECT COUNT(*) AS n FROM contracts").fetchone()["n"], "performance_records": conn.execute("SELECT COUNT(*) AS n FROM performance_records").fetchone()["n"], "alerts": conn.execute("SELECT COUNT(*) AS n FROM alerts").fetchone()["n"], "renewal_tasks": conn.execute("SELECT COUNT(*) AS n FROM renewal_tasks").fetchone()["n"], "dispatch_log": conn.execute("SELECT COUNT(*) AS n FROM dispatch_log").fetchone()["n"], } finally: conn.close() checks.append(("seed_loaded", f"contracts={counts['contracts']} perf={counts['performance_records']}", counts["contracts"] > 0)) except Exception as e: checks.append(("seed_check", f"ERROR: {e}", False)) all_ok = all(c[2] for c in checks) for name, val, ok in checks: marker = "OK " if ok else "FAIL" print(f"[{marker}] {name}: {val}") if error_msg: print(f"[FAIL] error: {error_msg}") if all_ok: print("OK: health check passed") return 0 print("ERROR: health check failed") return 1 def cmd_version(_args: argparse.Namespace) -> int: print(f"{SKILL_VERSION}") return 0 def cmd_config_path(_args: argparse.Namespace) -> int: print(get_env_path()) return 0 def cmd_init_db(_args: argparse.Namespace) -> int: counts = init_db() print(f"OK: init-db complete") for k, v in counts.items(): print(f" {k}: {v} rows") return 0 def cmd_scan(args: argparse.Namespace) -> int: thresholds = _parse_thresholds(args.thresholds) conn = get_connection() try: result = scanner.scan(conn, thresholds=thresholds) finally: conn.close() print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 def cmd_generate_task(args: argparse.Namespace) -> int: contract_id = args.contract_id task_id = args.task_id conn = get_connection() try: result = task_generator.regenerate_task_for_contract_id(conn, contract_id, task_id) conn.commit() except ValueError as e: print(f"ERROR: {e}") return 2 finally: conn.close() print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 def cmd_dispatch(args: argparse.Namespace) -> int: conn = get_connection() try: result = dispatcher.dispatch(conn, args.unit_id, args.month or "") finally: conn.close() print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 def cmd_mark_renewed(args: argparse.Namespace) -> int: conn = get_connection() try: from db import repository as repo contract = repo.get_contract(conn, args.contract_id) if contract is None: print(f"ERROR: contract_id={args.contract_id!r} 不存在") return 2 with conn: n = repo.update_contract_status(conn, args.contract_id, "已续签", args.material_id) closed = repo.close_open_alerts_for_contract(conn, args.contract_id) print(json.dumps({ "contract_id": args.contract_id, "status": "已续签", "material_id": args.material_id, "updated_rows": n, "closed_alerts": closed, }, ensure_ascii=False, indent=2)) finally: conn.close() return 0 def cmd_render_html(args: argparse.Namespace) -> int: conn = get_connection() try: output_path = html_renderer.render_task_html(conn, args.task_id) except ValueError as e: print(f"ERROR: {e}") return 2 except FileNotFoundError as e: print(f"ERROR: {e}") return 3 finally: conn.close() print(json.dumps({ "task_id": args.task_id, "output_path": str(output_path), "size_bytes": output_path.stat().st_size, }, ensure_ascii=False, indent=2)) return 0 # ---------- argparse ---------- def _parse_thresholds(s: str) -> tuple[int, int, int]: parts = [int(p.strip()) for p in s.split(",")] if len(parts) != 3 or sorted(parts, reverse=True) != parts: raise argparse.ArgumentTypeError( f"thresholds 必须是 3 个整数(黄,橙,红)且从大到小,例如 90,60,30;收到 {s!r}" ) return (parts[0], parts[1], parts[2]) def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( prog="main.py", description=f"合同到期智能预警与续聘辅助 v{SKILL_VERSION}", ) sub = p.add_subparsers(dest="cmd", required=True) sub.add_parser("health", help="健康检查(Python / DB / 目录)").set_defaults(handler=cmd_health) sub.add_parser("version", help="查看版本").set_defaults(handler=cmd_version) sub.add_parser("config-path", help="查看 .env 路径").set_defaults(handler=cmd_config_path) sub.add_parser("init-db", help="建表 + 加载样例数据").set_defaults(handler=cmd_init_db) sp = sub.add_parser( "scan-expiring-contracts", help="扫描即将到期合同(async/cron)", ) sp.add_argument( "--thresholds", default="90,60,30", help="三级预警阈值(黄,橙,红)从大到小,默认 90,60,30", ) sp.set_defaults(handler=cmd_scan) sp = sub.add_parser( "generate-renewal-task", help="为指定合同生成续聘任务书", ) sp.add_argument("--contract-id", required=True, help="合同 ID") sp.add_argument("--task-id", default=None, help="可选:指定 task_id(默认自动生成)") sp.set_defaults(handler=cmd_generate_task) sp = sub.add_parser( "dispatch-task-notice", help="按单位/月份派发续聘通知(dry-run)", ) sp.add_argument("--unit-id", required=True, help="二级单位 ID") sp.add_argument("--month", default="", help="月份 YYYY-MM(留空 = 全部)") sp.set_defaults(handler=cmd_dispatch) sp = sub.add_parser( "mark-contract-renewed", help="标记合同已续签", ) sp.add_argument("--contract-id", required=True, help="合同 ID") sp.add_argument("--material-id", required=True, help="续签材料 ID") sp.set_defaults(handler=cmd_mark_renewed) sp = sub.add_parser( "render-html", help="渲染任务书 HTML", ) sp.add_argument("--task-id", required=True, help="任务 ID") sp.set_defaults(handler=cmd_render_html) return p def main(argv: Optional[List[str]] = None) -> int: argv = argv if argv is not None else sys.argv[1:] if not argv: print(f"合同到期智能预警与续聘辅助 v{SKILL_VERSION}") print("Commands: health, version, config-path, init-db, " "scan-expiring-contracts, generate-renewal-task, " "dispatch-task-notice, mark-contract-renewed, render-html") return 1 setup_skill_logging() parser = build_parser() args = parser.parse_args(argv) handler: Callable[[argparse.Namespace], int] = args.handler return handler(args)