Spaces:
Paused
Paused
File size: 5,649 Bytes
9a69412 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | # app/store_sqlite.py
import json
import os
import sqlite3
import threading
import time
WINDOW = 6 # เก็บกี่เทิร์นล่าสุด
TURN_TTL = 7200 # 2 ชม.
SESS_TTL = 86400 # 24 ชม.
def _pick_db_path() -> str:
"""หาโฟลเดอร์ที่เขียนได้จริง — Space ฟรีไม่มี /data"""
cands = []
if os.environ.get("DB_PATH"):
cands.append(os.environ["DB_PATH"])
cands += ["/data/npc.db", "/home/user/app/data/npc.db", "/tmp/npc.db"]
for c in cands:
try:
d = os.path.dirname(c) or "."
os.makedirs(d, exist_ok=True)
probe = os.path.join(d, ".wtest")
with open(probe, "w") as f:
f.write("1")
os.remove(probe)
return c
except Exception:
continue
return ":memory:"
DB_PATH = _pick_db_path()
print(f"[store] db path = {DB_PATH}", flush=True)
_conn = sqlite3.connect(DB_PATH, check_same_thread=False, timeout=5.0)
_conn.execute("PRAGMA journal_mode=WAL") # อ่าน/เขียนพร้อมกันได้
_conn.execute("PRAGMA synchronous=NORMAL") # เร็วขึ้น ~3x
_conn.execute("PRAGMA busy_timeout=5000")
_lock = threading.Lock()
_conn.executescript("""
CREATE TABLE IF NOT EXISTS turns (
sid TEXT, seq INTEGER, u TEXT, a TEXT, beat TEXT, ts INTEGER,
PRIMARY KEY (sid, seq));
CREATE TABLE IF NOT EXISTS sessions (
sid TEXT PRIMARY KEY, pid TEXT, npc TEXT,
summary TEXT DEFAULT '', n INTEGER DEFAULT 0, ts INTEGER);
CREATE TABLE IF NOT EXISTS players (
pid TEXT PRIMARY KEY, name TEXT, facts TEXT DEFAULT '{}',
rel TEXT DEFAULT '{}', quests TEXT DEFAULT '{}',
v INTEGER DEFAULT 0, last_seen INTEGER);
CREATE TABLE IF NOT EXISTS rl (
pid TEXT, minute INTEGER, n INTEGER, PRIMARY KEY (pid, minute));
CREATE INDEX IF NOT EXISTS ix_turns ON turns(sid, seq DESC);
""")
_conn.commit()
# ─────────── window ───────────
def push_turn(sid, pid, npc, u, a):
now = int(time.time())
with _lock, _conn:
_conn.execute(
"INSERT INTO sessions(sid,pid,npc,n,ts) VALUES(?,?,?,1,?) "
"ON CONFLICT(sid) DO UPDATE SET n=n+1, ts=excluded.ts",
(sid, pid, npc, now))
n = _conn.execute("SELECT n FROM sessions WHERE sid=?", (sid,)).fetchone()[0]
_conn.execute("INSERT OR REPLACE INTO turns VALUES(?,?,?,?,?,?)",
(sid, n, u, a, None, now))
_conn.execute("DELETE FROM turns WHERE sid=? AND seq<=?", (sid, n - WINDOW))
return n
def get_window(sid):
rows = _conn.execute(
"SELECT u,a,beat FROM turns WHERE sid=? ORDER BY seq ASC", (sid,)).fetchall()
return [{"u": r[0], "a": r[1], "beat": r[2]} for r in rows]
def get_turn_count(sid):
r = _conn.execute("SELECT n FROM sessions WHERE sid=?", (sid,)).fetchone()
return r[0] if r else 0
def reset_session(sid):
with _lock, _conn:
_conn.execute("DELETE FROM turns WHERE sid=?", (sid,))
_conn.execute("DELETE FROM sessions WHERE sid=?", (sid,))
# ─────────── summary ───────────
def get_summary(sid):
r = _conn.execute("SELECT summary FROM sessions WHERE sid=?", (sid,)).fetchone()
return (r[0] if r else "") or ""
def set_summary(sid, text):
with _lock, _conn:
_conn.execute("UPDATE sessions SET summary=? WHERE sid=?", (text, sid))
# ─────────── profile ───────────
def get_profile(pid):
r = _conn.execute(
"SELECT name,facts,rel,quests,v FROM players WHERE pid=?", (pid,)).fetchone()
if not r:
return {"pid": pid, "name": None, "facts": {}, "rel": {}, "quests": {}, "v": 0}
return {"pid": pid, "name": r[0], "facts": json.loads(r[1]),
"rel": json.loads(r[2]), "quests": json.loads(r[3]), "v": r[4]}
def save_profile(p):
with _lock, _conn:
_conn.execute(
"INSERT INTO players(pid,name,facts,rel,quests,v,last_seen) "
"VALUES(?,?,?,?,?,?,?) ON CONFLICT(pid) DO UPDATE SET "
"name=excluded.name, facts=excluded.facts, rel=excluded.rel, "
"quests=excluded.quests, v=players.v+1, last_seen=excluded.last_seen",
(p["pid"], p.get("name"), json.dumps(p.get("facts", {})),
json.dumps(p.get("rel", {})), json.dumps(p.get("quests", {})),
p.get("v", 0) + 1, int(time.time())))
# ─────────── rate limit ───────────
def rate_limit(pid, limit=12):
m = int(time.time() // 60)
with _lock, _conn:
_conn.execute("INSERT INTO rl(pid,minute,n) VALUES(?,?,1) "
"ON CONFLICT(pid,minute) DO UPDATE SET n=n+1", (pid, m))
n = _conn.execute("SELECT n FROM rl WHERE pid=? AND minute=?",
(pid, m)).fetchone()[0]
return n <= limit
# ─────────── janitor (แทน TTL ของ Redis) ───────────
def janitor():
now = int(time.time())
with _lock, _conn:
_conn.execute("DELETE FROM rl WHERE minute < ?", (now // 60 - 2,))
_conn.execute("DELETE FROM turns WHERE ts < ?", (now - TURN_TTL,))
_conn.execute("DELETE FROM sessions WHERE ts < ?", (now - SESS_TTL,))
def stats():
q = lambda s: _conn.execute(s).fetchone()[0]
return {"db": DB_PATH, "turns": q("SELECT COUNT(*) FROM turns"),
"sessions": q("SELECT COUNT(*) FROM sessions"),
"players": q("SELECT COUNT(*) FROM players")} |