Files
match-guide-track-default/scripts/service/task_service.py
chenfuqiang 4a8613b27e fix: 批量匹配跳过画像待补录教师 + render-html 按行重建 + 金标准 fixtures 业务化(P0+P1)
- run_match 新增 skip_incomplete:批量缺省跳过画像不完整教师并记录 skipped,指定单教师仍 PROFILE_INCOMPLETE;汇总页出"画像待补录"行
- render-html 补 --report-id 行上下文(inputMapping $row.$pk),CLI 容错解析回退最近 10 份
- report_service 日志改容错 _log(),修复未 setup 时单跑测试崩溃的顺序依赖
- fixtures 替换为教师画像/赛道/期望结论金标准样例,新增 sample_tracks.json
- 新增 4 个批量跳过用例(314 项,除 3 个已知 jsonschema 环境失败外全绿)
- tests/README 增补本机无共享 runtime 的测试方式;TUTORIAL/ACTIONS.md 同步表述
2026-09-24 18:01:41 +08:00

705 lines
30 KiB
Python
Raw 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/match_service.py``(匹配内核)、
``service/profile_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 这是模板演示入口,请使用 match 命令执行申报赛道匹配。")
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 os
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 ("teachers", "tracks", "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_teacher(
*,
name: str,
department: str = "",
title: str = "",
tags: str = "",
papers: str = "",
projects: str = "",
sci_q1: Optional[int] = None,
core_papers: Optional[int] = None,
) -> int:
log = _get_task_logger()
from service.profile_service import upsert_teacher
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=add_teacher teacher=%s stage=profile", name)
emit("开始写入教师画像", skill=SKILL_SLUG, stage="profile")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=add_teacher stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
log.error("add_teacher_denied stage=auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
fields: dict[str, Any] = {"teacher_name": name}
for key, value in (("department", department), ("title", title), ("research_tags", tags), ("recent_papers", papers), ("project_history", projects)):
if value:
fields[key] = value
if sci_q1 is not None:
fields["sci_q1_count"] = sci_q1
if core_papers is not None:
fields["core_paper_count"] = core_papers
payload = upsert_teacher(fields)
_print_json(payload)
finish(status="success", message=f"教师 {name} 画像已{('更新' if payload['action'] == 'updated' else '录入')}", skill=SKILL_SLUG, stage="profile")
return 0
except ValueError as exc:
log.warning("task_failed task_type=add_teacher error_code=TEACHER_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="TEACHER_INVALID", stage="profile")
print(f"ERROR:TEACHER_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=add_teacher stage=profile")
finish(status="failed", message="画像写入异常,详见统一日志", skill=SKILL_SLUG, stage="profile")
return 1
def cmd_import_teachers(teachers_json: str) -> int:
log = _get_task_logger()
from service.profile_service import import_teachers
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=import_teachers stage=profile")
emit("开始批量导入教师画像", skill=SKILL_SLUG, stage="profile")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=import_teachers stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
rows = _parse_json_arg(teachers_json, "--teachers-json")
payload = import_teachers(rows)
_print_json(payload)
finish(status="success", message=f"导入完成:新增 {payload['created']},更新 {payload['updated']}", skill=SKILL_SLUG, stage="profile")
return 0
except ValueError as exc:
log.warning("task_failed task_type=import_teachers error_code=TEACHER_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="TEACHER_INVALID", stage="profile")
print(f"ERROR:TEACHER_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=import_teachers stage=profile")
finish(status="failed", message="画像导入异常,详见统一日志", skill=SKILL_SLUG, stage="profile")
return 1
def cmd_import_tracks(tracks_json: str) -> int:
log = _get_task_logger()
from service.profile_service import import_tracks
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=import_tracks stage=track")
emit("开始批量导入指南赛道", skill=SKILL_SLUG, stage="track")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=import_tracks stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
rows = _parse_json_arg(tracks_json, "--tracks-json")
payload = import_tracks(rows)
_print_json(payload)
finish(status="success", message=f"导入完成:新增 {payload['created']},更新 {payload['updated']}", skill=SKILL_SLUG, stage="track")
return 0
except ValueError as exc:
log.warning("task_failed task_type=import_tracks error_code=TRACK_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="TRACK_INVALID", stage="track")
print(f"ERROR:TRACK_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=import_tracks stage=track")
finish(status="failed", message="赛道导入异常,详见统一日志", skill=SKILL_SLUG, stage="track")
return 1
def cmd_add_track(
*,
year: int,
track_name: str,
program: str = "",
agency: str = "",
tags: str = "",
priority: str = "",
min_title: str = "",
min_sci_q1: Optional[int] = None,
quota: Optional[int] = None,
source: str = "",
) -> int:
log = _get_task_logger()
from service.profile_service import upsert_track
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=add_track year=%s track=%s stage=track", year, track_name)
emit("开始写入指南赛道", skill=SKILL_SLUG, stage="track")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=add_track 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] = {"year": year, "track_name": track_name}
for key, value in (("agency", agency), ("program_name", program), ("track_tags", tags), ("priority_keywords", priority), ("min_title", min_title), ("source_doc", source)):
if value:
fields[key] = value
if min_sci_q1 is not None:
fields["min_sci_q1"] = min_sci_q1
if quota is not None:
fields["quota"] = quota
payload = upsert_track(fields)
_print_json(payload)
finish(status="success", message=f"赛道 {track_name} 已{('更新' if payload['action'] == 'updated' else '录入')}", skill=SKILL_SLUG, stage="track")
return 0
except ValueError as exc:
log.warning("task_failed task_type=add_track error_code=TRACK_INVALID detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="TRACK_INVALID", stage="track")
print(f"ERROR:TRACK_INVALID {exc}")
return 1
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=add_track stage=track")
finish(status="failed", message="赛道写入异常,详见统一日志", skill=SKILL_SLUG, stage="track")
return 1
def cmd_list_teachers(department: str = "") -> int:
log = _get_task_logger()
from service.profile_service import list_teachers
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list_teachers department=%s", department)
try:
teachers = list_teachers(department=department)
_print_json({"ok": True, "count": len(teachers), "teachers": teachers})
return 0
except Exception:
log.exception("task_failed task_type=list_teachers")
print("ERROR:QUERY_FAILED 教师画像查询异常,详见统一日志")
return 1
def cmd_list_tracks(year: Optional[int] = None) -> int:
log = _get_task_logger()
from service.profile_service import list_tracks
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list_tracks year=%s", year)
try:
tracks = list_tracks(year=year)
_print_json({"ok": True, "count": len(tracks), "tracks": tracks})
return 0
except Exception:
log.exception("task_failed task_type=list_tracks")
print("ERROR:QUERY_FAILED 赛道查询异常,详见统一日志")
return 1
# ---------------------------------------------------------------------------
# 匹配主命令
# ---------------------------------------------------------------------------
def cmd_match(*, teacher: str = "", year: Optional[int] = None) -> int:
"""申报赛道匹配(同步):缺省匹配全部教师;可指定教师与指南年度。"""
log = _get_task_logger()
from service.match_service import load_teachers, load_tracks, run_match
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=match teacher=%s year=%s stage=match", teacher, year)
emit("开始申报赛道匹配", skill=SKILL_SLUG, stage="match")
try:
ok, reason = check_entitlement(SKILL_SLUG)
if not ok:
log.warning("entitlement_failed task_type=match stage=auth")
finish(status="failed", message=reason, skill=SKILL_SLUG, error_code="ENTITLEMENT_DENIED", stage="auth")
print(f"ERROR:ENTITLEMENT_DENIED {reason}")
return 1
teachers = load_teachers(teacher_name=teacher)
tracks = load_tracks(year=year)
if not teachers:
message = "没有可匹配的教师画像:请先录入教师(add-teacher / import-teachers)"
log.warning("task_failed task_type=match error_code=TEACHER_NOT_FOUND stage=match")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="TEACHER_NOT_FOUND", stage="match")
print(f"ERROR:TEACHER_NOT_FOUND {message}")
return 1
if not tracks:
message = "指南赛道库为空:请先录入赛道(add-track / import-tracks)"
log.warning("task_failed task_type=match error_code=TRACK_EMPTY stage=match")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="TRACK_EMPTY", stage="match")
print(f"ERROR:TRACK_EMPTY {message}")
return 1
season = str(year or tracks[0].get("year") or "")
# 指定教师 → 严格模式,画像不完整直接 PROFILE_INCOMPLETE;
# 缺省全量 → 跳过画像待补录教师并记录,不中断批次(E1)。
result = run_match(teachers, tracks, season, skip_incomplete=not teacher)
payload = {k: v for k, v in result.items() if k != "reports"}
payload["reports_brief"] = [
{
"report_id": r["report_id"],
"teacher_name": r["teacher_name"],
"tier": (r["top3"][0]["tier"] if r["top3"] else "无可推荐"),
"top_track": (r["top3"][0]["track"] if r["top3"] else ""),
"match_pct": (r["top3"][0]["match_pct"] if r["top3"] else None),
"report_file": r["report_file"],
}
for r in result["reports"]
]
_print_json(payload)
skipped_count = len(result.get("skipped") or [])
message = f"匹配完成 {result['count']} 名教师,建议书已生成"
if skipped_count:
message += f";{skipped_count} 名教师画像待补录已跳过"
log.info("match_skipped count=%s stage=match", skipped_count)
finish(
status="success",
message=message,
skill=SKILL_SLUG,
stage="match",
batch_no=result["batch_no"],
)
return 0
except JobStopped:
raise
except ValueError as exc:
log.warning("task_failed task_type=match error_code=PROFILE_INCOMPLETE detail=%s", exc)
finish(status="failed", message=str(exc), skill=SKILL_SLUG, error_code="PROFILE_INCOMPLETE", stage="match")
print(f"ERROR:PROFILE_INCOMPLETE {exc}")
return 1
except Exception:
log.exception("task_failed task_type=match stage=match")
emit("匹配失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="match")
finish(status="failed", message="匹配执行异常,详见统一日志", skill=SKILL_SLUG, stage="match")
return 1
def cmd_match_batch_input() -> int:
"""批量匹配(async Action 入口):匹配 input/ 目录中的教师画像 JSON 与库内赛道。"""
log = _get_task_logger()
from service.match_service import load_tracks, run_match
from util.runtime_paths import get_input_dir
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=match_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=match_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
input_dir = get_input_dir()
files = sorted(
os.path.join(input_dir, name)
for name in os.listdir(input_dir)
if name.lower().endswith(".json")
)
if not files:
message = f"input 目录没有教师画像 JSON:{input_dir}"
log.warning("task_failed task_type=match_batch error_code=TEACHER_NOT_FOUND stage=batch")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="TEACHER_NOT_FOUND", stage="batch")
print(f"ERROR:TEACHER_NOT_FOUND {message}")
return 1
teachers = []
for path in files:
with open(path, encoding="utf-8") as f:
payload = json.load(f)
if isinstance(payload, dict):
payload = payload.get("teachers") or [payload]
teachers.extend(payload)
tracks = load_tracks()
if not tracks:
message = "指南赛道库为空:请先录入赛道(add-track / import-tracks)"
log.warning("task_failed task_type=match_batch error_code=TRACK_EMPTY stage=batch")
finish(status="failed", message=message, skill=SKILL_SLUG, error_code="TRACK_EMPTY", stage="batch")
print(f"ERROR:TRACK_EMPTY {message}")
return 1
season = str(tracks[0].get("year") or "")
result = run_match(teachers, tracks, season, skip_incomplete=True)
for index, report in enumerate(result["reports"], start=1):
top = (report.get("top3") or [{}])[0]
emit(
f"[{index}/{len(result['reports'])}] {report['teacher_name']}:"
f"{(top.get('tier') or '无可推荐')}{(' · ' + top.get('track', '')) if top.get('track') else ''}",
skill=SKILL_SLUG,
stage="batch",
)
for entry in result.get("skipped") or []:
emit(
f"{entry.get('teacher_name')}:画像待补录,本期已跳过({entry.get('reason')})",
type="warn",
skill=SKILL_SLUG,
stage="batch",
)
_print_json(
{
"ok": True,
"batch_no": result["batch_no"],
"count": result["count"],
"skipped": result.get("skipped") or [],
"tier_counts": result["tier_counts"],
"brief_file": result["brief_file"],
}
)
message = f"批量匹配完成 {result['count']} 名教师"
if result.get("skipped"):
message += f",{len(result['skipped'])} 名画像待补录已跳过"
finish(status="success", message=message, skill=SKILL_SLUG, stage="batch", batch_no=result["batch_no"])
return 0
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=match_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_reports(limit: int = 20) -> int:
log = _get_task_logger()
from service.profile_service import list_reports
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=list_reports limit=%s", limit)
try:
reports = list_reports(limit=limit)
_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.profile_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.profile_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_stats() -> int:
log = _get_task_logger()
from service.profile_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