Add OpenClaw skills, platform kit, and template docs

Made-with: Cursor
This commit is contained in:
2026-04-04 10:35:02 +08:00
parent e37b03c00f
commit 35f4758da2
83 changed files with 8971 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from content_manager.db.connection import get_conn, init_db
__all__ = ["get_conn", "init_db"]

View File

@@ -0,0 +1,122 @@
"""articles 表:仅负责 SQL 读写,不含业务规则。"""
from __future__ import annotations
import sqlite3
from typing import Any, List, Optional, Tuple
def insert_article(
conn: sqlite3.Connection,
title: str,
body: str,
content_html: Optional[str],
status: str,
source: str,
account_id: Optional[str],
error_msg: Optional[str],
llm_target: Optional[str],
extra_json: Optional[str],
created_at: int,
updated_at: int,
) -> int:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO articles (
title, body, content_html, status, source, account_id, error_msg, llm_target, extra_json,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
title,
body,
content_html,
status,
source,
account_id,
error_msg,
llm_target,
extra_json,
created_at,
updated_at,
),
)
return int(cur.lastrowid)
def update_article_body(
conn: sqlite3.Connection,
article_id: int,
title: str,
body: str,
updated_at: int,
) -> None:
cur = conn.cursor()
cur.execute(
"""
UPDATE articles SET title = ?, body = ?, updated_at = ?
WHERE id = ?
""",
(title, body, updated_at, article_id),
)
def fetch_by_id(conn: sqlite3.Connection, article_id: int) -> Optional[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, title, body, content_html, status, source, account_id, error_msg,
llm_target, extra_json, created_at, updated_at
FROM articles WHERE id = ?
""",
(article_id,),
)
return cur.fetchone()
def exists_id(conn: sqlite3.Connection, article_id: int) -> bool:
cur = conn.cursor()
cur.execute("SELECT id FROM articles WHERE id = ?", (article_id,))
return cur.fetchone() is not None
def list_recent(conn: sqlite3.Connection, limit: int) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT
id, title, body, content_html,
status, source, account_id, error_msg, llm_target, extra_json,
created_at, updated_at
FROM articles ORDER BY updated_at DESC, id DESC
LIMIT ?
""",
(int(limit),),
)
return list(cur.fetchall())
def delete_by_id(conn: sqlite3.Connection, article_id: int) -> int:
cur = conn.cursor()
cur.execute("DELETE FROM articles WHERE id = ?", (article_id,))
return int(cur.rowcount)
def update_feedback(
conn: sqlite3.Connection,
article_id: int,
status: str,
account_id: Optional[str],
error_msg: Optional[str],
updated_at: int,
) -> None:
cur = conn.cursor()
cur.execute(
"""
UPDATE articles
SET status = ?, account_id = ?, error_msg = ?, updated_at = ?
WHERE id = ?
""",
(status, account_id, error_msg, updated_at, article_id),
)

View File

