feat: 项目经费进度播报助手首版 v1.0.1(track-fund-progress-default)
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
620
scripts/service/task_service.py
Normal file
620
scripts/service/task_service.py
Normal file
@@ -0,0 +1,620 @@
|
||||
"""任务编排层:job_context / emit / finish、鉴权与错误转换。
|
||||
|
||||
领域业务在 ``service/fund_service.py``(播报内核)、
|
||||
``service/project_service.py``(台账/规则/统计)、
|
||||
``service/report_service.py``(HTML 简报),由本模块调用;
|
||||
多入口(CLI / Action / 对话 / cron)复用同一内核,禁止按入口分叉业务。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
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 jiangchang_skill_core.rpa.video_session import RpaVideoSession
|
||||
from service.entitlement_service import check_entitlement
|
||||
from service.task_run_support import (
|
||||
_print_video_summary,
|
||||
build_video_info,
|
||||
merge_video_into_result_summary,
|
||||
)
|
||||
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_root
|
||||
from db.connection import init_db
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 模板演示入口(保留:满足 POLICY-RPA-003 录屏集成与 CLI run 契约;不承载本技能业务)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _run_template_demo(target: Optional[str], input_id: Optional[str]) -> tuple[int, dict[str, Any]]:
|
||||
"""最小 RpaVideoSession 示范:不启动浏览器、不执行业务,仅演示录屏包裹范式。"""
|
||||
batch_id = uuid.uuid4().hex[:12]
|
||||
data_dir = get_skill_root()
|
||||
|
||||
async with RpaVideoSession(
|
||||
skill_slug=SKILL_SLUG,
|
||||
skill_data_dir=data_dir,
|
||||
batch_id=batch_id,
|
||||
title="开始执行示例任务",
|
||||
closing_title="示例任务执行完成",
|
||||
) as video:
|
||||
video.add_step("准备执行示例任务")
|
||||
if target:
|
||||
video.add_step(f"接收目标参数:{target}")
|
||||
if input_id:
|
||||
video.add_step(f"接收输入标识:{input_id}")
|
||||
video.add_step("示例任务执行完成")
|
||||
|
||||
video_summary = video.summary()
|
||||
video_info = build_video_info(video_summary, {}, {})
|
||||
return 1, video_info
|
||||
|
||||
|
||||
def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int:
|
||||
"""模板演示入口:本技能主能力请使用 broadcast 等业务命令。"""
|
||||
log = _get_task_logger()
|
||||
task_type = "demo"
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info(
|
||||
"task_start task_type=%s target_id=%s input_id=%s stage=run",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
emit("开始处理任务", skill=SKILL_SLUG, stage="run")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
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 1
|
||||
|
||||
log.info("entitlement_ok task_type=%s stage=auth", task_type)
|
||||
log.info("rpa_start stage=demo_prepare task_type=%s", task_type)
|
||||
log.info("task_run_demo_start target_id=%s input_id=%s stage=demo_prepare", target, input_id)
|
||||
rc, video_info = asyncio.run(_run_template_demo(target, input_id))
|
||||
log.info("rpa_done stage=demo_prepare status=ok")
|
||||
log.info(
|
||||
"task_run_demo_done target_id=%s input_id=%s stage=demo_finish status=failed",
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
_print_video_summary(video_info)
|
||||
|
||||
summary_payload = merge_video_into_result_summary(
|
||||
{
|
||||
"template_demo": True,
|
||||
"target": target,
|
||||
"input_id": input_id,
|
||||
},
|
||||
video_info,
|
||||
)
|
||||
|
||||
log.warning(
|
||||
"task_failed task_type=%s stage=demo_finish error_code=TEMPLATE_STUB",
|
||||
task_type,
|
||||
)
|
||||
finish(
|
||||
status="failed",
|
||||
message="模板演示入口未承载本技能业务",
|
||||
skill=SKILL_SLUG,
|
||||
error_code="TEMPLATE_STUB",
|
||||
**summary_payload,
|
||||
)
|
||||
print("ERROR:TEMPLATE_STUB 这是模板演示入口,请使用 broadcast 命令执行经费进度播报。")
|
||||
return rc
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.exception(
|
||||
"task_failed task_type=%s target_id=%s input_id=%s",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
emit("任务失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="run")
|
||||
finish(
|
||||
status="failed",
|
||||
message="任务执行异常,详见统一日志",
|
||||
skill=SKILL_SLUG,
|
||||
stage="run",
|
||||
)
|
||||
return 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 基础命令
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_config_path() -> int:
|
||||
example_path = os.path.join(get_skill_root(), ".env.example")
|
||||
env_path = config.get_env_file_path() or ""
|
||||
_print_json(
|
||||
{
|
||||
"skill": SKILL_SLUG,
|
||||
"env_path": env_path,
|
||||
"example_path": os.path.abspath(example_path),
|
||||
}
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_health() -> int:
|
||||
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)
|
||||
return 1 if runtime.has_fatal_issues else 0
|
||||
|
||||
|
||||
def cmd_version() -> int:
|
||||
_print_json({"version": SKILL_VERSION, "skill": SKILL_SLUG})
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_init_db() -> int:
|
||||
"""幂等创建/迁移本地 SQLite 与数据管理元数据(宿主 install/upgrade 可调用)。"""
|
||||
log = _get_task_logger()
|
||||
try:
|
||||
seeded = init_db()
|
||||
except Exception as exc: # noqa: BLE001 — CLI 边界,统一成可机读失败
|
||||
print(
|
||||
json.dumps(
|
||||
{"ok": False, "skill": SKILL_SLUG, "error": str(exc)},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 1
|
||||
_print_json(
|
||||
{
|
||||
"ok": True,
|
||||
"skill": SKILL_SLUG,
|
||||
"db_path": get_db_path(),
|
||||
"seeded": seeded,
|
||||
}
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 项目台账
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_add_project(
|
||||
*,
|
||||
project_no: str,
|
||||
name: str = "",
|
||||
project_type: str = "",
|
||||
pi: str = "",
|
||||
department: str = "",
|
||||
total_approved: Optional[float] = None,
|
||||
total_received: Optional[float] = None,
|
||||
total_spent: Optional[float] = None,
|
||||
frozen: Optional[float] = None,
|
||||
frozen_since: str = "",
|
||||
start: str = "",
|
||||
end: str = "",
|
||||
status: str = "",
|
||||
remark: str = "",
|
||||
) -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import upsert_project
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=add_project project_no=%s stage=project", project_no)
|
||||
emit("开始写入项目台账", skill=SKILL_SLUG, stage="project")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
log.warning("entitlement_failed task_type=add_project stage=auth")
|
||||
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
|
||||
log.error("add_project_denied stage=auth")
|
||||
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||
return 1
|
||||
fields: dict[str, Any] = {"project_no": project_no}
|
||||
if name:
|
||||
fields["project_name"] = name
|
||||
if project_type:
|
||||
fields["project_type"] = project_type
|
||||
if pi:
|
||||
fields["pi_name"] = pi
|
||||
if department:
|
||||
fields["department"] = department
|
||||
for key, value in (
|
||||
("total_approved", total_approved),
|
||||
("total_received", total_received),
|
||||
("total_spent", total_spent),
|
||||
("frozen_amount", frozen),
|
||||
):
|
||||
if value is not None:
|
||||
fields[key] = value
|
||||
for key, value in (("frozen_since", frozen_since), ("start_date", start), ("end_date", end), ("status", status), ("remark", remark)):
|
||||
if value:
|
||||
fields[key] = value
|
||||
payload = upsert_project(fields)
|
||||
_print_json(payload)
|
||||
finish(status="success", message=f"项目 {project_no} 已{'更新' if payload['action'] == 'updated' else '录入'}", skill=SKILL_SLUG, stage="project")
|
||||
return 0
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=add_project error_code=PROJECT_INVALID detail=%s", exc)
|
||||
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="project")
|
||||
print(f"ERROR:PROJECT_INVALID {exc}")
|
||||
return 1
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=add_project stage=project")
|
||||
finish(status="failed", message="台账写入异常,详见统一日志", skill=SKILL_SLUG, stage="project")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_import_projects(projects_json: str) -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import import_projects, parse_projects_json
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=import_projects stage=project")
|
||||
emit("开始批量导入项目台账", skill=SKILL_SLUG, stage="project")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
log.warning("entitlement_failed task_type=import_projects stage=auth")
|
||||
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
|
||||
log.error("import_projects_denied stage=auth")
|
||||
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||
return 1
|
||||
projects = parse_projects_json(projects_json)
|
||||
payload = import_projects(projects)
|
||||
_print_json(payload)
|
||||
finish(status="success", message=f"导入完成:新增 {payload['created']},更新 {payload['updated']}", skill=SKILL_SLUG, stage="project")
|
||||
return 0
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=import_projects error_code=PROJECT_INVALID detail=%s", exc)
|
||||
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="project")
|
||||
print(f"ERROR:PROJECT_INVALID {exc}")
|
||||
return 1
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=import_projects stage=project")
|
||||
finish(status="failed", message="台账导入异常,详见统一日志", skill=SKILL_SLUG, stage="project")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_list_projects(pi: str = "", status: str = "") -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import list_projects
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=list_projects pi=%s status=%s", pi, status)
|
||||
try:
|
||||
projects = list_projects(pi_name=pi, status=status)
|
||||
_print_json({"ok": True, "count": len(projects), "projects": projects})
|
||||
return 0
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=list_projects")
|
||||
print("ERROR:QUERY_FAILED 台账查询异常,详见统一日志")
|
||||
return 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 播报主命令
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_broadcast(
|
||||
*,
|
||||
project_no: str = "",
|
||||
pi: str = "",
|
||||
as_of: str = "",
|
||||
include_all_status: bool = False,
|
||||
) -> int:
|
||||
"""经费进度播报(同步):全部在研项目或指定项目。"""
|
||||
log = _get_task_logger()
|
||||
from datetime import date
|
||||
|
||||
from service.fund_service import query_projects, run_broadcast
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=broadcast project_no=%s pi=%s stage=broadcast", project_no, pi)
|
||||
emit("开始经费进度播报", skill=SKILL_SLUG, stage="broadcast")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
log.warning("entitlement_failed task_type=broadcast stage=auth")
|
||||
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
|
||||
log.error("broadcast_denied stage=auth")
|
||||
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||
return 1
|
||||
|
||||
as_of_date = date.fromisoformat(as_of) if as_of else date.today()
|
||||
projects = query_projects(project_no=project_no, pi_name=pi, include_all_status=include_all_status)
|
||||
if not projects:
|
||||
message = "没有可播报的项目:请先录入台账(在研项目为空或筛选条件不匹配)"
|
||||
log.warning("task_failed task_type=broadcast error_code=NO_PROJECTS stage=broadcast")
|
||||
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="NO_PROJECTS", stage="broadcast")
|
||||
log.error("broadcast_no_projects stage=broadcast")
|
||||
print(f"ERROR:NO_PROJECTS {message}")
|
||||
return 1
|
||||
|
||||
result = run_broadcast(projects, as_of_date)
|
||||
payload = {k: v for k, v in result.items() if k != "reports"}
|
||||
payload["reports_brief"] = [
|
||||
{k: r[k] for k in ("report_id", "project_no", "project_name", "pi_name", "status_tag", "spend_rate", "time_progress", "deviation", "report_file")}
|
||||
for r in result["reports"]
|
||||
]
|
||||
_print_json(payload)
|
||||
emit(
|
||||
f"播报完成 {result['count']} 个项目:严重滞后 {result['status_counts'].get('严重滞后', 0)},"
|
||||
f"异常偏快 {result['status_counts'].get('支出异常偏快', 0)}",
|
||||
skill=SKILL_SLUG,
|
||||
stage="broadcast",
|
||||
)
|
||||
finish(
|
||||
status="success",
|
||||
message=f"播报完成 {result['count']} 个项目,简报已生成",
|
||||
skill=SKILL_SLUG,
|
||||
stage="broadcast",
|
||||
batch_no=result["batch_no"],
|
||||
)
|
||||
return 0
|
||||
except JobStopped:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=broadcast error_code=PROJECT_INVALID detail=%s", exc)
|
||||
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="broadcast")
|
||||
print(f"ERROR:PROJECT_INVALID {exc}")
|
||||
return 1
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=broadcast stage=broadcast")
|
||||
emit("播报失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="broadcast")
|
||||
finish(status="failed", message="播报执行异常,详见统一日志", skill=SKILL_SLUG, stage="broadcast")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_broadcast_monthly() -> int:
|
||||
"""月度播报(async Action 入口):全部在研项目逐项播报 + 汇总简报,任务中心可见进度。"""
|
||||
log = _get_task_logger()
|
||||
from datetime import date
|
||||
|
||||
from service.fund_service import query_projects, run_broadcast
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=broadcast_monthly stage=monthly")
|
||||
emit("开始月度经费播报", skill=SKILL_SLUG, stage="monthly")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
log.warning("entitlement_failed task_type=broadcast_monthly stage=auth")
|
||||
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
|
||||
log.error("broadcast_monthly_denied stage=auth")
|
||||
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||
return 1
|
||||
|
||||
projects = query_projects()
|
||||
if not projects:
|
||||
message = "台账中没有“在研”项目,月度播报跳过"
|
||||
log.warning("task_failed task_type=broadcast_monthly error_code=NO_PROJECTS stage=monthly")
|
||||
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="NO_PROJECTS", stage="monthly")
|
||||
log.error("broadcast_monthly_no_projects stage=monthly")
|
||||
print(f"ERROR:NO_PROJECTS {message}")
|
||||
return 1
|
||||
|
||||
result = run_broadcast(projects, date.today())
|
||||
for index, report in enumerate(result["reports"], start=1):
|
||||
emit(
|
||||
f"[{index}/{len(result['reports'])}] {report['project_name']}:{report['status_tag']}",
|
||||
skill=SKILL_SLUG,
|
||||
stage="monthly",
|
||||
)
|
||||
_print_json(
|
||||
{
|
||||
"ok": True,
|
||||
"batch_no": result["batch_no"],
|
||||
"as_of": result["as_of"],
|
||||
"count": result["count"],
|
||||
"status_counts": result["status_counts"],
|
||||
"brief_file": result["brief_file"],
|
||||
"reports_brief": [
|
||||
{k: r[k] for k in ("report_id", "project_no", "project_name", "pi_name", "status_tag", "summary")}
|
||||
for r in result["reports"]
|
||||
],
|
||||
}
|
||||
)
|
||||
finish(
|
||||
status="success",
|
||||
message=f"月度播报完成:{result['count']} 个项目,严重滞后 {result['status_counts'].get('严重滞后', 0)} 个",
|
||||
skill=SKILL_SLUG,
|
||||
stage="monthly",
|
||||
batch_no=result["batch_no"],
|
||||
)
|
||||
return 0
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=broadcast_monthly stage=monthly")
|
||||
emit("月度播报失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="monthly")
|
||||
finish(status="failed", message="月度播报执行异常,详见统一日志", skill=SKILL_SLUG, stage="monthly")
|
||||
return 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 播报记录与阈值
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def cmd_list_reports(limit: int = 20, status_tag: str = "") -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import list_reports
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=list_reports limit=%s status_tag=%s", limit, status_tag)
|
||||
try:
|
||||
reports = list_reports(limit=limit, status_tag=status_tag)
|
||||
_print_json({"ok": True, "count": len(reports), "reports": reports})
|
||||
return 0
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=list_reports")
|
||||
print("ERROR:QUERY_FAILED 播报记录查询异常,详见统一日志")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_show_report(report_id: int) -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import get_report
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=show_report report_id=%s", report_id)
|
||||
try:
|
||||
report = get_report(int(report_id))
|
||||
_print_json({"ok": True, "report": report})
|
||||
return 0
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=show_report error_code=REPORT_NOT_FOUND detail=%s", exc)
|
||||
print(f"ERROR:REPORT_NOT_FOUND {exc}")
|
||||
return 1
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=show_report")
|
||||
print("ERROR:QUERY_FAILED 播报记录查询异常,详见统一日志")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_render_html(report_id: Optional[int] = None) -> int:
|
||||
log = _get_task_logger()
|
||||
from service.report_service import build_report_for_report_id
|
||||
from service.project_service import list_reports
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=render_html report_id=%s stage=render", report_id)
|
||||
try:
|
||||
if report_id:
|
||||
targets = [int(report_id)]
|
||||
else:
|
||||
targets = [r["id"] for r in list_reports(limit=10)]
|
||||
if not targets:
|
||||
log.warning("task_failed task_type=render_html error_code=REPORT_NOT_FOUND stage=render")
|
||||
finish(status="failed", message="暂无可重建的播报记录", skill=SKILL_SLUG, error_code="REPORT_NOT_FOUND", stage="render")
|
||||
print("ERROR:REPORT_NOT_FOUND 暂无可重建的播报记录")
|
||||
return 1
|
||||
written = [{"report_id": rid, "path": build_report_for_report_id(rid)} for rid in targets]
|
||||
_print_json({"ok": True, "count": len(written), "reports": written})
|
||||
finish(status="success", message=f"已重建 {len(written)} 份经费执行简报", skill=SKILL_SLUG, stage="render")
|
||||
return 0
|
||||
except JobStopped:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=render_html error_code=REPORT_NOT_FOUND detail=%s", exc)
|
||||
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="REPORT_NOT_FOUND", stage="render")
|
||||
print(f"ERROR:REPORT_NOT_FOUND {exc}")
|
||||
return 1
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=render_html stage=render")
|
||||
finish(status="failed", message="简报重建异常,详见统一日志", skill=SKILL_SLUG, stage="render")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_view_rules() -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import view_rules
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=view_rules stage=rules")
|
||||
try:
|
||||
_print_json({"ok": True, "rules": view_rules()})
|
||||
return 0
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=view_rules")
|
||||
print("ERROR:QUERY_FAILED 预警规则查询异常,详见统一日志")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_set_rule(rule_key: str, value: float) -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import set_rule
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=set_rule rule_key=%s value=%s stage=rules", rule_key, value)
|
||||
emit("开始更新预警阈值", skill=SKILL_SLUG, stage="rules")
|
||||
try:
|
||||
ok, reason = check_entitlement(SKILL_SLUG)
|
||||
if not ok:
|
||||
log.warning("entitlement_failed task_type=set_rule stage=auth")
|
||||
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
|
||||
log.error("set_rule_denied stage=auth")
|
||||
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
|
||||
return 1
|
||||
payload = set_rule(rule_key, float(value))
|
||||
_print_json(payload)
|
||||
finish(status="success", message=f"阈值 {rule_key} 已更新为 {value}", skill=SKILL_SLUG, stage="rules")
|
||||
return 0
|
||||
except ValueError as exc:
|
||||
log.warning("task_failed task_type=set_rule error_code=RULE_INVALID detail=%s", exc)
|
||||
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="RULE_INVALID", stage="rules")
|
||||
print(f"ERROR:RULE_INVALID {exc}")
|
||||
return 1
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=set_rule stage=rules")
|
||||
finish(status="failed", message="阈值更新异常,详见统一日志", skill=SKILL_SLUG, stage="rules")
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_stats() -> int:
|
||||
log = _get_task_logger()
|
||||
from service.project_service import stats
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info("task_start task_type=stats")
|
||||
try:
|
||||
_print_json(stats())
|
||||
return 0
|
||||
except Exception:
|
||||
log.exception("task_failed task_type=stats")
|
||||
print("ERROR:QUERY_FAILED 统计查询异常,详见统一日志")
|
||||
return 1
|
||||
Reference in New Issue
Block a user