Files
add-todo-all/examples/todo_skill/scripts/service/task_service.py
2026-09-09 10:29:02 +08:00

190 lines
6.8 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.
"""任务编排层(对齐模板 task_service.py 的职责分工)。
- ``cmd_health`` / ``cmd_version`` / ``cmd_config_path`` / ``cmd_init_db``:标准 CLI;
- ``cmd_remind``:演示 async 任务范式(``job_context`` + ``emit`` + ``finish``),
真实平台会推送到任务中心 UI;本 demo 在 stdout 打印 JSON Lines 模拟。
业务内核(CRUD)在 ``todo_service``,本模块只做编排,不复制业务逻辑。
"""
from __future__ import annotations
import asyncio
import json
import os
import time
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 db.connection import init_db
from service import todo_service
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
def _get_task_logger():
try:
return get_skill_logger()
except RuntimeError:
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
return get_skill_logger()
# ---------------- 标准命令(sync,当场返回) ----------------
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)}",
f"db_path: {get_db_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 与数据管理元数据。"""
try:
init_db()
except Exception as exc: # 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
# ---------------- 业务命令(sync) ----------------
def cmd_list(*, status: Optional[str] = None, limit: int = 100) -> int:
todos = todo_service.list_todos(status=status, limit=limit)
print(json.dumps(
{"count": len(todos), "items": [t.to_dict() for t in todos]},
ensure_ascii=False,
))
return 0
def cmd_add(*, title: str, priority: str = "normal", due_at: Optional[int] = None) -> int:
try:
todo = todo_service.add_todo(title=title, priority=priority, due_at=due_at)
except ValueError as exc:
print(f"ERROR:TODO_INVALID: {exc}")
return 1
print(json.dumps(todo.to_dict(), ensure_ascii=False))
return 0
def cmd_done(*, todo_ids: list[int]) -> int:
"""批量标记完成。返回 JSON 列表,每项含 ok/id/error。"""
results: list[dict] = []
for tid in todo_ids:
try:
todo = todo_service.mark_done(tid)
results.append({"ok": True, "id": tid, "todo": todo.to_dict()})
except ValueError as exc:
results.append({"ok": False, "id": tid, "error": str(exc)})
print(json.dumps({"count": len(results), "items": results}, ensure_ascii=False))
return 0 if all(r["ok"] for r in results) else 1
def cmd_delete(*, todo_ids: list[int]) -> int:
"""批量删除。"""
results: list[dict] = []
for tid in todo_ids:
deleted = todo_service.delete_todo(tid)
results.append({"ok": deleted, "id": tid})
print(json.dumps({"count": len(results), "items": results}, ensure_ascii=False))
return 0 if all(r["ok"] for r in results) else 1
def cmd_stats() -> int:
print(json.dumps(todo_service.stats(), ensure_ascii=False))
return 0
# ---------------- async 任务范式(demo) ----------------
async def _run_remind_async(*, seconds: int) -> dict[str, Any]:
"""模拟长耗时任务:每秒 emit 一次进度,最后 finish 终态。
真实平台会推送到任务中心 UI 并支持取消;本 demo 在 stdout 打印 JSON Lines。
"""
steps = max(1, int(seconds))
for i in range(1, steps + 1):
await asyncio.sleep(1)
emit(f"提醒任务进度 {i}/{steps}", skill=SKILL_SLUG, stage="remind", progress=i, total=steps)
# 真实业务在这里会查 todos 表挑出 due/pending 项发出提醒;demo 不做真实推送
summary = todo_service.stats()
return {"remind": True, "duration_seconds": steps, "stats": summary}
def cmd_remind(*, seconds: int = 3) -> int:
"""演示 async 任务编排。CLI 入口仍走同一内核;Agent 真实调度时
通过 ``run_skill_action``(executionProfile: async)触发。"""
log = _get_task_logger()
task_type = "remind"
with job_context(skill=SKILL_SLUG):
log.info("task_start task_type=%s stage=remind seconds=%s", task_type, seconds)
emit("开始执行提醒任务", skill=SKILL_SLUG, stage="remind")
try:
result = asyncio.run(_run_remind_async(seconds=seconds))
log.info("task_done task_type=%s status=ok", task_type)
finish(
status="ok",
message=f"提醒任务完成,处理 {seconds} 秒",
skill=SKILL_SLUG,
stage="remind",
**result,
)
print(json.dumps({"ok": True, "task_type": task_type, **result}, ensure_ascii=False))
return 0
except JobStopped:
raise
except Exception:
log.exception("task_failed task_type=%s", task_type)
emit("任务失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="remind")
finish(
status="failed",
message="提醒任务执行异常,详见统一日志",
skill=SKILL_SLUG,
stage="remind",
)
return 1