@@ -0,0 +1,113 @@
"""数据库连接与初始化(建表、文章旧库迁移、提示词种子)。"""
from __future__ import annotations
import sqlite3
from typing import TYPE_CHECKING
from content_manager.config import get_db_path
from content_manager.db.schema import (
ARTICLES_TABLE_SQL,
IMAGES_TABLE_SQL,
PROMPT_TEMPLATE_USAGE_TABLE_SQL,
PROMPT_TEMPLATES_TABLE_SQL,
VIDEOS_TABLE_SQL,
)
from content_manager.util.timeutil import now_unix, parse_ts_to_unix
if TYPE_CHECKING:
pass
def get_conn() -> sqlite3.Connection:
return sqlite3.connect(get_db_path())
def _is_legacy_articles_table(cur: sqlite3.Cursor) -> bool:
cur.execute("PRAGMA table_info(articles)")
rows = cur.fetchall()
if not rows:
return False
for _cid, name, ctype, _nn, _d, _pk in rows:
if name == "id" and ctype and ctype.upper() == "INTEGER":
return False
cur.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='articles'")
row = cur.fetchone()
if row and row[0] and "TEXT" in row[0] and "id" in row[0]:
return True
return False
def _migrate_legacy_articles(conn: sqlite3.Connection) -> None:
cur = conn.cursor()
cur.executescript(
"""
CREATE TABLE _articles_migrated (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
content_html TEXT,
status TEXT NOT NULL DEFAULT 'draft',
source TEXT NOT NULL DEFAULT 'manual',
account_id TEXT,
error_msg TEXT,
llm_target TEXT,
extra_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
)
cur.execute(
"SELECT id, title, content, content_html, status, account_id, error_msg, created_at, updated_at FROM articles"
)
ts = now_unix()
for row in cur.fetchall():
_oid, title, content, content_html, status, account_id, error_msg, cat, uat = row
body = content or ""
ch = content_html if content_html else None
cts = parse_ts_to_unix(cat) or ts
uts = parse_ts_to_unix(uat) or ts
cur.execute(
"""
INSERT INTO _articles_migrated (
title, body, content_html, status, source, account_id, error_msg,
created_at, updated_at
) VALUES (?, ?, ?, ?, 'import', ?, ?, ?, ?)
""",
(
title or "",
body,
ch,
(status or "draft"),
account_id,
error_msg,
cts,
uts,
),
)
cur.execute("DROP TABLE articles")
cur.execute("ALTER TABLE _articles_migrated RENAME TO articles")
conn.commit()
def init_db() -> None:
conn = get_conn()
try:
cur = conn.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='articles'")
if cur.fetchone():
if _is_legacy_articles_table(cur):
_migrate_legacy_articles(conn)
else:
cur.executescript(ARTICLES_TABLE_SQL)
cur.executescript(IMAGES_TABLE_SQL)
cur.executescript(VIDEOS_TABLE_SQL)
cur.executescript(PROMPT_TEMPLATES_TABLE_SQL)
cur.executescript(PROMPT_TEMPLATE_USAGE_TABLE_SQL)
from content_manager.db.prompts_repository import seed_prompt_templates_if_empty
seed_prompt_templates_if_empty(conn.cursor())
conn.commit()
finally:
conn.close()

View File

@@ -0,0 +1,88 @@
"""images 表:仅保存文件相对路径等元数据。"""
from __future__ import annotations
import sqlite3
from typing import Any, List, Optional, Tuple
def insert_row(
conn: sqlite3.Connection,
file_path: str,
title: Optional[str],
status: str,
source: str,
account_id: Optional[str],
error_msg: Optional[str],
extra_json: Optional[str],
created_at: int,
updated_at: int,
) -> int:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO images (
file_path, title, status, source, account_id, error_msg, extra_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(file_path, title, status, source, account_id, error_msg, extra_json, created_at, updated_at),
)
return int(cur.lastrowid)
def update_file_path(conn: sqlite3.Connection, image_id: int, file_path: str, updated_at: int) -> None:
cur = conn.cursor()
cur.execute(
"UPDATE images SET file_path = ?, updated_at = ? WHERE id = ?",
(file_path, updated_at, image_id),
)
def fetch_by_id(conn: sqlite3.Connection, image_id: int) -> Optional[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, file_path, title, status, source, account_id, error_msg, extra_json, created_at, updated_at
FROM images WHERE id = ?
""",
(image_id,),
)
return cur.fetchone()
def list_recent(conn: sqlite3.Connection, limit: int) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, file_path, title, status, source, account_id, error_msg, extra_json, created_at, updated_at
FROM images ORDER BY updated_at DESC, id DESC
LIMIT ?
""",
(int(limit),),
)
return list(cur.fetchall())
def delete_by_id(conn: sqlite3.Connection, image_id: int) -> int:
cur = conn.cursor()
cur.execute("DELETE FROM images WHERE id = ?", (image_id,))
return int(cur.rowcount)
def update_feedback(
conn: sqlite3.Connection,
image_id: int,
status: str,
account_id: Optional[str],
error_msg: Optional[str],
updated_at: int,
) -> None:
cur = conn.cursor()
cur.execute(
"""
UPDATE images
SET status = ?, account_id = ?, error_msg = ?, updated_at = ?
WHERE id = ?
""",
(status, account_id, error_msg, updated_at, image_id),
)

View File

@@ -0,0 +1,114 @@
"""提示词模板:表内数据访问与种子。"""
from __future__ import annotations
import random
import sqlite3
from typing import Any, Dict, List, Optional, Tuple
from content_manager.constants import PROMPT_TEMPLATE_SEEDS, PUBLISH_PLATFORM_CN
from content_manager.util.timeutil import now_unix
def seed_prompt_templates_if_empty(cur: sqlite3.Cursor) -> None:
ts = now_unix()
for platform, templates in PROMPT_TEMPLATE_SEEDS.items():
cur.execute("SELECT COUNT(*) FROM prompt_templates WHERE platform = ?", (platform,))
count = int(cur.fetchone()[0] or 0)
if count > 0:
continue
for idx, tpl in enumerate(templates, start=1):
cur.execute(
"""
INSERT INTO prompt_templates (platform, name, template_text, is_active, created_at, updated_at)
VALUES (?, ?, ?, 1, ?, ?)
""",
(platform, f"{PUBLISH_PLATFORM_CN.get(platform, platform)}模板{idx}", tpl, ts, ts),
)
def count_by_platform(cur: sqlite3.Cursor, platform: str) -> int:
cur.execute("SELECT COUNT(*) FROM prompt_templates WHERE platform = ?", (platform,))
return int(cur.fetchone()[0] or 0)
def fetch_active_templates(conn: sqlite3.Connection, platform: str) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, platform, name, template_text
FROM prompt_templates
WHERE platform = ? AND is_active = 1
ORDER BY id ASC
""",
(platform,),
)
return list(cur.fetchall())
def fetch_common_fallback(conn: sqlite3.Connection) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, platform, name, template_text
FROM prompt_templates
WHERE platform = 'common' AND is_active = 1
ORDER BY id ASC
"""
)
return list(cur.fetchall())
def pick_random_template(rows: List[Tuple[Any, ...]]) -> Optional[Dict[str, Any]]:
if not rows:
return None
rid, p, name, text = random.choice(rows)
return {"id": int(rid), "platform": p, "name": name, "template_text": text}
def insert_usage(
conn: sqlite3.Connection,
template_id: int,
llm_target: str,
platform: str,
topic: str,
article_id: Optional[int],
) -> None:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO prompt_template_usage (template_id, llm_target, platform, topic, article_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""",
(template_id, llm_target, platform, topic, article_id, now_unix()),
)
def list_templates(
conn: sqlite3.Connection,
platform: Optional[str],
limit: int,
) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
if platform:
cur.execute(
"""
SELECT id, platform, name, is_active, updated_at
FROM prompt_templates
WHERE platform = ?
ORDER BY id DESC
LIMIT ?
""",
(platform, int(limit)),
)
else:
cur.execute(
"""
SELECT id, platform, name, is_active, updated_at
FROM prompt_templates
ORDER BY id DESC
LIMIT ?
""",
(int(limit),),
)
return list(cur.fetchall())

