test
This commit is contained in:
0
scripts/service/__init__.py
Normal file
0
scripts/service/__init__.py
Normal file
@@ -1,17 +1,17 @@
|
||||
"""任务编排模板。
|
||||
"""任务编排层(对齐模板 task_service.py 的职责分工)。
|
||||
|
||||
编排层:job_context / emit / checkpoint / finish、鉴权与错误转换。
|
||||
过程看统一文件日志;进度/终态看任务中心;业务结果写入业务表(有才建)。
|
||||
领域业务请放在 ``service/<domain>_service.py``,由本模块调用;多入口(CLI/Action/cron)复用同一内核。
|
||||
复制后在 cmd_run(或 cmd_sync_*)中接线到真实 domain service,勿按 toolbar/cron/agent 分叉业务。
|
||||
- ``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 uuid
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
from jiangchang_skill_core import collect_runtime_diagnostics, config, format_runtime_health_lines
|
||||
@@ -29,6 +29,9 @@ 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 db.connection import init_db
|
||||
|
||||
from service import todo_service
|
||||
|
||||
|
||||
|
||||
def _get_task_logger():
|
||||
try:
|
||||
@@ -65,7 +68,7 @@ async def _run_template_demo(target: Optional[str], input_id: Optional[str]) ->
|
||||
def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int:
|
||||
"""通用任务执行入口模板。复制后请实现真实业务逻辑。"""
|
||||
log = _get_task_logger()
|
||||
task_type = "demo"
|
||||
task_type = "cmd_run"
|
||||
|
||||
with job_context(skill=SKILL_SLUG):
|
||||
log.info(
|
||||
@@ -146,19 +149,16 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
return 1
|
||||
|
||||
|
||||
|
||||
# ---------------- 标准命令(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,
|
||||
)
|
||||
)
|
||||
print(json.dumps(
|
||||
{"skill": SKILL_SLUG, "env_path": env_path, "example_path": os.path.abspath(example_path)},
|
||||
ensure_ascii=False,
|
||||
))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -179,6 +179,7 @@ def cmd_health() -> int:
|
||||
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)
|
||||
@@ -191,25 +192,118 @@ def cmd_version() -> int:
|
||||
|
||||
|
||||
def cmd_init_db() -> int:
|
||||
"""幂等创建/迁移本地 SQLite 与数据管理元数据(宿主 install/upgrade 可调用)。"""
|
||||
"""幂等创建本地 SQLite 与数据管理元数据。"""
|
||||
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(),
|
||||
},
|
||||
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)
|
||||
_get_task_logger().info(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
|
||||
_get_task_logger().info(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)})
|
||||
_get_task_logger().info(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})
|
||||
_get_task_logger().info(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
|
||||
|
||||
|
||||
|
||||
167
scripts/service/todo_service.py
Normal file
167
scripts/service/todo_service.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""待办事项领域服务(业务内核)。
|
||||
|
||||
按模板铁律:「单业务内核、多入口」——CLI / Action / 数据管理按钮 / 定时任务
|
||||
都调用这里的同一个函数,禁止按入口分叉业务行为。
|
||||
|
||||
直接用 sqlite3 做持久化(demo 简化,未抽 repository)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from db.connection import get_conn
|
||||
|
||||
# 业务常量(不暴露给宿主,技能自定)
|
||||
STATUS_PENDING = "pending"
|
||||
STATUS_DONE = "done"
|
||||
VALID_STATUS = {STATUS_PENDING, STATUS_DONE}
|
||||
|
||||
PRIORITY_HIGH = "high"
|
||||
PRIORITY_NORMAL = "normal"
|
||||
PRIORITY_LOW = "low"
|
||||
VALID_PRIORITY = {PRIORITY_HIGH, PRIORITY_NORMAL, PRIORITY_LOW}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Todo:
|
||||
id: int
|
||||
title: str
|
||||
status: str
|
||||
priority: str
|
||||
due_at: Optional[int]
|
||||
created_at: int
|
||||
updated_at: int
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"status": self.status,
|
||||
"priority": self.priority,
|
||||
"due_at": self.due_at,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def _now() -> int:
|
||||
return int(time.time())
|
||||
|
||||
|
||||
def add_todo(
|
||||
*,
|
||||
title: str,
|
||||
priority: str = PRIORITY_NORMAL,
|
||||
due_at: Optional[int] = None,
|
||||
) -> Todo:
|
||||
"""新增一条待办。领域校验:标题非空、优先级合法。"""
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
raise ValueError("标题不能为空")
|
||||
if priority not in VALID_PRIORITY:
|
||||
raise ValueError(f"优先级不合法,可选:{sorted(VALID_PRIORITY)}")
|
||||
now = _now()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"INSERT INTO todos (title, status, priority, due_at, created_at, updated_at) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(title, STATUS_PENDING, priority, due_at, now, now),
|
||||
)
|
||||
new_id = cur.lastrowid
|
||||
conn.commit()
|
||||
return Todo(new_id, title, STATUS_PENDING, priority, due_at, now, now)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def list_todos(*, status: Optional[str] = None, limit: int = 100) -> list[Todo]:
|
||||
"""列出待办;可按状态过滤。默认按创建时间倒序。"""
|
||||
sql = "SELECT id, title, status, priority, due_at, created_at, updated_at FROM todos"
|
||||
params: list = []
|
||||
if status:
|
||||
if status not in VALID_STATUS:
|
||||
raise ValueError(f"状态不合法,可选:{sorted(VALID_STATUS)}")
|
||||
sql += " WHERE status = ?"
|
||||
params.append(status)
|
||||
sql += " ORDER BY created_at DESC LIMIT ?"
|
||||
params.append(limit)
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, params)
|
||||
rows = cur.fetchall()
|
||||
return [Todo(*row) for row in rows]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_todo(todo_id: int) -> Optional[Todo]:
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, title, status, priority, due_at, created_at, updated_at "
|
||||
"FROM todos WHERE id = ?",
|
||||
(todo_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return Todo(*row) if row else None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def mark_done(todo_id: int) -> Todo:
|
||||
"""标记一条待办为完成。"""
|
||||
todo = get_todo(todo_id)
|
||||
if not todo:
|
||||
raise ValueError(f"待办 {todo_id} 不存在")
|
||||
if todo.status == STATUS_DONE:
|
||||
raise ValueError(f"待办 {todo_id} 已是完成状态")
|
||||
now = _now()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"UPDATE todos SET status = ?, updated_at = ? WHERE id = ?",
|
||||
(STATUS_DONE, now, todo_id),
|
||||
)
|
||||
conn.commit()
|
||||
todo.status = STATUS_DONE
|
||||
todo.updated_at = now
|
||||
return todo
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_todo(todo_id: int) -> bool:
|
||||
"""删除一条待办;返回是否真的删了一行。"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM todos WHERE id = ?", (todo_id,))
|
||||
conn.commit()
|
||||
return cur.rowcount > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def stats() -> dict:
|
||||
"""简单统计:用于 ``todo stats`` 命令与 demo 报告。"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT status, COUNT(*) FROM todos GROUP BY status")
|
||||
by_status = {row[0]: int(row[1]) for row in cur.fetchall()}
|
||||
cur.execute("SELECT COUNT(*) FROM todos")
|
||||
total = int(cur.fetchone()[0])
|
||||
return {
|
||||
"total": total,
|
||||
"pending": by_status.get(STATUS_PENDING, 0),
|
||||
"done": by_status.get(STATUS_DONE, 0),
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user