Files
skill-template/scripts/service/task_service.py
chendelian 4a0be93b4a
All checks were successful
技能自动化发布 / release (push) Successful in 5s
chore: auto release commit (2026-07-03 12:09:55)
2026-07-03 12:09:55 +08:00

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