"""任务编排:下载 Amazon 结算报表、写入 task_logs。""" from __future__ import annotations import asyncio import json import os import re import uuid from typing import Any, Dict, Optional from jiangchang_skill_core import collect_runtime_diagnostics, config, format_runtime_health_lines from jiangchang_skill_core.rpa.video_session import RpaVideoSession from db import task_logs_repository as tlr from service.amazon_report_adapter import get_adapter 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 ( DEFAULT_DATE_FROM, DEFAULT_DATE_TO, PLATFORM_KIT_MIN_VERSION, SKILL_SLUG, SKILL_VERSION, ) from util.runtime_paths import get_skill_data_dir, get_skill_root from util.timeutil import unix_to_iso _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") def _validate_date(label: str, value: str) -> Optional[str]: v = (value or "").strip() if not v: return f"{label} 不能为空" if not _DATE_RE.match(v): return f"{label} 格式须为 YYYY-MM-DD" return None def _artifacts_dir(batch_id: str) -> str: return os.path.join(get_skill_data_dir(), "rpa-artifacts", batch_id) def _downloads_dir() -> str: path = os.path.join(get_skill_data_dir(), "downloads") os.makedirs(path, exist_ok=True) return path async def _run_with_video( *, date_from: str, date_to: str, seller_code: Optional[str], batch_id: str, ) -> tuple[dict[str, Any], dict[str, Any]]: """可选 RpaVideoSession 包裹 adapter 执行。""" data_dir = get_skill_data_dir() art_dir = _artifacts_dir(batch_id) os.makedirs(art_dir, exist_ok=True) async with RpaVideoSession( skill_slug=SKILL_SLUG, skill_data_dir=data_dir, batch_id=batch_id, title="开始下载 Amazon 结算报表", closing_title="结算报表下载完成", ) as video: video.add_step("准备下载结算报表") video.add_step(f"日期范围:{date_from} 至 {date_to}") if seller_code: video.add_step(f"指定店铺:{seller_code}") adapter = get_adapter(artifacts_dir=art_dir) video.add_step(f"使用 adapter:{adapter.name}") result = adapter.download_settlement( date_from=date_from, date_to=date_to, seller_code=seller_code, downloads_dir=_downloads_dir(), artifacts_dir=art_dir, ) if result.ok: video.add_step(f"下载成功:{result.filename or ''}") else: video.add_step(f"下载失败:{result.error_msg or '未知错误'}") video_summary = video.summary() video_info = build_video_info(video_summary, {}, {}) payload: dict[str, Any] = { "ok": result.ok, "job_id": result.job_id, "file_path": result.file_path, "filename": result.filename, "date_from": result.date_from, "date_to": result.date_to, "seller_code": result.seller_code or seller_code, "adapter": result.artifacts.get("adapter") or adapter.name, "artifacts": result.artifacts, } if not result.ok: payload["error"] = result.error_msg return payload, video_info def cmd_run( date_from: Optional[str] = None, date_to: Optional[str] = None, seller_code: Optional[str] = None, ) -> int: ok, reason = check_entitlement(SKILL_SLUG) if not ok: print(f"ERROR:ENTITLEMENT {reason}") return 1 d_from = (date_from or DEFAULT_DATE_FROM).strip() d_to = (date_to or DEFAULT_DATE_TO).strip() seller = (seller_code or "").strip() or None for label, val in (("date_from", d_from), ("date_to", d_to)): err = _validate_date(label, val) if err: print(f"ERROR:INVALID_ARGS {err}") return 1 batch_id = uuid.uuid4().hex[:12] record_video = config.get_bool("OPENCLAW_RECORD_VIDEO", default=True) if record_video: payload, video_info = asyncio.run( _run_with_video( date_from=d_from, date_to=d_to, seller_code=seller, batch_id=batch_id, ) ) else: art_dir = _artifacts_dir(batch_id) os.makedirs(art_dir, exist_ok=True) adapter = get_adapter(artifacts_dir=art_dir) result = adapter.download_settlement( date_from=d_from, date_to=d_to, seller_code=seller, downloads_dir=_downloads_dir(), artifacts_dir=art_dir, ) payload = { "ok": result.ok, "job_id": result.job_id, "file_path": result.file_path, "filename": result.filename, "date_from": result.date_from, "date_to": result.date_to, "seller_code": result.seller_code or seller, "adapter": result.artifacts.get("adapter") or adapter.name, "artifacts": result.artifacts, } if not result.ok: payload["error"] = result.error_msg video_info = {} _print_video_summary(video_info if record_video else None) summary_payload = merge_video_into_result_summary(payload, video_info if record_video else None) status = "success" if payload.get("ok") else "failed" tlr.save_task_log( task_type="download_settlement", target_id=seller or "default", input_id=f"{d_from}_{d_to}", input_title=f"结算报表 {d_from} ~ {d_to}", status=status, error_msg=None if payload.get("ok") else str(payload.get("error") or ""), result_summary=json.dumps(summary_payload, ensure_ascii=False), ) stdout_payload = { "ok": payload.get("ok"), "job_id": payload.get("job_id"), "file_path": payload.get("file_path"), "filename": payload.get("filename"), "date_from": payload.get("date_from"), "date_to": payload.get("date_to"), } print(json.dumps(stdout_payload, ensure_ascii=False)) return 0 if payload.get("ok") else 1 def cmd_logs( limit: int = 10, status: Optional[str] = None, task_type: Optional[str] = None, target_id: Optional[str] = None, ) -> int: if limit <= 0: limit = 10 rows = tlr.list_task_logs(limit, status, task_type, target_id) if not rows: print("暂无任务日志") return 0 sep_line = "_" * 39 for idx, r in enumerate(rows): rid, ttype, tid, iid, ititle, st, err, rsum, cat, uat = r print(f"id:{rid}") print(f"task_type:{ttype or ''}") print(f"target_id:{tid or ''}") print(f"input_id:{iid or ''}") print(f"input_title:{ititle or ''}") print(f"status:{st or ''}") print(f"error_msg:{err or ''}") print(f"result_summary:{rsum or ''}") print(f"created_at:{unix_to_iso(cat) or str(cat or '')}") print(f"updated_at:{unix_to_iso(uat) or str(uat or '')}") if idx != len(rows) - 1: print(sep_line) print() return 0 def cmd_log_get(log_id: str) -> int: if not str(log_id).isdigit(): print("❌ log_id 必须是数字") return 1 row = tlr.get_task_log_by_id(int(log_id)) if not row: print("❌ 没有这条任务日志") return 1 rid, ttype, tid, iid, ititle, st, err, rsum, cat, uat = row print( json.dumps( { "id": int(rid), "task_type": ttype, "target_id": tid, "input_id": iid, "input_title": ititle, "status": st, "error_msg": err, "result_summary": rsum, "created_at": unix_to_iso(cat), "updated_at": unix_to_iso(uat), }, ensure_ascii=False, ) ) return 0 def cmd_config_path() -> int: example_path = os.path.join(get_skill_root(), ".env.example") env_path = config.get_env_file_path() or "" print( json.dumps( { "skill": SKILL_SLUG, "env_path": env_path, "example_path": os.path.abspath(example_path), }, ensure_ascii=False, ) ) 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.dumps({"version": SKILL_VERSION, "skill": SKILL_SLUG}, ensure_ascii=False)) return 0