301 lines
9.2 KiB
Python
301 lines
9.2 KiB
Python
"""任务编排:下载 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
|