from __future__ import annotations import hashlib import hmac import secrets import sqlite3 import time from pathlib import Path class UserStore: def __init__(self, db_path: str) -> None: self._db_path = db_path p = Path(db_path) if p.parent and not p.parent.exists(): p.parent.mkdir(parents=True, exist_ok=True) self._init_db() def _conn(self) -> sqlite3.Connection: c = sqlite3.connect(self._db_path) c.row_factory = sqlite3.Row return c def _init_db(self) -> None: with self._conn() as c: self._ensure_users_table(c) self._ensure_sessions_table(c) c.execute( """ CREATE TABLE IF NOT EXISTS wechat_bindings ( user_id INTEGER PRIMARY KEY, appid TEXT NOT NULL, secret TEXT NOT NULL, author TEXT NOT NULL DEFAULT '', thumb_media_id TEXT NOT NULL DEFAULT '', thumb_image_path TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) c.execute( """ CREATE TABLE IF NOT EXISTS wechat_accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, account_name TEXT NOT NULL, appid TEXT NOT NULL, secret TEXT NOT NULL, author TEXT NOT NULL DEFAULT '', thumb_media_id TEXT NOT NULL DEFAULT '', thumb_image_path TEXT NOT NULL DEFAULT '', updated_at INTEGER NOT NULL, UNIQUE(user_id, account_name), FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) c.execute( """ CREATE TABLE IF NOT EXISTS user_prefs ( user_id INTEGER PRIMARY KEY, active_wechat_account_id INTEGER, updated_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) pref_cols = self._table_columns(c, "user_prefs") if "active_ai_model_id" not in pref_cols: c.execute("ALTER TABLE user_prefs ADD COLUMN active_ai_model_id INTEGER") c.execute( """ CREATE TABLE IF NOT EXISTS ai_models ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, model_name TEXT NOT NULL, api_key TEXT NOT NULL, base_url TEXT NOT NULL DEFAULT '', model TEXT NOT NULL, timeout_sec REAL NOT NULL DEFAULT 120.0, max_output_tokens INTEGER NOT NULL DEFAULT 8192, max_retries INTEGER NOT NULL DEFAULT 0, updated_at INTEGER NOT NULL, UNIQUE(user_id, model_name), FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) # 兼容历史单绑定结构,自动迁移为默认账号 rows = c.execute( "SELECT user_id, appid, secret, author, thumb_media_id, thumb_image_path, updated_at FROM wechat_bindings" ).fetchall() for r in rows: now = int(time.time()) cur = c.execute( """ INSERT OR IGNORE INTO wechat_accounts (user_id, account_name, appid, secret, author, thumb_media_id, thumb_image_path, updated_at) VALUES (?, '默认公众号', ?, ?, ?, ?, ?, ?) """, ( int(r["user_id"]), r["appid"] or "", r["secret"] or "", r["author"] or "", r["thumb_media_id"] or "", r["thumb_image_path"] or "", int(r["updated_at"] or now), ), ) if cur.rowcount: aid = int( c.execute( "SELECT id FROM wechat_accounts WHERE user_id=? AND account_name='默认公众号'", (int(r["user_id"]),), ).fetchone()["id"] ) c.execute( """ INSERT INTO user_prefs(user_id, active_wechat_account_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_wechat_account_id=COALESCE(user_prefs.active_wechat_account_id, excluded.active_wechat_account_id), updated_at=excluded.updated_at """, (int(r["user_id"]), aid, now), ) def _table_columns(self, c: sqlite3.Connection, table_name: str) -> set[str]: rows = c.execute(f"PRAGMA table_info({table_name})").fetchall() return {str(r["name"]) for r in rows} def _ensure_users_table(self, c: sqlite3.Connection) -> None: required = { "id", "username", "password_hash", "password_salt", "reset_code_hash", "reset_code_salt", "created_at", "deleted_at", } c.execute( """ CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, reset_code_hash TEXT NOT NULL DEFAULT '', reset_code_salt TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, deleted_at INTEGER ) """ ) cols = self._table_columns(c, "users") if required.issubset(cols): return now = int(time.time()) c.execute("PRAGMA foreign_keys=OFF") c.execute("DROP TABLE IF EXISTS users_new") c.execute( """ CREATE TABLE users_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, password_salt TEXT NOT NULL, reset_code_hash TEXT NOT NULL, reset_code_salt TEXT NOT NULL, created_at INTEGER NOT NULL, deleted_at INTEGER ) """ ) if {"username", "password_hash", "password_salt"}.issubset(cols): rows = c.execute("SELECT * FROM users").fetchall() for r in rows: username = (r["username"] or "").strip() if not username: continue reset_salt = r["reset_code_salt"] if "reset_code_salt" in cols else secrets.token_hex(8) reset_hash = r["reset_code_hash"] if "reset_code_hash" in cols else "" if not reset_hash: legacy_code = self._generate_reset_code() reset_hash = self._hash_reset_code(legacy_code, reset_salt) created_at = int(r["created_at"]) if "created_at" in cols and r["created_at"] else now deleted_at = int(r["deleted_at"]) if "deleted_at" in cols and r["deleted_at"] else None c.execute( """ INSERT OR IGNORE INTO users_new( id, username, password_hash, password_salt, reset_code_hash, reset_code_salt, created_at, deleted_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( int(r["id"]), username, r["password_hash"], r["password_salt"], reset_hash, reset_salt, created_at, deleted_at, ), ) elif {"username", "password"}.issubset(cols): if "created_at" in cols: rows = c.execute("SELECT id, username, password, created_at FROM users").fetchall() else: rows = c.execute("SELECT id, username, password FROM users").fetchall() for r in rows: username = (r["username"] or "").strip() raw_pwd = str(r["password"] or "") if not username or not raw_pwd: continue salt = secrets.token_hex(16) pwd_hash = self._hash_password(raw_pwd, salt) reset_code = self._generate_reset_code() reset_salt = secrets.token_hex(8) reset_hash = self._hash_reset_code(reset_code, reset_salt) created_at = int(r["created_at"]) if "created_at" in r.keys() and r["created_at"] else now c.execute( """ INSERT OR IGNORE INTO users_new( id, username, password_hash, password_salt, reset_code_hash, reset_code_salt, created_at, deleted_at ) VALUES (?, ?, ?, ?, ?, ?, ?, NULL) """, (int(r["id"]), username, pwd_hash, salt, reset_hash, reset_salt, created_at), ) c.execute("DROP TABLE users") c.execute("ALTER TABLE users_new RENAME TO users") c.execute("PRAGMA foreign_keys=ON") def _ensure_sessions_table(self, c: sqlite3.Connection) -> None: required = {"token_hash", "user_id", "expires_at", "created_at"} c.execute( """ CREATE TABLE IF NOT EXISTS sessions ( token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) cols = self._table_columns(c, "sessions") if required.issubset(cols): return c.execute("DROP TABLE IF EXISTS sessions") c.execute( """ CREATE TABLE sessions ( token_hash TEXT PRIMARY KEY, user_id INTEGER NOT NULL, expires_at INTEGER NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(user_id) REFERENCES users(id) ) """ ) def _hash_password(self, password: str, salt: str) -> str: data = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 120_000) return data.hex() def _hash_token(self, token: str) -> str: return hashlib.sha256(token.encode("utf-8")).hexdigest() def _generate_reset_code(self) -> str: return secrets.token_urlsafe(9).replace("-", "").replace("_", "")[:12] def _hash_reset_code(self, reset_code: str, salt: str) -> str: return hashlib.sha256(f"{salt}:{reset_code}".encode("utf-8")).hexdigest() def create_user(self, username: str, password: str) -> dict | None: now = int(time.time()) salt = secrets.token_hex(16) pwd_hash = self._hash_password(password, salt) reset_code = self._generate_reset_code() reset_salt = secrets.token_hex(8) reset_hash = self._hash_reset_code(reset_code, reset_salt) try: with self._conn() as c: cur = c.execute( """ INSERT INTO users( username, password_hash, password_salt, reset_code_hash, reset_code_salt, created_at, deleted_at ) VALUES (?, ?, ?, ?, ?, ?, NULL) """, (username, pwd_hash, salt, reset_hash, reset_salt, now), ) uid = int(cur.lastrowid) return {"id": uid, "username": username, "reset_code": reset_code} except sqlite3.IntegrityError: return None except sqlite3.Error as exc: raise RuntimeError(f"create_user_db_error: {exc}") from exc def verify_user(self, username: str, password: str) -> dict | None: try: with self._conn() as c: row = c.execute( """ SELECT id, username, password_hash, password_salt FROM users WHERE username=? AND deleted_at IS NULL """, (username,), ).fetchone() except sqlite3.Error as exc: raise RuntimeError(f"verify_user_db_error: {exc}") from exc if not row: return None calc = self._hash_password(password, row["password_salt"]) if not hmac.compare_digest(calc, row["password_hash"]): return None return {"id": int(row["id"]), "username": row["username"]} def change_password(self, user_id: int, old_password: str, new_password: str) -> bool: with self._conn() as c: row = c.execute( "SELECT password_hash, password_salt FROM users WHERE id=?", (user_id,), ).fetchone() if not row: return False calc_old = self._hash_password(old_password, row["password_salt"]) if not hmac.compare_digest(calc_old, row["password_hash"]): return False new_salt = secrets.token_hex(16) new_hash = self._hash_password(new_password, new_salt) c.execute( "UPDATE users SET password_hash=?, password_salt=? WHERE id=?", (new_hash, new_salt, user_id), ) return True def reset_password_by_username(self, username: str, reset_code: str, new_password: str) -> bool: uname = (username or "").strip() rcode = (reset_code or "").strip() if not uname: return False with self._conn() as c: row = c.execute( """ SELECT id, reset_code_hash, reset_code_salt FROM users WHERE username=? AND deleted_at IS NULL """, (uname,), ).fetchone() if not row: return False if not rcode: return False calc = self._hash_reset_code(rcode, row["reset_code_salt"] or "") if not hmac.compare_digest(calc, row["reset_code_hash"] or ""): return False new_salt = secrets.token_hex(16) new_hash = self._hash_password(new_password, new_salt) c.execute( "UPDATE users SET password_hash=?, password_salt=? WHERE id=?", (new_hash, new_salt, int(row["id"])), ) return True def create_session(self, user_id: int, ttl_seconds: int = 7 * 24 * 3600) -> str: token = secrets.token_urlsafe(32) token_hash = self._hash_token(token) now = int(time.time()) exp = now + max(600, int(ttl_seconds)) with self._conn() as c: c.execute( "INSERT OR REPLACE INTO sessions(token_hash, user_id, expires_at, created_at) VALUES (?, ?, ?, ?)", (token_hash, user_id, exp, now), ) return token def delete_session(self, token: str) -> None: if not token: return with self._conn() as c: c.execute("DELETE FROM sessions WHERE token_hash=?", (self._hash_token(token),)) def delete_sessions_by_user(self, user_id: int) -> None: with self._conn() as c: c.execute("DELETE FROM sessions WHERE user_id=?", (user_id,)) def get_user_by_session(self, token: str) -> dict | None: if not token: return None now = int(time.time()) th = self._hash_token(token) with self._conn() as c: c.execute("DELETE FROM sessions WHERE expires_at < ?", (now,)) row = c.execute( """ SELECT u.id, u.username FROM sessions s JOIN users u ON u.id=s.user_id WHERE s.token_hash=? AND s.expires_at>=? AND u.deleted_at IS NULL """, (th, now), ).fetchone() if not row: return None return {"id": int(row["id"]), "username": row["username"]} def delete_user_logically(self, user_id: int, password: str, reset_code: str) -> bool: now = int(time.time()) with self._conn() as c: row = c.execute( """ SELECT id, password_hash, password_salt, reset_code_hash, reset_code_salt FROM users WHERE id=? AND deleted_at IS NULL """, (user_id,), ).fetchone() if not row: return False calc_pwd = self._hash_password(password or "", row["password_salt"] or "") if not hmac.compare_digest(calc_pwd, row["password_hash"] or ""): return False calc_reset = self._hash_reset_code(reset_code or "", row["reset_code_salt"] or "") if not hmac.compare_digest(calc_reset, row["reset_code_hash"] or ""): return False c.execute("DELETE FROM sessions WHERE user_id=?", (user_id,)) c.execute("DELETE FROM wechat_accounts WHERE user_id=?", (user_id,)) c.execute("DELETE FROM ai_models WHERE user_id=?", (user_id,)) c.execute("DELETE FROM user_prefs WHERE user_id=?", (user_id,)) c.execute("DELETE FROM wechat_bindings WHERE user_id=?", (user_id,)) c.execute( "UPDATE users SET deleted_at=?, username=username || '#deleted' || ? WHERE id=?", (now, str(now), user_id), ) return True def save_wechat_binding( self, user_id: int, appid: str, secret: str, author: str = "", thumb_media_id: str = "", thumb_image_path: str = "", ) -> None: now = int(time.time()) with self._conn() as c: c.execute( """ INSERT INTO wechat_bindings(user_id, appid, secret, author, thumb_media_id, thumb_image_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET appid=excluded.appid, secret=excluded.secret, author=excluded.author, thumb_media_id=excluded.thumb_media_id, thumb_image_path=excluded.thumb_image_path, updated_at=excluded.updated_at """, (user_id, appid, secret, author, thumb_media_id, thumb_image_path, now), ) def get_wechat_binding(self, user_id: int) -> dict | None: return self.get_active_wechat_binding(user_id) def list_wechat_bindings(self, user_id: int) -> list[dict]: with self._conn() as c: rows = c.execute( """ SELECT id, account_name, appid, author, thumb_media_id, thumb_image_path, updated_at FROM wechat_accounts WHERE user_id=? ORDER BY updated_at DESC, id DESC """, (user_id,), ).fetchall() pref = c.execute( "SELECT active_wechat_account_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() active_id = int(pref["active_wechat_account_id"]) if pref and pref["active_wechat_account_id"] else None out: list[dict] = [] for r in rows: out.append( { "id": int(r["id"]), "account_name": r["account_name"] or "", "appid": r["appid"] or "", "author": r["author"] or "", "thumb_media_id": r["thumb_media_id"] or "", "thumb_image_path": r["thumb_image_path"] or "", "updated_at": int(r["updated_at"] or 0), "active": int(r["id"]) == active_id, } ) return out def add_wechat_binding( self, user_id: int, account_name: str, appid: str, secret: str, author: str = "", thumb_media_id: str = "", thumb_image_path: str = "", ) -> dict: now = int(time.time()) name = account_name.strip() or f"公众号{now % 10000}" with self._conn() as c: try: cur = c.execute( """ INSERT INTO wechat_accounts (user_id, account_name, appid, secret, author, thumb_media_id, thumb_image_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, (user_id, name, appid, secret, author, thumb_media_id, thumb_image_path, now), ) except sqlite3.IntegrityError: name = f"{name}-{now % 1000}" cur = c.execute( """ INSERT INTO wechat_accounts (user_id, account_name, appid, secret, author, thumb_media_id, thumb_image_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, (user_id, name, appid, secret, author, thumb_media_id, thumb_image_path, now), ) aid = int(cur.lastrowid) c.execute( """ INSERT INTO user_prefs(user_id, active_wechat_account_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_wechat_account_id=excluded.active_wechat_account_id, updated_at=excluded.updated_at """, (user_id, aid, now), ) return {"id": aid, "account_name": name} def switch_active_wechat_binding(self, user_id: int, account_id: int) -> bool: now = int(time.time()) with self._conn() as c: row = c.execute( "SELECT id FROM wechat_accounts WHERE id=? AND user_id=?", (account_id, user_id), ).fetchone() if not row: return False c.execute( """ INSERT INTO user_prefs(user_id, active_wechat_account_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_wechat_account_id=excluded.active_wechat_account_id, updated_at=excluded.updated_at """, (user_id, account_id, now), ) return True def get_active_wechat_binding(self, user_id: int) -> dict | None: with self._conn() as c: pref = c.execute( "SELECT active_wechat_account_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() aid = int(pref["active_wechat_account_id"]) if pref and pref["active_wechat_account_id"] else None row = None if aid: row = c.execute( """ SELECT id, account_name, appid, secret, author, thumb_media_id, thumb_image_path, updated_at FROM wechat_accounts WHERE id=? AND user_id=? """, (aid, user_id), ).fetchone() if not row: row = c.execute( """ SELECT id, account_name, appid, secret, author, thumb_media_id, thumb_image_path, updated_at FROM wechat_accounts WHERE user_id=? ORDER BY updated_at DESC, id DESC LIMIT 1 """, (user_id,), ).fetchone() if row: c.execute( """ INSERT INTO user_prefs(user_id, active_wechat_account_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_wechat_account_id=excluded.active_wechat_account_id, updated_at=excluded.updated_at """, (user_id, int(row["id"]), int(time.time())), ) if not row: return None return { "id": int(row["id"]), "account_name": row["account_name"] or "", "appid": row["appid"] or "", "secret": row["secret"] or "", "author": row["author"] or "", "thumb_media_id": row["thumb_media_id"] or "", "thumb_image_path": row["thumb_image_path"] or "", "updated_at": int(row["updated_at"] or 0), } def list_ai_models(self, user_id: int) -> list[dict]: with self._conn() as c: rows = c.execute( """ SELECT id, model_name, base_url, model, timeout_sec, max_output_tokens, max_retries, updated_at FROM ai_models WHERE user_id=? ORDER BY updated_at DESC, id DESC """, (user_id,), ).fetchall() pref = c.execute( "SELECT active_ai_model_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() active_id = int(pref["active_ai_model_id"]) if pref and pref["active_ai_model_id"] else None out: list[dict] = [] for r in rows: out.append( { "id": int(r["id"]), "model_name": r["model_name"] or "", "base_url": r["base_url"] or "", "model": r["model"] or "", "timeout_sec": float(r["timeout_sec"] or 120.0), "max_output_tokens": int(r["max_output_tokens"] or 8192), "max_retries": int(r["max_retries"] or 0), "updated_at": int(r["updated_at"] or 0), "active": int(r["id"]) == active_id, } ) return out def add_ai_model( self, user_id: int, model_name: str, api_key: str, base_url: str, model: str, timeout_sec: float = 120.0, max_output_tokens: int = 8192, max_retries: int = 0, ) -> dict: now = int(time.time()) name = model_name.strip() or f"模型{now % 10000}" with self._conn() as c: try: cur = c.execute( """ INSERT INTO ai_models (user_id, model_name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (user_id, name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, now), ) except sqlite3.IntegrityError: name = f"{name}-{now % 1000}" cur = c.execute( """ INSERT INTO ai_models (user_id, model_name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (user_id, name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, now), ) aid = int(cur.lastrowid) c.execute( """ INSERT INTO user_prefs(user_id, active_ai_model_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_ai_model_id=excluded.active_ai_model_id, updated_at=excluded.updated_at """, (user_id, aid, now), ) return {"id": aid, "model_name": name} def switch_active_ai_model(self, user_id: int, model_id: int) -> bool: now = int(time.time()) with self._conn() as c: row = c.execute( "SELECT id FROM ai_models WHERE id=? AND user_id=?", (model_id, user_id), ).fetchone() if not row: return False c.execute( """ INSERT INTO user_prefs(user_id, active_ai_model_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_ai_model_id=excluded.active_ai_model_id, updated_at=excluded.updated_at """, (user_id, model_id, now), ) return True def delete_ai_model(self, user_id: int, model_id: int) -> bool: now = int(time.time()) with self._conn() as c: row = c.execute( "SELECT id FROM ai_models WHERE id=? AND user_id=?", (model_id, user_id), ).fetchone() if not row: return False c.execute("DELETE FROM ai_models WHERE id=? AND user_id=?", (model_id, user_id)) pref = c.execute( "SELECT active_ai_model_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() active_id = int(pref["active_ai_model_id"]) if pref and pref["active_ai_model_id"] else None if active_id == model_id: replacement = c.execute( """ SELECT id FROM ai_models WHERE user_id=? ORDER BY updated_at DESC, id DESC LIMIT 1 """, (user_id,), ).fetchone() replacement_id = int(replacement["id"]) if replacement else None c.execute( """ INSERT INTO user_prefs(user_id, active_ai_model_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_ai_model_id=excluded.active_ai_model_id, updated_at=excluded.updated_at """, (user_id, replacement_id, now), ) return True def get_active_ai_model(self, user_id: int) -> dict | None: with self._conn() as c: pref = c.execute( "SELECT active_ai_model_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() aid = int(pref["active_ai_model_id"]) if pref and pref["active_ai_model_id"] else None row = None if aid: row = c.execute( """ SELECT id, model_name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, updated_at FROM ai_models WHERE id=? AND user_id=? """, (aid, user_id), ).fetchone() if not row: row = c.execute( """ SELECT id, model_name, api_key, base_url, model, timeout_sec, max_output_tokens, max_retries, updated_at FROM ai_models WHERE user_id=? ORDER BY updated_at DESC, id DESC LIMIT 1 """, (user_id,), ).fetchone() if row: c.execute( """ INSERT INTO user_prefs(user_id, active_ai_model_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_ai_model_id=excluded.active_ai_model_id, updated_at=excluded.updated_at """, (user_id, int(row["id"]), int(time.time())), ) if not row: return None return { "id": int(row["id"]), "model_name": row["model_name"] or "", "api_key": row["api_key"] or "", "base_url": row["base_url"] or "", "model": row["model"] or "", "timeout_sec": float(row["timeout_sec"] or 120.0), "max_output_tokens": int(row["max_output_tokens"] or 8192), "max_retries": int(row["max_retries"] or 0), "updated_at": int(row["updated_at"] or 0), } def delete_wechat_binding(self, user_id: int, account_id: int) -> bool: now = int(time.time()) with self._conn() as c: row = c.execute( "SELECT id FROM wechat_accounts WHERE id=? AND user_id=?", (account_id, user_id), ).fetchone() if not row: return False c.execute("DELETE FROM wechat_accounts WHERE id=? AND user_id=?", (account_id, user_id)) pref = c.execute( "SELECT active_wechat_account_id FROM user_prefs WHERE user_id=?", (user_id,), ).fetchone() active_id = int(pref["active_wechat_account_id"]) if pref and pref["active_wechat_account_id"] else None if active_id == account_id: replacement = c.execute( """ SELECT id FROM wechat_accounts WHERE user_id=? ORDER BY updated_at DESC, id DESC LIMIT 1 """, (user_id,), ).fetchone() replacement_id = int(replacement["id"]) if replacement else None c.execute( """ INSERT INTO user_prefs(user_id, active_wechat_account_id, updated_at) VALUES (?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET active_wechat_account_id=excluded.active_wechat_account_id, updated_at=excluded.updated_at """, (user_id, replacement_id, now), ) return True