This commit is contained in:
2026-09-09 10:29:02 +08:00
parent 2aa49b5263
commit ccb12dad7c
102 changed files with 7879 additions and 559 deletions

View File

View 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

View 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()