All checks were successful
技能自动化发布 / release (push) Successful in 9s
- 状态标签:C3 触发且 D 未判 → 待补录,优先级对齐需求 §6.2 - broadcast-monthly 逐项目增量 emit,任务中心可见进行中进度 - progress_reports 回填 project_id;新增 items_json/updated_at;旧库重建式安全迁移(含回归测试) - 简报:TITLE 转义防注入、章节对齐 §10(独立「暂付款/结题提醒」)、页脚逐字统一 - 金标准测试 +17:±10%/20% 阈值恰界、C2 钳位、N2 反例、C3 逐字段、HTML 转义、async+cron 定位守护、迁移回归 - 文档 v1.1(开发文档 + REQUIREMENTS):ERROR:<CODE> <message>、reports_brief、--all-status、skill-detail 表外挂载、C1 level=warn - 落盘验收报告 acceptance-reports/20260924-105401-acceptance.md(338 测试全绿) - 版本 1.0.1 → 1.0.2(constants / SKILL.md / CHANGELOG 三处对齐)
622 lines
26 KiB
Python
622 lines
26 KiB
Python
"""任务编排层: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
|
||
|
||
def _emit_progress(index: int, total: int, report: dict[str, Any]) -> None:
|
||
emit(
|
||
f"[{index}/{total}] {report['project_name']}:{report['status_tag']}",
|
||
skill=SKILL_SLUG,
|
||
stage="monthly",
|
||
)
|
||
|
||
result = run_broadcast(projects, date.today(), on_progress=_emit_progress)
|
||
_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
|