test
This commit is contained in:
0
scripts/cli/__init__.py
Normal file
0
scripts/cli/__init__.py
Normal file
@@ -1,8 +1,4 @@
|
||||
"""CLI:argparse 与分发模板。
|
||||
|
||||
只负责参数解析并转交 service;业务内核在 domain / task_service,勿在此按入口复制逻辑。
|
||||
"""
|
||||
|
||||
"""CLI:argparse 解析与分发(业务内核在 service,本层只做参数→service 映射)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -10,11 +6,17 @@ import sys
|
||||
from typing import List, Optional
|
||||
|
||||
from service.task_service import (
|
||||
cmd_add,
|
||||
cmd_config_path,
|
||||
cmd_delete,
|
||||
cmd_done,
|
||||
cmd_health,
|
||||
cmd_init_db,
|
||||
cmd_run,
|
||||
cmd_list,
|
||||
cmd_remind,
|
||||
cmd_stats,
|
||||
cmd_version,
|
||||
cmd_run,
|
||||
)
|
||||
from util.config_bootstrap import bootstrap_skill_config
|
||||
from util.argparse_zh import ZhArgumentParser
|
||||
@@ -29,64 +31,89 @@ def _cli_str_or_none(raw: Optional[str]) -> Optional[str]:
|
||||
return v or None
|
||||
|
||||
|
||||
def _handle_run(args: argparse.Namespace) -> int:
|
||||
tail = [str(x).strip() for x in (args.run_tail or []) if str(x).strip()]
|
||||
if len(tail) > 2:
|
||||
print("❌ 参数过多。")
|
||||
print("用法:python main.py run [target [input_id]] | run [-t target] [-i input_id]")
|
||||
return 1
|
||||
|
||||
t_target: Optional[str] = None
|
||||
t_input: Optional[str] = None
|
||||
if len(tail) == 2:
|
||||
t_target, t_input = tail[0], tail[1]
|
||||
elif len(tail) == 1:
|
||||
if tail[0].isdigit():
|
||||
t_input = tail[0]
|
||||
else:
|
||||
t_target = tail[0]
|
||||
|
||||
pick_t = _cli_str_or_none(getattr(args, "target", None))
|
||||
pick_i = _cli_str_or_none(getattr(args, "input_id", None))
|
||||
target = pick_t or t_target
|
||||
input_id = pick_i or t_input
|
||||
return cmd_run(target=target, input_id=input_id)
|
||||
def _parse_ids(raw: str) -> list[int]:
|
||||
"""解析 ``1`` 或 ``1,2,3`` 为 int 列表。"""
|
||||
parts = [p.strip() for p in (raw or "").split(",") if p.strip()]
|
||||
try:
|
||||
return [int(p) for p in parts]
|
||||
except ValueError as exc:
|
||||
raise SystemExit(f"参数错误:ids 必须是数字或逗号分隔的数字列表,收到 {raw!r}\n{exc}")
|
||||
|
||||
|
||||
def _print_full_usage() -> None:
|
||||
print("通用业务技能模板(main.py)可用命令:")
|
||||
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
||||
print(" python main.py health")
|
||||
print(" python main.py config-path")
|
||||
print(" python main.py version")
|
||||
print(" python main.py init-db")
|
||||
print("待办事项 add-todo-all(main.py)可用命令:")
|
||||
print(" python main.py init-db # 初始化数据库与元数据")
|
||||
print(" python main.py run add <title> [-p high|normal|low] [-d <unix秒>")
|
||||
print(" python main.py run list [-s pending|done] [-n <limit>]")
|
||||
print(" python main.py run done <id>")
|
||||
print(" python main.py run delete <id>")
|
||||
print(" python main.py run stats")
|
||||
print(" python main.py remind [-s <秒>]")
|
||||
print(" python main.py health / version / config-path")
|
||||
|
||||
|
||||
def _add_run_subparser(sub) -> None:
|
||||
sp = sub.add_parser("run", help="待办事项操作")
|
||||
todo_sub = sp.add_subparsers(dest="todo_cmd", required=True, parser_class=ZhArgumentParser)
|
||||
|
||||
p = todo_sub.add_parser("add", help="新增一条待办")
|
||||
p.add_argument("title", help="待办标题(必填)")
|
||||
p.add_argument("-p", "--priority", default="normal",
|
||||
choices=["high", "normal", "low"], help="优先级,默认 normal")
|
||||
p.add_argument("-d", "--due-at", type=int, default=None,
|
||||
help="截止时间(Unix 秒,可选)")
|
||||
p.set_defaults(handler=lambda a: cmd_add(
|
||||
title=a.title, priority=a.priority, due_at=a.due_at))
|
||||
|
||||
p = todo_sub.add_parser("list", help="列出待办")
|
||||
p.add_argument("-s", "--status", default=None,
|
||||
choices=["pending", "done"], help="按状态过滤")
|
||||
p.add_argument("-n", "--limit", type=int, default=100, help="返回数量上限")
|
||||
p.set_defaults(handler=lambda a: cmd_list(status=a.status, limit=a.limit))
|
||||
|
||||
p = todo_sub.add_parser("done", help="标记完成(可传逗号分隔的多个 id)")
|
||||
p.add_argument("ids", help="待办 id 或逗号分隔的 id 列表(如 1 或 1,2,3)")
|
||||
p.set_defaults(handler=lambda a: cmd_done(todo_ids=_parse_ids(a.ids)))
|
||||
|
||||
p = todo_sub.add_parser("delete", help="删除(可传逗号分隔的多个 id)")
|
||||
p.add_argument("ids", help="待办 id 或逗号分隔的 id 列表")
|
||||
p.set_defaults(handler=lambda a: cmd_delete(todo_ids=_parse_ids(a.ids)))
|
||||
|
||||
p = todo_sub.add_parser("stats", help="简单统计")
|
||||
p.set_defaults(handler=lambda _a: cmd_stats())
|
||||
|
||||
|
||||
|
||||
def _add_remind_subparser(sub) -> None:
|
||||
sp = sub.add_parser("remind", help="演示 async 提醒任务(job_context/emit/finish)")
|
||||
sp.add_argument("-s", "--seconds", type=int, default=3,
|
||||
help="模拟运行秒数,默认 3")
|
||||
sp.set_defaults(handler=lambda a: cmd_remind(seconds=a.seconds))
|
||||
|
||||
|
||||
def build_parser() -> ZhArgumentParser:
|
||||
p = ZhArgumentParser(
|
||||
prog="main.py",
|
||||
description="通用业务技能模板:任务执行命令骨架、健康检查、版本输出。",
|
||||
description="待办事项",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True, parser_class=ZhArgumentParser)
|
||||
|
||||
sp = sub.add_parser("run", help="任务执行命令骨架")
|
||||
sp.add_argument("--target", "-t", default=None, metavar="目标")
|
||||
sp.add_argument("--input-id", "-i", default=None, metavar="输入id", dest="input_id")
|
||||
sp.add_argument("run_tail", nargs="*", metavar="位置参数")
|
||||
sp.set_defaults(handler=_handle_run)
|
||||
sp = sub.add_parser("init-db", help="幂等初始化 SQLite 与数据管理元数据")
|
||||
sp.set_defaults(handler=lambda _a: cmd_init_db())
|
||||
|
||||
_add_run_subparser(sub)
|
||||
_add_remind_subparser(sub)
|
||||
|
||||
sp = sub.add_parser("health", help="健康检查")
|
||||
sp.set_defaults(handler=lambda _a: cmd_health())
|
||||
|
||||
sp = sub.add_parser("config-path", help="输出用户 .env 与模板路径")
|
||||
sp.set_defaults(handler=lambda _a: cmd_config_path())
|
||||
|
||||
sp = sub.add_parser("version", help="版本信息(JSON)")
|
||||
sp.set_defaults(handler=lambda _a: cmd_version())
|
||||
|
||||
sp = sub.add_parser("init-db", help="幂等初始化本地数据库与数据管理元数据")
|
||||
sp.set_defaults(handler=lambda _a: cmd_init_db())
|
||||
sp = sub.add_parser("config-path", help="输出 .env 与模板路径")
|
||||
sp.set_defaults(handler=lambda _a: cmd_config_path())
|
||||
|
||||
return p
|
||||
|
||||
|
||||
@@ -98,11 +125,14 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
if not argv:
|
||||
_print_full_usage()
|
||||
return 1
|
||||
|
||||
if len(argv) == 2 and argv[0] not in {
|
||||
"run", "health", "config-path", "version", "init-db",
|
||||
"-h", "--help",
|
||||
}:
|
||||
return cmd_run(target=argv[0], input_id=argv[1])
|
||||
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return int(args.handler(args))
|
||||
|
||||
|
||||
0
scripts/db/__init__.py
Normal file
0
scripts/db/__init__.py
Normal file
@@ -1,25 +1,96 @@
|
||||
"""SQLite 连接与幂等初始化模板。
|
||||
"""SQLite 连接与幂等初始化。
|
||||
|
||||
模板默认只建数据管理元数据表。业务表由复制后的技能自行 CREATE,并同步展示元数据。
|
||||
``init_db`` 建两张表:
|
||||
1. ``todos`` — 业务表(demo:待办事项)
|
||||
2. ``_jiangchang_tables`` / ``_jiangchang_columns`` — 宿主数据管理展示元数据
|
||||
|
||||
业务表字段顺序就是宿主列展示顺序(cid 顺序)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from db.display_metadata import init_display_metadata
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
seed_column_metadata_batch,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
from util.runtime_paths import get_db_path
|
||||
|
||||
# ---- 业务表 DDL(待办事项)----
|
||||
# 字段顺序 = 数据管理列展示顺序;状态/优先级用 options_json 给宿主做下拉筛选
|
||||
_TODOS_DDL = """
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
priority TEXT NOT NULL DEFAULT 'normal',
|
||||
due_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
)
|
||||
"""
|
||||
|
||||
_TODOS_INDEX = """
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status)
|
||||
"""
|
||||
|
||||
|
||||
def get_conn() -> sqlite3.Connection:
|
||||
return sqlite3.connect(get_db_path())
|
||||
|
||||
|
||||
def _seed_todos_display_metadata(cursor) -> None:
|
||||
"""写 ``todos`` 表与字段的中文展示元数据。"""
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="todos",
|
||||
display_name="待办事项",
|
||||
description="本技能的核心业务表:每条待办事项一行。",
|
||||
grid_help="状态变更走技能 CLI(todo done / todo delete),不要在表里直接改。",
|
||||
visible=1,
|
||||
readonly=1, # 整表只读:增删改通过技能 Action 走,宿主不直写
|
||||
default_sort_json='[{"column":"created_at","direction":"desc"}]',
|
||||
default_visible_columns_json='["title","status","priority","due_at","created_at"]',
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cursor,
|
||||
table_name="todos",
|
||||
columns=[
|
||||
{"column_name": "id", "display_name": "编号", "description": "主键",
|
||||
"visible": 1, "searchable": 1, "editable": 0, "display_type": "number"},
|
||||
{"column_name": "title", "display_name": "标题", "description": "待办事项内容",
|
||||
"grid_help": "简明描述要做的事", "visible": 1, "searchable": 1, "editable": 0,
|
||||
"display_type": "text"},
|
||||
{"column_name": "status", "display_name": "状态", "description": "pending/done",
|
||||
"grid_help": "pending=待办,done=完成", "visible": 1, "searchable": 1, "editable": 0,
|
||||
"display_type": "enum",
|
||||
"options_json": '[{"value":"pending","label":"待办"},{"value":"done","label":"完成"}]'},
|
||||
{"column_name": "priority", "display_name": "优先级", "description": "high/normal/low",
|
||||
"visible": 1, "searchable": 1, "editable": 0, "display_type": "enum",
|
||||
"options_json": '[{"value":"high","label":"高"},{"value":"normal","label":"中"},{"value":"low","label":"低"}]'},
|
||||
{"column_name": "due_at", "display_name": "截止时间", "description": "Unix 秒;可空",
|
||||
"visible": 1, "searchable": 1, "editable": 0, "display_type": "datetime_unix_seconds"},
|
||||
{"column_name": "created_at", "display_name": "创建时间", "description": "Unix 秒",
|
||||
"visible": 1, "searchable": 0, "editable": 0, "display_type": "datetime_unix_seconds"},
|
||||
{"column_name": "updated_at", "display_name": "更新时间", "description": "Unix 秒",
|
||||
"visible": 0, "searchable": 0, "editable": 0, "display_type": "datetime_unix_seconds"},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
"""幂等创建/迁移本地 SQLite 与数据管理元数据。"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
init_display_metadata(cur)
|
||||
# 1. 元数据表(先建,方便其它步骤 upsert)
|
||||
init_display_metadata_tables(cur)
|
||||
# 2. 业务表
|
||||
cur.execute(_TODOS_DDL)
|
||||
cur.execute(_TODOS_INDEX)
|
||||
# 3. 业务表展示元数据
|
||||
_seed_todos_display_metadata(cur)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
@@ -21,7 +21,7 @@ from db.timestamp_columns import has_updated_at_trigger
|
||||
from util.constants import SKILL_SLUG
|
||||
from util.slug_naming import LEGACY_EXEMPT_SLUGS, validate_slug_semantics
|
||||
|
||||
_TEMPLATE_PLACEHOLDER_SLUGS = frozenset({"add-todo-all", "add_todo_all"})
|
||||
_TEMPLATE_PLACEHOLDER_SLUGS = frozenset({"your-skill-slug", "your_skill_slug"})
|
||||
_ENGLISH_PLACEHOLDER_NAME_PATTERNS = (
|
||||
re.compile(r"^my\s+skill$", re.I),
|
||||
re.compile(r"^your[-_\s]?skill", re.I),
|
||||
|
||||
@@ -1,38 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
skill-template CLI 入口。
|
||||
"""待办事项 CLI 入口。
|
||||
|
||||
复制为新技能后,请修改注释与常量,但保留当前分层结构:
|
||||
cli(argv)→ service(业务编排)→ db(持久化)→ util(通用工具)。
|
||||
分层:cli(argv)→ service(业务编排)→ db(持久化)→ util(通用工具)。
|
||||
|
||||
jiangchang_skill_core 来自宿主共享 Python runtime 安装的 jiangchang-platform-kit,
|
||||
不在技能仓库内 vendored。
|
||||
独立运行说明:
|
||||
真实技能依赖宿主共享 Python runtime 的 ``jiangchang_skill_core``。
|
||||
本 demo 自带一份最小 mock(``scripts/_mock_platform/``),在没有宿主时
|
||||
由本文件自动注入 sys.path,使 demo 在任意 Python 3.10+ 环境可直接跑通。
|
||||
设置环境变量 ``JIANGCHANG_DATA_ROOT`` 即可切到真实平台 runtime 模式。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from jiangchang_skill_core.runtime_env import apply_cli_local_defaults
|
||||
import logging
|
||||
from util.logging_config import setup_skill_logging, get_skill_logger
|
||||
|
||||
if sys.platform == "win32":
|
||||
import io
|
||||
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
_scripts_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if _scripts_dir not in sys.path:
|
||||
sys.path.insert(0, _scripts_dir)
|
||||
|
||||
from jiangchang_skill_core.runtime_env import apply_cli_local_defaults
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
apply_cli_local_defaults()
|
||||
|
||||
from util.config_bootstrap import bootstrap_skill_config
|
||||
from util.config_bootstrap import bootstrap_skill_config # noqa: E402
|
||||
|
||||
bootstrap_skill_config()
|
||||
setup_skill_logging("add-todo-all", __name__)
|
||||
|
||||
from cli.app import main
|
||||
|
||||
|
||||
|
||||
from cli.app import main # noqa: E402
|
||||
|
||||
if __name__ == "__main__":
|
||||
get_skill_logger().info("Hello, world!")
|
||||
raise SystemExit(main())
|
||||
|
||||
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()
|
||||
0
scripts/util/__init__.py
Normal file
0
scripts/util/__init__.py
Normal file
@@ -1,5 +1,4 @@
|
||||
"""argparse 中文错误说明。"""
|
||||
|
||||
"""argparse 中文错误说明(与模板一致)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""技能配置初始化:.env.example 落盘与用户 .env 缺失项合并。"""
|
||||
"""配置初始化:从 .env.example 复制到用户 .env,并合并缺失键。
|
||||
|
||||
行为对齐模板:``main.py`` 与 ``cli.app.main()`` 启动时均会调用一次。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
@@ -11,8 +13,11 @@ from util.runtime_paths import get_skill_root
|
||||
|
||||
|
||||
def bootstrap_skill_config() -> str:
|
||||
"""确保用户数据目录 .env 存在,并追加 .env.example 中的新配置项。"""
|
||||
"""确保用户 .env 存在,并追加 .env.example 中的新配置项。"""
|
||||
example_path = os.path.join(get_skill_root(), ".env.example")
|
||||
if not os.path.isfile(example_path):
|
||||
# demo 没有强制 .env.example,直接返回空路径
|
||||
return ""
|
||||
env_path = config.ensure_env_file(SKILL_SLUG, example_path)
|
||||
comment = f"{SKILL_SLUG} v{SKILL_VERSION}" if SKILL_VERSION else None
|
||||
config.merge_missing_env_keys(example_path, env_path, comment_skill=comment)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
"""技能标识与版本(待办事项)。
|
||||
|
||||
真实技能需保证 slug = 目录名 = DB 文件名 = ``metadata.openclaw.slug``。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# 英文 kebab-case 机器标识;与目录名、actions.json 的 ``skill`` 字段一致
|
||||
SKILL_SLUG = "add-todo-all"
|
||||
SKILL_VERSION = "1.0.98"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.add_todo_all"
|
||||
SKILL_VERSION = "0.1.0"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.add-todo-all"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
"""Re-export unified logging (implementation: jiangchang_skill_core.unified_logging)."""
|
||||
"""日志:直接用 stdlib logging(真实模板用 jiangchang_skill_core.unified_logging)。
|
||||
|
||||
from jiangchang_skill_core.unified_logging import (
|
||||
attach_unified_file_handler,
|
||||
ensure_trace_for_process,
|
||||
get_skill_log_file_path,
|
||||
get_skill_logger,
|
||||
get_unified_logs_dir,
|
||||
setup_skill_logging,
|
||||
subprocess_env_with_trace,
|
||||
)
|
||||
为保持与模板 ``from util.logging_config import get_skill_logger, setup_skill_logging``
|
||||
一致的调用面,本 demo 提供同名薄封装。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"attach_unified_file_handler",
|
||||
"ensure_trace_for_process",
|
||||
"get_skill_log_file_path",
|
||||
"get_skill_logger",
|
||||
"get_unified_logs_dir",
|
||||
"setup_skill_logging",
|
||||
"subprocess_env_with_trace",
|
||||
]
|
||||
import logging
|
||||
|
||||
import jiangchang_skill_core.unified_logging as unified_logging
|
||||
|
||||
from util.constants import LOG_LOGGER_NAME
|
||||
|
||||
_configured = False
|
||||
|
||||
|
||||
def setup_skill_logging(skill_slug: str, logger_name: str) -> None:
|
||||
# global _configured
|
||||
# if _configured:
|
||||
# return
|
||||
# handler = logging.StreamHandler()
|
||||
# handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
||||
# logger = logging.getLogger(logger_name)
|
||||
# logger.setLevel(logging.INFO)
|
||||
# if not logger.handlers:
|
||||
# logger.addHandler(handler)
|
||||
# _configured = True
|
||||
unified_logging.setup_skill_logging(skill_slug, logger_name)
|
||||
|
||||
|
||||
def get_skill_logger() -> logging.Logger:
|
||||
#return logging.getLogger(LOG_LOGGER_NAME)
|
||||
return unified_logging.get_skill_logger()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""数据根、技能目录、兄弟技能根路径与用户数据子目录解析。"""
|
||||
"""技能数据目录与 DB 路径解析(简化版)。
|
||||
|
||||
真实模板用 ``jiangchang_skill_core.runtime_env``;本 demo 复用其 mock。
|
||||
数据目录 = ``{data_root}/{user_id}/{slug}/``,DB 即 ``{slug}.db``。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from jiangchang_skill_core.runtime_env import get_data_root, get_sibling_skills_root, get_user_id
|
||||
from jiangchang_skill_core.runtime_env import get_data_root, get_user_id
|
||||
|
||||
from util.constants import SKILL_SLUG
|
||||
|
||||
@@ -32,28 +35,22 @@ CONFIG_KEY_TEMP_DIR = "SKILL_TEMP_DIR"
|
||||
|
||||
|
||||
def get_skill_root() -> str:
|
||||
"""技能根目录(含 SKILL.md / scripts/ 的目录)。"""
|
||||
return os.path.dirname(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_openclaw_root() -> str:
|
||||
return get_sibling_skills_root(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_skills_root() -> str:
|
||||
return get_sibling_skills_root(_SCRIPTS_DIR)
|
||||
|
||||
|
||||
def get_skill_data_dir() -> str:
|
||||
"""本技能数据目录:``{data_root}/{user_id}/{slug}/``。"""
|
||||
path = os.path.join(get_data_root(), get_user_id(), SKILL_SLUG)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_db_path(filename: str | None = None) -> str:
|
||||
"""SQLite 文件路径,默认 ``{slug}.db``。"""
|
||||
name = filename or f"{SKILL_SLUG}.db"
|
||||
return os.path.join(get_skill_data_dir(), name)
|
||||
|
||||
|
||||
def _normalize_subdir(value: str) -> str:
|
||||
cleaned = value.strip().strip("/\\")
|
||||
if not cleaned or cleaned.startswith(".."):
|
||||
@@ -153,3 +150,4 @@ def list_resolved_data_paths() -> dict[str, str]:
|
||||
"temp_dir": get_temp_dir(create=False),
|
||||
"videos_dir": get_videos_dir(create=False),
|
||||
}
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ LEGACY_EXEMPT_SLUGS: frozenset[str] = frozenset(
|
||||
{
|
||||
"account-manager",
|
||||
"skill-template",
|
||||
"add-todo-all",
|
||||
"add_todo_all",
|
||||
"your-skill-slug",
|
||||
"your_skill_slug",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user