"""任务编排:鉴权、进度 emit、请假考勤领域调度。""" from __future__ import annotations import json import os from typing import Any, Optional from jiangchang_skill_core import collect_runtime_diagnostics, config, format_runtime_health_lines from jiangchang_skill_core.activity import JobStopped, emit, finish, job_context from db.connection import init_db from service.employee_import_service import IMPORT_SUBDIR, ensure_builtin_samples from service.entitlement_service import check_entitlement from service.leave_service import calculate_leave, render_leave_report from util.constants import LOG_LOGGER_NAME, PLATFORM_KIT_MIN_VERSION, SKILL_SLUG, SKILL_VERSION from util.logging_config import get_skill_logger, setup_skill_logging from util.runtime_paths import get_db_path, get_skill_data_dir, get_skill_root def _get_task_logger(): try: return get_skill_logger() except RuntimeError: setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME) return get_skill_logger() def _print_json(payload: dict[str, Any]) -> None: print(json.dumps(payload, ensure_ascii=False, indent=2)) _FINISH_RESERVED = frozenset( {"status", "message", "skill", "error_code", "stage", "ok", "type", "schema_version"} ) def _finish_success(message: str, payload: Optional[dict[str, Any]] = None, **extra: Any) -> None: body: dict[str, Any] = {} if payload: body.update({k: v for k, v in payload.items() if k not in _FINISH_RESERVED}) body.update({k: v for k, v in extra.items() if k not in _FINISH_RESERVED}) finish(status="success", message=message, skill=SKILL_SLUG, **body) def _parse_int(raw: Optional[str], name: str) -> int: if raw is None or not str(raw).strip(): raise ValueError(f"缺少参数: {name}") text = str(raw).strip() if "{{" in text or "}}" in text: raise ValueError(f"参数 {name} 未正确代入(收到 {text!r})。请选中行后再操作。") try: return int(text) except ValueError as exc: raise ValueError(f"参数 {name} 必须是整数,当前为 {text!r}") from exc def _parse_optional_int(raw: Optional[str], name: str) -> Optional[int]: if raw is None or not str(raw).strip(): return None text = str(raw).strip() if "{{" in text or "}}" in text: return None return _parse_int(raw, name) def _check_entitlement_or_fail(log, task_type: str) -> bool: ok, reason = check_entitlement(SKILL_SLUG) if ok: return True log.warning( "entitlement_failed task_type=%s stage=auth error_code=ENTITLEMENT_DENIED", task_type, ) emit("鉴权失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="auth") finish( status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth", ) print(f"ERROR:ENTITLEMENT_DENIED {reason}") return False def _parse_bool(raw: Optional[str]) -> bool: if raw is None: return False text = str(raw).strip().lower() if "{{" in text or "}}" in text: return False return text in ("1", "true", "yes", "是", "y") def cmd_calculate_leave( employee_id: Optional[str] = None, leave_type: Optional[str] = None, start_date: Optional[str] = None, end_date: Optional[str] = None, has_certificate: Optional[str] = None, certificate_type: Optional[str] = None, ) -> int: log = _get_task_logger() task_type = "calculate-leave" with job_context(skill=SKILL_SLUG): log.info( "task_start task_type=%s employee_id=%s leave_type=%s", task_type, employee_id, leave_type, ) emit("开始核算请假", skill=SKILL_SLUG, stage="calculate") try: if not _check_entitlement_or_fail(log, task_type): return 1 eid = _parse_int(employee_id, "employee_id") lt = (leave_type or "").strip() if not lt or "{{" in lt: raise ValueError("缺少参数: leave_type") sd = (start_date or "").strip() if not sd or "{{" in sd: raise ValueError("缺少参数: start_date") ed = (end_date or "").strip() if not ed or "{{" in ed: raise ValueError("缺少参数: end_date") ct = (certificate_type or "").strip() if "{{" in ct or "}}" in ct: ct = None payload = calculate_leave( eid, leave_type=lt, start_date=sd, end_date=ed, has_certificate=_parse_bool(has_certificate), certificate_type=ct or None, ) _finish_success(str(payload.get("message") or "核算完成"), payload) print("OK") _print_json(payload) return 0 except ValueError as exc: log.warning("task_failed task_type=%s msg=%s", task_type, exc) print(f"ERROR:{exc}") finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="VALIDATION") return 1 except JobStopped: raise except Exception: log.exception("task_failed task_type=%s", task_type) finish(status="failed", message="请假核算失败", skill=SKILL_SLUG, stage="calculate") return 1 def cmd_import_employees(file_path: Optional[str] = None) -> int: log = _get_task_logger() task_type = "import-employees" with job_context(skill=SKILL_SLUG): log.info("task_start task_type=%s file=%s", task_type, file_path) emit("导入教职工", skill=SKILL_SLUG, stage="import") try: if not _check_entitlement_or_fail(log, task_type): return 1 from service.employee_import_service import import_employees_json path = (file_path or "").strip() or "示例教职工.json" payload = import_employees_json(path) _finish_success(str(payload.get("message") or "教职工已导入"), payload) print("OK") _print_json(payload) return 0 except ValueError as exc: log.warning("task_failed task_type=%s msg=%s", task_type, exc) print(f"ERROR:{exc}") finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="VALIDATION") return 1 except Exception: log.exception("task_failed task_type=%s", task_type) finish(status="failed", message="导入教职工失败", skill=SKILL_SLUG, stage="import") return 1 def cmd_render_html(request_id: Optional[str] = None) -> int: log = _get_task_logger() emit("生成请假报告", skill=SKILL_SLUG, stage="report") try: rid = _parse_optional_int(request_id, "request_id") payload = render_leave_report(rid) log.info("render_html_ok path=%s", payload.get("html_path")) print("OK") _print_json(payload) return 0 except ValueError as exc: log.warning("render_html_failed msg=%s", exc) print(f"ERROR:{exc}") return 1 def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int: """兼容入口:需 employee_id(来自 target 或 input_id)。""" log = _get_task_logger() employee_raw = target or input_id if not employee_raw or not str(employee_raw).strip(): msg = "缺少参数: employee_id(run 需指定教职工编号,或使用 calculate-leave)" log.warning("task_failed task_type=run msg=%s", msg) print(f"ERROR:{msg}") return 1 print("ERROR:run 入口仅兼容旧调用,请使用 calculate-leave 子命令并提供完整参数。") return 1 def cmd_health() -> int: log = _get_task_logger() try: init_db() ensure_builtin_samples(overwrite=False) except Exception as exc: # noqa: BLE001 log.warning("health_init_db_failed msg=%s", exc) print(f"ERROR:init_db {exc}") return 1 runtime = collect_runtime_diagnostics( skill_slug=SKILL_SLUG, platform_kit_min_version=PLATFORM_KIT_MIN_VERSION, skill_root=get_skill_root(), ) example_path = os.path.join(get_skill_root(), ".env.example") env_path = config.get_env_file_path() or "" env_exists = bool(env_path and os.path.isfile(env_path)) health_status = "failed" if runtime.has_fatal_issues else "ok" lines = [ f"{SKILL_SLUG} health: {health_status}", *format_runtime_health_lines(runtime), f"env_path: {env_path}", f"env_exists: {env_exists}", f"example_path: {os.path.abspath(example_path)}", ] for line in lines: print(line) log.info("health_ok status=%s", health_status) return 1 if runtime.has_fatal_issues else 0 def cmd_version() -> int: print(json.dumps({"version": SKILL_VERSION, "skill": SKILL_SLUG}, ensure_ascii=False)) return 0 def cmd_config_path() -> int: example_path = os.path.join(get_skill_root(), ".env.example") env_path = config.get_env_file_path() or "" data = get_skill_data_dir() print( json.dumps( { "skill": SKILL_SLUG, "env_path": env_path, "example_path": os.path.abspath(example_path), "data_dir": data, "employees_dir": f"input/{IMPORT_SUBDIR}", }, ensure_ascii=False, ) ) return 0 def cmd_init_db() -> int: try: init_db() ensure_builtin_samples(overwrite=False) except Exception as exc: # noqa: BLE001 print(json.dumps({"ok": False, "skill": SKILL_SLUG, "error": str(exc)}, ensure_ascii=False)) return 1 print( json.dumps( { "ok": True, "skill": SKILL_SLUG, "db_path": get_db_path(), "employees_dir": f"input/{IMPORT_SUBDIR}", }, ensure_ascii=False, ) ) return 0