Files
HeXi/hexi/plugins/nonebot_plugin_helldivers_tools/equipment_store.py
T
sansenhoshiandClaude b61d09f09f Add HeXi bot codebase: custom plugins, web frontends, tests
- hexi core: message handling, rate limiting, cooldown, plugin manager
- Custom plugins: BF stats, daily check-in, quotes, persona cards, etc.
- Community plugins vendored under hexi/plugins with local fixes
- Web admin frontends (learning-chat, persona-admin), unified hexi/web
- Tests for rate_limit/cooldown/memes/persona; poetry.lock

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-01 13:13:40 +08:00

321 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""战备(SQLite)数据存取层。
把原来写在 data/equipment.json 的战备数据迁移到 SQLite,供
- equipment.py 随机/组合抽取
- 战备管理后台(stratagem_admin.py) 的读/写/导入/导出
三层语义弱化为单表 + is_custom 标记:
- 从 data/equipment.json 首次播种的记录 is_custom=0(基础库)
- 管理后台手动新增的记录 is_custom=1
- 手动编辑都是直接改行,不另建覆盖层(当前不做 API 同步,因此单表即可)
"""
from __future__ import annotations
import json
import sqlite3
import time
from pathlib import Path
from typing import Any
from nonebot import logger
basic_path = Path(__file__).resolve().parent
DATA_DIR = basic_path / "data"
DB_PATH = DATA_DIR / "equipment.db"
SEED_PATH = DATA_DIR / "archive" / "equipment.json"
def _connect() -> sqlite3.Connection:
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def _now() -> str:
return time.strftime("%Y-%m-%d %H:%M:%S")
def _load_seed() -> list[dict[str, Any]]:
"""读取随包的 equipment.json 作为初始基础库。"""
if not SEED_PATH.exists():
return []
try:
data = json.loads(SEED_PATH.read_text(encoding="utf-8"))
return data if isinstance(data, list) else []
except Exception as e: # noqa: BLE001
logger.warning(f"读取 equipment.json 失败,跳过播种: {e}")
return []
def init_db() -> None:
"""建表(WAL 便于网页与 bot 并发读写),空表时从 equipment.json 播种。"""
DATA_DIR.mkdir(parents=True, exist_ok=True)
with _connect() as conn:
conn.execute("PRAGMA journal_mode=WAL")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS stratagems (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
name_en TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT 'blue',
backpack INTEGER NOT NULL DEFAULT 0,
icon TEXT NOT NULL DEFAULT '',
alias TEXT NOT NULL DEFAULT '[]',
is_custom INTEGER NOT NULL DEFAULT 0,
created_at TEXT,
updated_at TEXT,
UNIQUE(name)
)
"""
)
cur = conn.execute("SELECT COUNT(*) AS n FROM stratagems")
if cur.fetchone()["n"] == 0:
now = _now()
for item in _load_seed():
if not isinstance(item, dict):
continue
name = (item.get("name") or "").strip()
if not name:
continue
alias = item.get("alias") or []
if isinstance(alias, str):
alias = [a.strip() for a in alias.split(",") if a.strip()]
conn.execute(
"""
INSERT OR IGNORE INTO stratagems
(name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)
""",
(
name,
item.get("name_en") or "",
item.get("type") or "blue",
1 if item.get("backpack") else 0,
item.get("path") or item.get("icon") or "",
json.dumps(alias, ensure_ascii=False),
0,
now,
now,
),
)
conn.commit()
def _row_to_item(row: sqlite3.Row) -> dict[str, Any]:
try:
alias = json.loads(row["alias"] or "[]")
if not isinstance(alias, list):
alias = []
except Exception: # noqa: BLE001
alias = []
return {
"id": row["id"],
"name": row["name"],
"name_en": row["name_en"] or "",
"type": row["type"] or "blue",
"backpack": bool(row["backpack"]),
"path": row["icon"] or "",
"icon": row["icon"] or "",
"alias": alias,
"is_custom": bool(row["is_custom"]),
}
def _normalize_alias(value: Any) -> list[str]:
if isinstance(value, str):
parts = [v.strip() for v in value.replace(",", ",").replace("、", ",").replace("\n", ",").split(",") if v.strip()]
elif isinstance(value, list):
parts = [v.strip() for v in value if isinstance(v, str) and v.strip()]
else:
parts = []
seen: set[str] = set()
out: list[str] = []
for p in parts:
if p not in seen:
seen.add(p)
out.append(p)
return out
def list_stratagems() -> list[dict[str, Any]]:
init_db()
with _connect() as conn:
rows = conn.execute("SELECT * FROM stratagems ORDER BY id").fetchall()
return [_row_to_item(r) for r in rows]
def count_stratagems() -> int:
init_db()
with _connect() as conn:
return int(conn.execute("SELECT COUNT(*) AS n FROM stratagems").fetchone()["n"])
def list_stratagems_page(page: int = 1, page_size: int = 20) -> tuple[list[dict[str, Any]], int]:
init_db()
total = count_stratagems()
offset = (page - 1) * page_size
with _connect() as conn:
rows = conn.execute(
"SELECT * FROM stratagems ORDER BY id LIMIT ? OFFSET ?",
(page_size, offset),
).fetchall()
return [_row_to_item(r) for r in rows], total
def get_stratagem(seed: int) -> dict[str, Any] | None:
init_db()
with _connect() as conn:
row = conn.execute("SELECT * FROM stratagems WHERE id=?", (seed,)).fetchone()
return _row_to_item(row) if row else None
def create_stratagem(item: dict[str, Any]) -> dict[str, Any]:
init_db()
name = (item.get("name") or "").strip()
if not name:
raise ValueError("name 不能为空")
now = _now()
with _connect() as conn:
cursor = conn.execute(
"""
INSERT INTO stratagems (name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)
""",
(
name,
item.get("name_en") or "",
item.get("type") or "blue",
1 if item.get("backpack") else 0,
item.get("icon") or item.get("path") or "",
json.dumps(_normalize_alias(item.get("alias")), ensure_ascii=False),
1,
now,
now,
),
)
conn.commit()
new_id = cursor.lastrowid
row = get_stratagem(new_id)
assert row is not None
return row
def update_stratagem(seed: int, fields: dict[str, Any]) -> dict[str, Any] | None:
init_db()
if not fields:
return get_stratagem(seed)
# 只允许安全字段
allowed = {"name", "name_en", "type", "backpack", "icon", "path", "alias"}
updates: dict[str, Any] = {}
for k in allowed:
if k in fields:
updates[k] = fields[k]
if "alias" in updates:
updates["alias"] = json.dumps(_normalize_alias(updates["alias"]), ensure_ascii=False)
if "path" in updates and "icon" not in updates:
updates["icon"] = updates.pop("path")
elif "icon" in updates:
updates.pop("path", None)
if not updates:
return get_stratagem(seed)
updates["updated_at"] = _now()
sets = ", ".join(f"{k}=?" for k in updates)
params = list(updates.values()) + [seed]
with _connect() as conn:
cur = conn.execute(f"UPDATE stratagems SET {sets} WHERE id=?", params)
conn.commit()
if cur.rowcount == 0:
return None
return get_stratagem(seed)
def delete_stratagem(seed: int) -> bool:
init_db()
with _connect() as conn:
cur = conn.execute("DELETE FROM stratagems WHERE id=?", (seed,))
conn.commit()
return cur.rowcount > 0
def import_items(items: list[dict[str, Any]]) -> dict[str, int]:
"""按 id(存在则更新) 或 name(存在则更新) 合并导入;新增的记录标记为 custom。
返回 {added, updated, skipped}
"""
init_db()
added = 0
updated = 0
skipped = 0
now = _now()
with _connect() as conn:
for item in items:
if not isinstance(item, dict):
skipped += 1
continue
name = (item.get("name") or "").strip()
if not name:
skipped += 1
continue
seed = item.get("id")
existing: dict[str, Any] | None = None
if isinstance(seed, int):
row = conn.execute("SELECT * FROM stratagems WHERE id=?", (seed,)).fetchone()
existing = _row_to_item(row) if row else None
if existing is None:
row = conn.execute("SELECT * FROM stratagems WHERE name=?", (name,)).fetchone()
existing = _row_to_item(row) if row else None
if existing is None:
conn.execute(
"""
INSERT INTO stratagems (name, name_en, type, backpack, icon, alias, is_custom, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?)
""",
(
name,
item.get("name_en") or "",
item.get("type") or "blue",
1 if item.get("backpack") else 0,
item.get("icon") or item.get("path") or "",
json.dumps(_normalize_alias(item.get("alias")), ensure_ascii=False),
1,
now,
now,
),
)
added += 1
else:
fields = {
"name": name,
"name_en": (item.get("name_en") or existing.get("name_en") or ""),
"type": (item.get("type") or existing.get("type") or "blue"),
"backpack": bool(item.get("backpack", existing.get("backpack"))),
"icon": (item.get("icon") or item.get("path") or existing.get("icon") or ""),
"alias": item.get("alias", existing.get("alias")),
}
conn.execute(
"""
UPDATE stratagems
SET name=?, name_en=?, type=?, backpack=?, icon=?, alias=?, updated_at=?
WHERE id=?
""",
(
fields["name"],
fields["name_en"],
fields["type"],
1 if fields["backpack"] else 0,
fields["icon"],
json.dumps(_normalize_alias(fields["alias"]), ensure_ascii=False),
now,
existing["id"],
),
)
updated += 1
conn.commit()
return {"added": added, "updated": updated, "skipped": skipped}
def export_items() -> list[dict[str, Any]]:
return list_stratagems()