Files
skill-template/scripts/service/task_service.py
chendelian 6b64aad138
All checks were successful
技能自动化发布 / release (push) Successful in 4s
feat: add standard init-db CLI for host install schema ensure
2026-07-14 17:26:24 +08:00

321 lines
11 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 / checkpoint / finish、鉴权、任务日志与错误转换。
领域业务请放在 ``service/<domain>_service.py``由本模块调用多入口CLI/Action/cron复用同一内核。
复制后在 cmd_run或 cmd_sync_*)中接线到真实 domain service勿按 toolbar/cron/agent 分叉业务。
"""
from __future__ import annotations
import asyncio
import json
import os
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.activity import JobStopped, emit, finish, job_context
from db import task_logs_repository as tlr
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_data_dir, get_skill_root
from util.timeutil import unix_to_iso
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()
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_data_dir()
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:
"""通用任务执行入口模板。复制后请实现真实业务逻辑。"""
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",
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 reason=%s", task_type, reason)
emit("鉴权失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="auth")
tlr.save_task_log(
task_type=task_type,
target_id=target,
input_id=input_id,
input_title="模板示例任务",
status="failed",
error_msg=reason,
result_summary=json.dumps({"stage": "auth", "error": reason}, ensure_ascii=False),
)
log.info(
"task_log_saved task_type=%s target_id=%s input_id=%s stage=auth status=failed",
task_type,
target,
input_id,
)
finish(
status="failed",
message=reason,
skill=SKILL_SLUG,
error_code="ENTITLEMENT_DENIED",
stage="auth",
)
print(f"{reason}")
return 1
log.info("task_run_demo_start target_id=%s input_id=%s", target, input_id)
rc, video_info = asyncio.run(_run_template_demo(target, input_id))
log.info("task_run_demo_done target_id=%s input_id=%s 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,
)
tlr.save_task_log(
task_type=task_type,
target_id=target,
input_id=input_id,
input_title="模板示例任务",
status="failed",
error_msg="模板仓库未实现真实业务",
result_summary=json.dumps(summary_payload, ensure_ascii=False),
)
log.info(
"task_log_saved task_type=%s target_id=%s input_id=%s status=failed",
task_type,
target,
input_id,
)
finish(
status="failed",
message="模板仓库未实现真实业务",
skill=SKILL_SLUG,
template_demo=True,
target=target,
input_id=input_id,
)
print("❌ 这是模板仓库,请复制后在 scripts/service/task_service.py 中实现 cmd_run 的真实业务逻辑。")
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")
try:
tlr.save_task_log(
task_type=task_type,
target_id=target,
input_id=input_id,
input_title="模板示例任务",
status="failed",
error_msg="任务执行异常,详见统一日志",
result_summary=json.dumps(
{"stage": "run", "error": "unexpected_exception"},
ensure_ascii=False,
),
)
log.info(
"task_log_saved task_type=%s target_id=%s input_id=%s stage=run status=failed",
task_type,
target,
input_id,
)
except Exception:
log.exception(
"task_log_save_failed task_type=%s target_id=%s input_id=%s",
task_type,
target,
input_id,
)
finish(
status="failed",
message="任务执行异常,详见统一日志",
skill=SKILL_SLUG,
stage="run",
)
return 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
def cmd_init_db() -> int:
"""幂等创建/迁移本地 SQLite 与数据管理元数据(宿主 install/upgrade 可调用)。"""
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.dumps(
{
"ok": True,
"skill": SKILL_SLUG,
"db_path": get_db_path(),
},
ensure_ascii=False,
)
)
return 0