release: v1.0.117 沉淀多数据源契约(SQLite 默认 + 可选 MySQL 云库)
All checks were successful
技能自动化发布 / release (push) Successful in 9s
All checks were successful
技能自动化发布 / release (push) Successful in 9s
新增 DATA_SOURCES.md、data-sources 示例与双后端 connection 骨架;CLI 与宿主 UI 可读写同一库。要求匠厂 Host ≥ 6.9.28。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
35
scripts/db/backend.py
Normal file
35
scripts/db/backend.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""数据库后端选择:配置了 MYSQL_DSN 时走 MySQL,否则回退本地 SQLite。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from jiangchang_skill_core import config
|
||||
|
||||
DbBackend = Literal["sqlite", "mysql"]
|
||||
|
||||
|
||||
def get_mysql_dsn() -> str:
|
||||
return (config.get("MYSQL_DSN") or "").strip()
|
||||
|
||||
|
||||
def resolve_backend() -> DbBackend:
|
||||
return "mysql" if get_mysql_dsn() else "sqlite"
|
||||
|
||||
|
||||
def parse_mysql_dsn(dsn: str) -> dict[str, object]:
|
||||
text = dsn.strip()
|
||||
if not text.startswith(("mysql://", "mysql2://")):
|
||||
raise ValueError("MYSQL_DSN must be a mysql:// URL.")
|
||||
parsed = urlparse(text)
|
||||
database = (parsed.path or "").lstrip("/")
|
||||
if not database:
|
||||
raise ValueError("MYSQL_DSN must include a database name.")
|
||||
return {
|
||||
"host": parsed.hostname or "127.0.0.1",
|
||||
"port": parsed.port or 3306,
|
||||
"user": unquote(parsed.username or ""),
|
||||
"password": unquote(parsed.password or ""),
|
||||
"database": database,
|
||||
}
|
||||
@@ -1,18 +1,38 @@
|
||||
"""SQLite 连接与幂等初始化模板。
|
||||
"""SQLite / MySQL 连接与幂等初始化模板。
|
||||
|
||||
模板默认只建数据管理元数据表。业务表由复制后的技能自行 CREATE,并同步展示元数据。
|
||||
配置了 MYSQL_DSN 时走 MySQL 云库,否则使用本地 {skill_slug}.db。见 references/DATA_SOURCES.md。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from db.backend import get_mysql_dsn, parse_mysql_dsn, resolve_backend
|
||||
from db.cursor_compat import AdaptedConnection
|
||||
from db.display_metadata import init_display_metadata
|
||||
from util.runtime_paths import get_db_path
|
||||
|
||||
|
||||
def get_conn() -> sqlite3.Connection:
|
||||
return sqlite3.connect(get_db_path())
|
||||
def get_conn() -> AdaptedConnection:
|
||||
backend = resolve_backend()
|
||||
if backend == "mysql":
|
||||
import pymysql
|
||||
|
||||
params = parse_mysql_dsn(get_mysql_dsn())
|
||||
raw = pymysql.connect(
|
||||
host=str(params["host"]),
|
||||
port=int(params["port"]),
|
||||
user=str(params["user"]),
|
||||
password=str(params["password"]),
|
||||
database=str(params["database"]),
|
||||
charset="utf8mb4",
|
||||
autocommit=False,
|
||||
)
|
||||
return AdaptedConnection(raw, "mysql")
|
||||
|
||||
raw = sqlite3.connect(get_db_path())
|
||||
return AdaptedConnection(raw, "sqlite")
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
@@ -23,3 +43,18 @@ def init_db() -> None:
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def describe_active_store() -> dict[str, str]:
|
||||
backend = resolve_backend()
|
||||
if backend == "mysql":
|
||||
params = parse_mysql_dsn(get_mysql_dsn())
|
||||
return {
|
||||
"backend": "mysql",
|
||||
"host": str(params["host"]),
|
||||
"database": str(params["database"]),
|
||||
}
|
||||
return {
|
||||
"backend": "sqlite",
|
||||
"db_path": get_db_path(),
|
||||
}
|
||||
|
||||
96
scripts/db/cursor_compat.py
Normal file
96
scripts/db/cursor_compat.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""统一 SQLite / MySQL 游标:占位符 ? 与 executescript。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from db.backend import DbBackend
|
||||
|
||||
_PLACEHOLDER_RE = re.compile(r"\?")
|
||||
|
||||
|
||||
def adapt_sql(sql: str, backend: DbBackend) -> str:
|
||||
if backend != "mysql":
|
||||
return sql
|
||||
return _PLACEHOLDER_RE.sub("%s", sql)
|
||||
|
||||
|
||||
def _split_sql_script(script: str) -> list[str]:
|
||||
parts: list[str] = []
|
||||
for chunk in script.split(";"):
|
||||
stmt = chunk.strip()
|
||||
if stmt:
|
||||
parts.append(stmt)
|
||||
return parts
|
||||
|
||||
|
||||
class AdaptedCursor:
|
||||
backend: DbBackend
|
||||
|
||||
def __init__(self, cursor: Any, backend: DbBackend) -> None:
|
||||
self._cursor = cursor
|
||||
self.backend = backend
|
||||
|
||||
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> Any:
|
||||
adapted = adapt_sql(sql, self.backend)
|
||||
if params is None:
|
||||
return self._cursor.execute(adapted)
|
||||
return self._cursor.execute(adapted, params)
|
||||
|
||||
def executemany(self, sql: str, seq_of_params: list[tuple[Any, ...]]) -> Any:
|
||||
return self._cursor.executemany(adapt_sql(sql, self.backend), seq_of_params)
|
||||
|
||||
def fetchone(self) -> Any:
|
||||
return self._cursor.fetchone()
|
||||
|
||||
def fetchall(self) -> list[Any]:
|
||||
return self._cursor.fetchall()
|
||||
|
||||
@property
|
||||
def lastrowid(self) -> int:
|
||||
return int(self._cursor.lastrowid)
|
||||
|
||||
@property
|
||||
def rowcount(self) -> int:
|
||||
return int(self._cursor.rowcount)
|
||||
|
||||
def executescript(self, script: str) -> None:
|
||||
if self.backend == "sqlite":
|
||||
self._cursor.connection.executescript(script)
|
||||
return
|
||||
for stmt in _split_sql_script(script):
|
||||
self.execute(stmt)
|
||||
|
||||
|
||||
class AdaptedConnection:
|
||||
backend: DbBackend
|
||||
|
||||
def __init__(self, conn: Any, backend: DbBackend) -> None:
|
||||
self._conn = conn
|
||||
self.backend = backend
|
||||
|
||||
def cursor(self) -> AdaptedCursor:
|
||||
return AdaptedCursor(self._conn.cursor(), self.backend)
|
||||
|
||||
def commit(self) -> None:
|
||||
self._conn.commit()
|
||||
|
||||
def close(self) -> None:
|
||||
self._conn.close()
|
||||
|
||||
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> Any:
|
||||
cur = self.cursor()
|
||||
return cur.execute(sql, params)
|
||||
|
||||
def executemany(self, sql: str, seq_of_params: list[tuple[Any, ...]]) -> Any:
|
||||
cur = self.cursor()
|
||||
return cur.executemany(sql, seq_of_params)
|
||||
|
||||
def executescript(self, script: str) -> None:
|
||||
if self.backend == "sqlite":
|
||||
self._conn.executescript(script)
|
||||
return
|
||||
cur = self.cursor()
|
||||
for stmt in _split_sql_script(script):
|
||||
cur.execute(stmt)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.116"
|
||||
SKILL_VERSION = "1.0.117"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
Reference in New Issue
Block a user