Files

707 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""任务编排层:job_context / emit / finish、鉴权与错误转换。
领域业务在 ``service/draft_service.py``(初稿内核)、
``service/ledger_service.py``(台账/记录)、``service/docx_service.py``(Word 渲染)、
``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 这是模板演示入口,请使用 draft-report 命令生成结题报告初稿。")
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:
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()})
return 0
# ---------------------------------------------------------------------------
# 台账维护
# ---------------------------------------------------------------------------
def _parse_json_arg(raw: str, label: str) -> list[dict[str, Any]]:
import sys
text = (raw or "").strip()
if not text:
raise ValueError(f"缺少导入 JSON({label})")
if text == "-":
text = sys.stdin.read().strip()
elif text.startswith(("[", "{")):
pass
elif os.path.isfile(text):
with open(text, encoding="utf-8") as f:
text = f.read().strip()
else:
raise ValueError(f"{label} 不是有效文件路径或 JSON:{text[:80]}")
try:
payload = json.loads(text)
except json.JSONDecodeError as exc:
raise ValueError(f"导入 JSON 解析失败:{exc}") from exc
if isinstance(payload, dict):
for key in ("projects", "achievements", "expenses", "items"):
if isinstance(payload.get(key), list):
payload = payload[key]
break
else:
payload = [payload]
if not isinstance(payload, list):
raise ValueError(f"{label} 应为对象数组")
return payload
def cmd_add_project(
*,
project_no: str,
name: str = "",
pi: str = "",
department: str = "",
program: str = "",
start: str = "",
end: str = "",
approved_budget: str = "",
expected_json: str = "",
excerpt: str = "",
status: str = "",
) -> int:
log = _get_task_logger()
from service.ledger_service import upsert_project
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=add_project project_no=%s stage=ledger", project_no)
emit("开始写入结题项目", skill=SKILL_SLUG, stage="ledger")
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")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
fields: dict[str, Any] = {"project_no": project_no}
for key, value in (("project_name", name), ("pi_name", pi), ("department", department), ("program", program), ("start_date", start), ("end_date", end), ("approved_budget", approved_budget), ("expected_json", expected_json), ("annual_report_excerpt", excerpt), ("status", status)):
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="ledger")
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="ledger")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=add_project stage=ledger")
finish(status="failed", message="项目写入异常,详见统一日志", skill=SKILL_SLUG, stage="ledger")
return 1
def cmd_add_achievement(
*,
project_no: str,
ach_type: str,
title: str,
venue: str = "",
date: str = "",
representative: str = "",
note: str = "",
) -> int:
log = _get_task_logger()
from service.ledger_service import upsert_achievement
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=add_achievement project_no=%s stage=ledger", project_no)
emit("开始写入成果", skill=SKILL_SLUG, stage="ledger")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=add_achievement stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
payload = upsert_achievement(
{"project_no": project_no, "achievement_type": ach_type, "title": title, "venue_or_no": venue, "ach_date": date, "is_representative": representative, "note": note}
)
_print_json(payload)
finish(status="success", message=f"成果已录入:{title[:20]}", skill=SKILL_SLUG, stage="ledger")
return 0
except ValueError as exc:
log.warning("task_failed task_type=add_achievement error_code=PROJECT_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="ledger")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=add_achievement stage=ledger")
finish(status="failed", message="成果写入异常,详见统一日志", skill=SKILL_SLUG, stage="ledger")
return 1
def cmd_add_expense(*, project_no: str, category: str, amount: float, date: str = "", description: str = "") -> int:
log = _get_task_logger()
from service.ledger_service import upsert_expense
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=add_expense project_no=%s stage=ledger", project_no)
emit("开始写入决算明细", skill=SKILL_SLUG, stage="ledger")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=add_expense stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
payload = upsert_expense({"project_no": project_no, "category": category, "amount": amount, "occurred_date": date, "description": description})
_print_json(payload)
finish(status="success", message=f"决算明细已录入:{category} {payload['amount']} 元", skill=SKILL_SLUG, stage="ledger")
return 0
except ValueError as exc:
log.warning("task_failed task_type=add_expense error_code=PROJECT_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="ledger")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=add_expense stage=ledger")
finish(status="failed", message="决算写入异常,详见统一日志", skill=SKILL_SLUG, stage="ledger")
return 1
def cmd_import(projects_json: str = "", achievements_json: str = "", expenses_json: str = "") -> int:
"""统一导入入口:projects/achievements/expenses 三类 JSON(至少提供一个)。"""
log = _get_task_logger()
from service.ledger_service import import_achievements, import_expenses, import_projects
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=import stage=ledger")
emit("开始批量导入结题数据", skill=SKILL_SLUG, stage="ledger")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=import stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
summary: dict[str, Any] = {"ok": True}
if projects_json:
summary["projects"] = import_projects(_parse_json_arg(projects_json, "--projects-json"))
if achievements_json:
summary["achievements"] = import_achievements(_parse_json_arg(achievements_json, "--achievements-json"))
if expenses_json:
summary["expenses"] = import_expenses(_parse_json_arg(expenses_json, "--expenses-json"))
if len(summary) == 1:
raise ValueError("至少提供一个导入 JSON(--projects-json / --achievements-json / --expenses-json)")
_print_json(summary)
finish(status="success", message="批量导入完成", skill=SKILL_SLUG, stage="ledger")
return 0
except ValueError as exc:
log.warning("task_failed task_type=import error_code=PROJECT_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="ledger")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=import stage=ledger")
finish(status="failed", message="批量导入异常,详见统一日志", skill=SKILL_SLUG, stage="ledger")
return 1
def _cmd_import_one(label: str, raw_json: str, import_fn) -> int:
"""单一台账批量导入(import-projects / import-achievements / import-expenses 共用)。"""
log = _get_task_logger()
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=import_%s stage=ledger", label)
emit(f"开始批量导入{label}", skill=SKILL_SLUG, stage="ledger")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=import_%s stage=auth", label)
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
payload = import_fn(_parse_json_arg(raw_json, f"--{label}-json"))
_print_json(payload)
finish(status="success", message=f"导入完成:新增 {payload['created']},更新 {payload['updated']}", skill=SKILL_SLUG, stage="ledger")
return 0
except ValueError as exc:
log.warning("task_failed task_type=import_%s error_code=PROJECT_INVALID detail=%s", label, exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="ledger")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=import_%s stage=ledger", label)
finish(status="failed", message="批量导入异常,详见统一日志", skill=SKILL_SLUG, stage="ledger")
return 1
def cmd_import_projects(projects_json: str) -> int:
from service.ledger_service import import_projects
return _cmd_import_one("projects", projects_json, import_projects)
def cmd_import_achievements(achievements_json: str) -> int:
from service.ledger_service import import_achievements
return _cmd_import_one("achievements", achievements_json, import_achievements)
def cmd_import_expenses(expenses_json: str) -> int:
from service.ledger_service import import_expenses
return _cmd_import_one("expenses", expenses_json, import_expenses)
def _cmd_list_one(label: str, fetch) -> int:
log = _get_task_logger()
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list_%s", label)
try:
rows = fetch()
_print_json({"ok": True, "count": len(rows), "rows": rows})
return 0
except Exception:
log.exception("task_failed task_type=list_%s", label)
print("ERROR:QUERY_FAILED 台账查询异常,详见统一日志")
return 1
def cmd_list_projects(status: str = "") -> int:
from service.ledger_service import list_projects
return _cmd_list_one("projects", lambda: list_projects(status=status))
def cmd_list_achievements(project_no: str) -> int:
from service.ledger_service import list_achievements
return _cmd_list_one("achievements", lambda: list_achievements(project_no))
def cmd_list_expenses(project_no: str) -> int:
from service.ledger_service import list_expenses
return _cmd_list_one("expenses", lambda: list_expenses(project_no))
def cmd_list(kind: str = "projects", project_no: str = "", status: str = "") -> int:
log = _get_task_logger()
from service.ledger_service import list_achievements, list_expenses, list_projects
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list kind=%s project_no=%s", kind, project_no)
try:
if kind == "achievements":
rows = list_achievements(project_no)
elif kind == "expenses":
rows = list_expenses(project_no)
else:
rows = list_projects(status=status)
_print_json({"ok": True, "count": len(rows), "rows": rows})
return 0
except Exception:
log.exception("task_failed task_type=list")
print("ERROR:QUERY_FAILED 台账查询异常,详见统一日志")
return 1
# ---------------------------------------------------------------------------
# 初稿生成
# ---------------------------------------------------------------------------
def cmd_draft_report(*, project_no: str) -> int:
"""结题报告初稿(主命令,同步):指定项目生成 Word 初稿 + HTML 预览。"""
log = _get_task_logger()
from service.draft_service import query_pending_projects, run_draft
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=draft_report project_no=%s stage=draft", project_no)
emit("开始生成结题报告初稿", skill=SKILL_SLUG, stage="draft")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=draft_report stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
projects = query_pending_projects(project_no=project_no)
if not projects:
message = f"未找到该项目(编号 {project_no})或项目状态不是“待结题”"
log.warning("task_failed task_type=draft_report error_code=PROJECT_NOT_FOUND stage=draft")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="PROJECT_NOT_FOUND", stage="draft")
print(f"ERROR:PROJECT_NOT_FOUND {message}")
return 1
project = projects[0]
achievements = []
expenses = []
from service.ledger_service import list_achievements, list_expenses
achievements = list_achievements(project_no)
expenses = list_expenses(project_no)
result = run_draft(project, achievements, expenses)
_print_json(result)
finish(
status="success",
message=f"初稿已生成:{result['draft_file']}",
skill=SKILL_SLUG,
stage="draft",
batch_no=result["batch_no"],
)
return 0
except JobStopped:
raise
except ValueError as exc:
log.warning("task_failed task_type=draft_report error_code=PROJECT_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROJECT_INVALID", stage="draft")
print(f"ERROR:PROJECT_INVALID {exc}")
return 1
except Exception:
log.exception("task_failed task_type=draft_report stage=draft")
emit("初稿生成失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="draft")
finish(status="failed", message="初稿生成异常,详见统一日志", skill=SKILL_SLUG, stage="draft")
return 1
def cmd_draft_batch_pending() -> int:
"""批量初稿(async Action 入口):为全部“待结题”项目生成初稿,任务中心可见进度。"""
log = _get_task_logger()
from service.draft_service import query_pending_projects, run_draft
from service.ledger_service import list_achievements, list_expenses
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=draft_batch stage=batch")
emit("开始批量生成结题初稿", skill=SKILL_SLUG, stage="batch")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=draft_batch stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
projects = query_pending_projects()
if not projects:
message = "没有“待结题”状态的项目,批量生成跳过"
log.warning("task_failed task_type=draft_batch error_code=PROJECT_NOT_FOUND stage=batch")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="PROJECT_NOT_FOUND", stage="batch")
print(f"ERROR:PROJECT_NOT_FOUND {message}")
return 1
drafts = []
for index, project in enumerate(projects, start=1):
emit(f"[{index}/{len(projects)}] {project.get('project_name') or project.get('project_no')}", skill=SKILL_SLUG, stage="batch")
try:
result = run_draft(project, list_achievements(project["project_no"]), list_expenses(project["project_no"]))
drafts.append({"draft_id": result["draft_id"], "project_no": result["project_no"], "draft_file": result["draft_file"]})
except Exception:
log.exception("draft_batch_item_failed project_no=%s", project.get("project_no"))
_print_json({"ok": True, "count": len(drafts), "drafts": drafts})
finish(status="success", message=f"批量初稿完成 {len(drafts)}/{len(projects)} 个项目", skill=SKILL_SLUG, stage="batch")
return 0
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=draft_batch stage=batch")
emit("批量初稿失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="batch")
finish(status="failed", message="批量初稿执行异常,详见统一日志", skill=SKILL_SLUG, stage="batch")
return 1
# ---------------------------------------------------------------------------
# 记录与统计
# ---------------------------------------------------------------------------
def cmd_list_drafts(limit: int = 20) -> int:
log = _get_task_logger()
from service.ledger_service import list_drafts
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list_drafts limit=%s", limit)
try:
drafts = list_drafts(limit=limit)
_print_json({"ok": True, "count": len(drafts), "drafts": drafts})
return 0
except Exception:
log.exception("task_failed task_type=list_drafts")
print("ERROR:QUERY_FAILED 初稿记录查询异常,详见统一日志")
return 1
def cmd_show_draft(draft_id: int) -> int:
log = _get_task_logger()
from service.ledger_service import get_draft
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=show_draft draft_id=%s", draft_id)
try:
draft = get_draft(int(draft_id))
_print_json({"ok": True, "draft": draft})
return 0
except ValueError as exc:
log.warning("task_failed task_type=show_draft error_code=DRAFT_NOT_FOUND detail=%s", exc)
print(f"ERROR:DRAFT_NOT_FOUND {exc}")
return 1
except Exception:
log.exception("task_failed task_type=show_draft")
print("ERROR:QUERY_FAILED 初稿记录查询异常,详见统一日志")
return 1
def cmd_render_html(report_id: Optional[int] = None) -> int:
log = _get_task_logger()
from service.ledger_service import list_drafts
from service.report_service import build_preview_for_draft_id
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 = [d["id"] for d in list_drafts(limit=10)]
if not targets:
log.warning("task_failed task_type=render_html error_code=DRAFT_NOT_FOUND stage=render")
finish(status="failed", message="暂无可重建的初稿记录", skill=SKILL_SLUG, error_code="DRAFT_NOT_FOUND", stage="render")
print("ERROR:DRAFT_NOT_FOUND 暂无可重建的初稿记录")
return 1
written = [{"draft_id": did, "path": build_preview_for_draft_id(did)} for did in targets]
_print_json({"ok": True, "count": len(written), "previews": 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=DRAFT_NOT_FOUND detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="DRAFT_NOT_FOUND", stage="render")
print(f"ERROR:DRAFT_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_stats() -> int:
log = _get_task_logger()
from service.ledger_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