22import hashlib
33import json
44import sqlite3
5+ import uuid
56from dataclasses import dataclass
7+ from datetime import UTC , datetime , timedelta
68from pathlib import Path
79
810
@@ -25,6 +27,12 @@ def total_imported(self) -> int:
2527 ))
2628
2729
30+ @dataclass (frozen = True )
31+ class QuotaReservation :
32+ reservation_id : str
33+ remaining : int
34+
35+
2836class RuntimeStore :
2937 """Owns durable runtime state without touching calibration data."""
3038
@@ -60,6 +68,22 @@ def initialize(self) -> None:
6068 usage_date TEXT NOT NULL,
6169 count INTEGER NOT NULL CHECK (count >= 0)
6270 );
71+ CREATE TABLE IF NOT EXISTS quota_reservations (
72+ reservation_id TEXT PRIMARY KEY,
73+ kind TEXT NOT NULL CHECK (kind IN ('activation', 'free')),
74+ code TEXT,
75+ client_hash TEXT,
76+ usage_date TEXT,
77+ state TEXT NOT NULL CHECK (state IN ('reserved', 'settled', 'released')),
78+ expires_at TEXT NOT NULL,
79+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
80+ CHECK (
81+ (kind = 'activation' AND code IS NOT NULL AND client_hash IS NULL)
82+ OR (kind = 'free' AND client_hash IS NOT NULL AND code IS NULL)
83+ )
84+ );
85+ CREATE INDEX IF NOT EXISTS quota_reservations_expiry
86+ ON quota_reservations(state, expires_at);
6387 CREATE TABLE IF NOT EXISTS fusion_generations (
6488 generation_id TEXT PRIMARY KEY,
6589 timestamp TEXT NOT NULL,
@@ -107,6 +131,184 @@ def initialize(self) -> None:
107131 """
108132 )
109133
134+ def seed_activation_codes (self , codes : dict ) -> int :
135+ """Insert environment-provided codes once without resetting consumed balances."""
136+ self .initialize ()
137+ inserted = 0
138+ with self .connect () as connection :
139+ connection .execute ("BEGIN IMMEDIATE" )
140+ for code , entry in codes .items ():
141+ if not isinstance (entry , dict ):
142+ continue
143+ remaining = entry .get ("剩余" , 0 )
144+ if not isinstance (remaining , int ) or remaining < 0 :
145+ continue
146+ result = connection .execute (
147+ "INSERT OR IGNORE INTO activation_codes(code, remaining, note) VALUES (?, ?, ?)" ,
148+ (str (code ).upper (), remaining , str (entry .get ("备注" , "" ))),
149+ )
150+ inserted += result .rowcount
151+ return inserted
152+
153+ def activation_remaining (self , code : str ) -> int | None :
154+ self .initialize ()
155+ with self .connect () as connection :
156+ row = connection .execute (
157+ "SELECT remaining FROM activation_codes WHERE code = ?" , (code .strip ().upper (),)
158+ ).fetchone ()
159+ return int (row ["remaining" ]) if row else None
160+
161+ def free_remaining (self , client_hash : str , usage_date : str , limit : int ) -> int :
162+ self .initialize ()
163+ with self .connect () as connection :
164+ row = connection .execute (
165+ "SELECT usage_date, count FROM free_usage WHERE client_hash = ?" , (client_hash ,)
166+ ).fetchone ()
167+ used = int (row ["count" ]) if row and row ["usage_date" ] == usage_date else 0
168+ return max (0 , limit - used )
169+
170+ def reserve_activation (self , code : str , * , expiry_seconds : int = 300 ) -> QuotaReservation | None :
171+ self .initialize ()
172+ normalized_code = code .strip ().upper ()
173+ now = self ._now ()
174+ with self .connect () as connection :
175+ connection .execute ("BEGIN IMMEDIATE" )
176+ self ._release_expired (connection , now )
177+ updated = connection .execute (
178+ """
179+ UPDATE activation_codes
180+ SET remaining = remaining - 1, updated_at = CURRENT_TIMESTAMP
181+ WHERE code = ? AND remaining > 0
182+ """ ,
183+ (normalized_code ,),
184+ ).rowcount
185+ if not updated :
186+ return None
187+ remaining = connection .execute (
188+ "SELECT remaining FROM activation_codes WHERE code = ?" , (normalized_code ,)
189+ ).fetchone ()["remaining" ]
190+ reservation_id = uuid .uuid4 ().hex
191+ connection .execute (
192+ """
193+ INSERT INTO quota_reservations(reservation_id, kind, code, state, expires_at)
194+ VALUES (?, 'activation', ?, 'reserved', ?)
195+ """ ,
196+ (reservation_id , normalized_code , self ._expires_at (now , expiry_seconds )),
197+ )
198+ return QuotaReservation (reservation_id , int (remaining ))
199+
200+ def reserve_free (
201+ self ,
202+ client_hash : str ,
203+ usage_date : str ,
204+ limit : int ,
205+ * ,
206+ expiry_seconds : int = 300 ,
207+ ) -> QuotaReservation | None :
208+ self .initialize ()
209+ now = self ._now ()
210+ with self .connect () as connection :
211+ connection .execute ("BEGIN IMMEDIATE" )
212+ self ._release_expired (connection , now )
213+ row = connection .execute (
214+ "SELECT usage_date, count FROM free_usage WHERE client_hash = ?" , (client_hash ,)
215+ ).fetchone ()
216+ used = int (row ["count" ]) if row and row ["usage_date" ] == usage_date else 0
217+ if used >= limit :
218+ return None
219+ next_count = used + 1
220+ connection .execute (
221+ """
222+ INSERT INTO free_usage(client_hash, usage_date, count) VALUES (?, ?, ?)
223+ ON CONFLICT(client_hash) DO UPDATE SET usage_date = excluded.usage_date, count = excluded.count
224+ """ ,
225+ (client_hash , usage_date , next_count ),
226+ )
227+ reservation_id = uuid .uuid4 ().hex
228+ connection .execute (
229+ """
230+ INSERT INTO quota_reservations(
231+ reservation_id, kind, client_hash, usage_date, state, expires_at
232+ ) VALUES (?, 'free', ?, ?, 'reserved', ?)
233+ """ ,
234+ (reservation_id , client_hash , usage_date , self ._expires_at (now , expiry_seconds )),
235+ )
236+ return QuotaReservation (reservation_id , limit - next_count )
237+
238+ def settle_reservation (self , reservation_id : str ) -> bool :
239+ """Mark a delivered response as consumed. Repeated settlement is harmless."""
240+ return self ._finish_reservation (reservation_id , consume = True )
241+
242+ def release_reservation (self , reservation_id : str ) -> bool :
243+ """Return a reservation that failed before delivering a response."""
244+ return self ._finish_reservation (reservation_id , consume = False )
245+
246+ def _finish_reservation (self , reservation_id : str , * , consume : bool ) -> bool :
247+ self .initialize ()
248+ with self .connect () as connection :
249+ connection .execute ("BEGIN IMMEDIATE" )
250+ row = connection .execute (
251+ "SELECT * FROM quota_reservations WHERE reservation_id = ?" , (reservation_id ,)
252+ ).fetchone ()
253+ if row is None or row ["state" ] != "reserved" :
254+ return False
255+ if consume :
256+ connection .execute (
257+ "UPDATE quota_reservations SET state = 'settled' WHERE reservation_id = ?" ,
258+ (reservation_id ,),
259+ )
260+ return True
261+ self ._restore_reservation (connection , row )
262+ connection .execute (
263+ "UPDATE quota_reservations SET state = 'released' WHERE reservation_id = ?" ,
264+ (reservation_id ,),
265+ )
266+ return True
267+
268+ @staticmethod
269+ def _now () -> datetime :
270+ return datetime .now (UTC )
271+
272+ @staticmethod
273+ def _expires_at (now : datetime , expiry_seconds : int ) -> str :
274+ return (now + timedelta (seconds = max (1 , expiry_seconds ))).isoformat ()
275+
276+ def _release_expired (self , connection : sqlite3 .Connection , now : datetime ) -> None :
277+ rows = connection .execute (
278+ """
279+ SELECT * FROM quota_reservations
280+ WHERE state = 'reserved' AND expires_at <= ?
281+ """ ,
282+ (now .isoformat (),),
283+ ).fetchall ()
284+ for row in rows :
285+ self ._restore_reservation (connection , row )
286+ connection .execute (
287+ "UPDATE quota_reservations SET state = 'released' WHERE reservation_id = ?" ,
288+ (row ["reservation_id" ],),
289+ )
290+
291+ @staticmethod
292+ def _restore_reservation (connection : sqlite3 .Connection , row : sqlite3 .Row ) -> None :
293+ if row ["kind" ] == "activation" :
294+ connection .execute (
295+ """
296+ UPDATE activation_codes
297+ SET remaining = remaining + 1, updated_at = CURRENT_TIMESTAMP
298+ WHERE code = ?
299+ """ ,
300+ (row ["code" ],),
301+ )
302+ else :
303+ connection .execute (
304+ """
305+ UPDATE free_usage
306+ SET count = CASE WHEN count > 0 THEN count - 1 ELSE 0 END
307+ WHERE client_hash = ? AND usage_date = ?
308+ """ ,
309+ (row ["client_hash" ], row ["usage_date" ]),
310+ )
311+
110312 def summary_counts (self ) -> dict [str , int ]:
111313 self .initialize ()
112314 tables = (
0 commit comments