"""SQLite 账号库:建表、读写、登录态字段。""" import json import sqlite3 import time from datetime import datetime from typing import Optional from am_platforms import PLATFORM_URLS, _PLATFORM_NAME_CN from am_runtime_paths import get_db_path # SQLite 无独立 DATETIME 类型:时间统一存 INTEGER Unix 秒(UTC),查询/JSON 再转 ISO8601。 # 下列 DDL 含注释,会原样写入 sqlite_master,便于 Navicat / DBeaver 等查看建表语句。 ACCOUNTS_TABLE_SQL = """ /* 多平台账号表:与本地 Chromium/Edge 用户数据目录(profile)绑定,供发布类技能读取登录态 */ CREATE TABLE accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, -- 账号主键(自增) name TEXT NOT NULL, -- 展示名称,如「搜狐1号」 platform TEXT NOT NULL, -- 平台标识,与内置 PLATFORMS 键一致,如 sohu phone TEXT, -- 可选绑定手机号 profile_dir TEXT, -- Playwright 用户数据目录(绝对路径);默认可读结构 profiles/<平台展示名>/<手机号>/ url TEXT, -- 平台入口或登录页 URL login_status INTEGER NOT NULL DEFAULT 0, -- 是否已登录:0 否 1 是(由脚本校验后写入) last_login_at INTEGER, -- 最近一次登录成功时间,Unix 秒 UTC;未登录过为 NULL extra_json TEXT, -- 扩展字段 JSON created_at INTEGER NOT NULL, -- 记录创建时间,Unix 秒 UTC updated_at INTEGER NOT NULL -- 记录最后更新时间,Unix 秒 UTC ); """ def _now_unix() -> int: return int(time.time()) def _unix_to_iso_local(ts: Optional[int]) -> Optional[str]: """对外 JSON:本地时区 ISO8601 字符串,便于人读。""" if ts is None: return None try: return datetime.fromtimestamp(int(ts)).isoformat(timespec="seconds") except (ValueError, OSError, OverflowError): return None def get_conn(): return sqlite3.connect(get_db_path()) def init_db(): conn = get_conn() try: cur = conn.cursor() cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='accounts'") if not cur.fetchone(): cur.executescript(ACCOUNTS_TABLE_SQL) conn.commit() finally: conn.close() def _normalize_account_id(account_id): if account_id is None: return None s = str(account_id).strip() if s.isdigit(): return int(s) return s def get_account_by_id(account_id): init_db() aid = _normalize_account_id(account_id) conn = get_conn() try: cur = conn.cursor() cur.execute( """ SELECT id, name, platform, phone, profile_dir, url, login_status, last_login_at, extra_json, created_at, updated_at FROM accounts WHERE id = ? """, (aid,), ) row = cur.fetchone() if not row: return None acc = { "id": row[0], "name": row[1], "platform": row[2], "phone": row[3] or "", "profile_dir": (row[4] or "").strip(), "url": row[5] or PLATFORM_URLS.get(row[2], ""), "login_status": int(row[6] or 0), "last_login_at": _unix_to_iso_local(row[7]), "created_at": _unix_to_iso_local(row[9]), "updated_at": _unix_to_iso_local(row[10]), } if row[8]: try: extra = json.loads(row[8]) if isinstance(extra, dict): acc.update(extra) except Exception: pass return acc finally: conn.close() def _default_name_for_platform(platform: str, index: int) -> str: cn = _PLATFORM_NAME_CN.get(platform) if cn: return f"{cn}{index}号" return f"{platform}_{index}" def _mark_login_status(account_id, success: bool): now = _now_unix() conn = get_conn() try: cur = conn.cursor() if success: cur.execute( "UPDATE accounts SET login_status = 1, last_login_at = ?, updated_at = ? WHERE id = ?", (now, now, account_id), ) else: cur.execute( "UPDATE accounts SET login_status = 0, updated_at = ? WHERE id = ?", (now, account_id), ) conn.commit() finally: conn.close()