Spaces:
Sleeping
Sleeping
| """状态层: 单体全局状态 + 锁 + 原子落盘 + HF Dataset 持久化. | |
| 设计要点: | |
| - threading.Lock 全局唯一;限流检查与递增在同一把锁内 | |
| - 写盘: 临时文件 + os.replace 原子改名;CommitScheduler 5min 周期 | |
| - 关键事件(蜕皮/生病/康复)调用 scheduler.trigger() 立即提交 | |
| - 启动时显式从 Dataset 拉最新状态(Space 重启不丢命) | |
| - snapshots: 每次喂养一帧; events: 日记与回放的史料 | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| import threading | |
| import time | |
| from datetime import datetime, timedelta, timezone | |
| from traits import TRAIT_KEYS | |
| DATA_DIR = os.environ.get("CRICKET_DATA", "data") | |
| STATE_PATH = os.path.join(DATA_DIR, "state.json") | |
| DATASET_REPO = os.environ.get("CRICKET_DATASET", "") # e.g. "lerp/cricket-commons-state" | |
| DAILY_FEED_LIMIT = int(os.environ.get("CRICKET_DAILY_LIMIT", "10")) | |
| CST = timezone(timedelta(hours=8)) # UTC+8 切日 | |
| LOCK = threading.Lock() | |
| _scheduler = None | |
| def _today() -> str: | |
| return datetime.now(CST).strftime("%Y-%m-%d") | |
| def new_state() -> dict: | |
| now = time.time() | |
| return { | |
| "born_at": now, | |
| "name": "未命名小蛐蛐", | |
| "traits": {k: 0.5 for k in TRAIT_KEYS}, | |
| "momentum": {k: 0.0 for k in TRAIT_KEYS}, | |
| "mood": "calm", | |
| "feed_count": 0, | |
| "sick": False, | |
| "sick_since": None, | |
| "recover_progress": 0, | |
| "diary": [], # [{date, zh, en}] | |
| "battles": [], # [{ts, challenger_text, reply, win}] | |
| "snapshots": [], # [{ts, traits, svg_params}] | |
| "events": [], # [{ts, type, input_digest, reaction_digest}] | |
| "feeders": {}, # {fingerprint: {date, count}} 软限流 | |
| } | |
| def load_state() -> dict: | |
| """启动流程: 先尝试从 Dataset 拉最新,再读本地,最后新生.""" | |
| os.makedirs(DATA_DIR, exist_ok=True) | |
| if DATASET_REPO and not os.path.exists(STATE_PATH): | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| p = hf_hub_download(repo_id=DATASET_REPO, filename="state.json", | |
| repo_type="dataset", local_dir=DATA_DIR) | |
| print(f"[state] restored from dataset: {p}") | |
| except Exception as e: | |
| print(f"[state] no remote state ({e}); fresh born") | |
| if os.path.exists(STATE_PATH): | |
| with open(STATE_PATH, encoding="utf-8") as f: | |
| return json.load(f) | |
| s = new_state() | |
| save_state(s) | |
| return s | |
| def _scheduler_lock(): | |
| global _scheduler | |
| if _scheduler is not None: | |
| return _scheduler.lock | |
| return threading.Lock() | |
| def save_state(state: dict) -> None: | |
| """临时文件 + 原子改名;若 scheduler 存在则在其 lock 内写.""" | |
| os.makedirs(DATA_DIR, exist_ok=True) | |
| tmp = STATE_PATH + ".tmp" | |
| lock = _scheduler.lock if _scheduler is not None else threading.Lock() | |
| with lock: | |
| with open(tmp, "w", encoding="utf-8") as f: | |
| json.dump(state, f, ensure_ascii=False) | |
| os.replace(tmp, STATE_PATH) | |
| def start_scheduler() -> None: | |
| """CommitScheduler: 每 5 分钟把 data/ 推到 Dataset repo.""" | |
| global _scheduler | |
| if not DATASET_REPO: | |
| print("[state] CRICKET_DATASET unset; persistence disabled (local only)") | |
| return | |
| from huggingface_hub import CommitScheduler | |
| _scheduler = CommitScheduler( | |
| repo_id=DATASET_REPO, | |
| repo_type="dataset", | |
| folder_path=DATA_DIR, | |
| every=5, # minutes | |
| ) | |
| print(f"[state] CommitScheduler started → {DATASET_REPO}") | |
| def trigger_commit() -> None: | |
| """关键事件(蜕皮/生病/康复)立即提交.""" | |
| if _scheduler is not None: | |
| try: | |
| _scheduler.trigger() | |
| except Exception as e: | |
| print(f"[state] trigger failed: {e}") | |
| def check_and_count_feed(state: dict, fingerprint: str) -> bool: | |
| """软限流: 同一把锁内检查+递增(调用方需已持有 LOCK). | |
| 返回 True=允许喂养.""" | |
| today = _today() | |
| rec = state["feeders"].get(fingerprint) | |
| if rec is None or rec.get("date") != today: | |
| rec = {"date": today, "count": 0} | |
| if rec["count"] >= DAILY_FEED_LIMIT: | |
| return False | |
| rec["count"] += 1 | |
| state["feeders"][fingerprint] = rec | |
| # 防 feeders 无限膨胀: 只留今天的 | |
| if len(state["feeders"]) > 5000: | |
| state["feeders"] = {k: v for k, v in state["feeders"].items() if v.get("date") == today} | |
| return True | |
| def append_event(state: dict, etype: str, input_digest: str = "", reaction_digest: str = "") -> None: | |
| state["events"].append({ | |
| "ts": time.time(), "type": etype, | |
| "input": (input_digest or "")[:80], "reaction": (reaction_digest or "")[:80], | |
| }) | |
| def append_snapshot(state: dict, svg_params: dict) -> None: | |
| state["snapshots"].append({"ts": time.time(), **svg_params}) | |
| def age_days(state: dict) -> float: | |
| return (time.time() - state.get("born_at", time.time())) / 86400.0 | |
| def events_for_day(state: dict, date_str: str) -> list[dict]: | |
| """某天(UTC+8)的事件,日记的史料.""" | |
| out = [] | |
| for e in state.get("events", []): | |
| d = datetime.fromtimestamp(e["ts"], CST).strftime("%Y-%m-%d") | |
| if d == date_str: | |
| out.append(e) | |
| return out | |