View File

@@ -0,0 +1,73 @@
"""建表 SQL不含迁移逻辑"""
ARTICLES_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
content_html TEXT,
status TEXT NOT NULL DEFAULT 'draft',
source TEXT NOT NULL DEFAULT 'manual',
account_id TEXT,
error_msg TEXT,
llm_target TEXT,
extra_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
IMAGES_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
title TEXT,
status TEXT NOT NULL DEFAULT 'draft',
source TEXT NOT NULL DEFAULT 'manual',
account_id TEXT,
error_msg TEXT,
extra_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
VIDEOS_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS videos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
title TEXT,
duration_ms INTEGER,
status TEXT NOT NULL DEFAULT 'draft',
source TEXT NOT NULL DEFAULT 'manual',
account_id TEXT,
error_msg TEXT,
extra_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
PROMPT_TEMPLATES_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS prompt_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
name TEXT NOT NULL,
template_text TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
"""
PROMPT_TEMPLATE_USAGE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS prompt_template_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
template_id INTEGER NOT NULL,
llm_target TEXT NOT NULL,
platform TEXT NOT NULL,
topic TEXT NOT NULL,
article_id INTEGER,
created_at INTEGER NOT NULL
);
"""

View File

@@ -0,0 +1,100 @@
"""videos 表:仅保存文件相对路径等元数据。"""
from __future__ import annotations
import sqlite3
from typing import Any, List, Optional, Tuple
def insert_row(
conn: sqlite3.Connection,
file_path: str,
title: Optional[str],
duration_ms: Optional[int],
status: str,
source: str,
account_id: Optional[str],
error_msg: Optional[str],
extra_json: Optional[str],
created_at: int,
updated_at: int,
) -> int:
cur = conn.cursor()
cur.execute(
"""
INSERT INTO videos (
file_path, title, duration_ms, status, source, account_id, error_msg, extra_json, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
file_path,
title,
duration_ms,
status,
source,
account_id,
error_msg,
extra_json,
created_at,
updated_at,
),
)
return int(cur.lastrowid)
def update_file_path(conn: sqlite3.Connection, video_id: int, file_path: str, updated_at: int) -> None:
cur = conn.cursor()
cur.execute(
"UPDATE videos SET file_path = ?, updated_at = ? WHERE id = ?",
(file_path, updated_at, video_id),
)
def fetch_by_id(conn: sqlite3.Connection, video_id: int) -> Optional[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, file_path, title, duration_ms, status, source, account_id, error_msg, extra_json, created_at, updated_at
FROM videos WHERE id = ?
""",
(video_id,),
)
return cur.fetchone()
def list_recent(conn: sqlite3.Connection, limit: int) -> List[Tuple[Any, ...]]:
cur = conn.cursor()
cur.execute(
"""
SELECT id, file_path, title, duration_ms, status, source, account_id, error_msg, extra_json, created_at, updated_at
FROM videos ORDER BY updated_at DESC, id DESC
LIMIT ?
""",
(int(limit),),
)
return list(cur.fetchall())
def delete_by_id(conn: sqlite3.Connection, video_id: int) -> int:
cur = conn.cursor()
cur.execute("DELETE FROM videos WHERE id = ?", (video_id,))
return int(cur.rowcount)
def update_feedback(
conn: sqlite3.Connection,
video_id: int,
status: str,
account_id: Optional[str],
error_msg: Optional[str],
updated_at: int,
) -> None:
cur = conn.cursor()
cur.execute(
"""
UPDATE videos
SET status = ?, account_id = ?, error_msg = ?, updated_at = ?
WHERE id = ?
""",
(status, account_id, error_msg, updated_at, video_id),
)