FinBuddy SQLCipher加密与前端重构

· FinBuddy

今日进展

  1. 数据库加密改造:新增 SQLCipher 加密引擎(encrypted_engine.py),替换原来的 aiosqlite 方案,db.py 从异步引擎改为同步加密引擎 + asyncio.to_thread 异步封装
  2. 密钥管理重构:新增 db_crypto.py(KeyHolder 单例)和 db_keystore.py(密钥存储),密钥从用户密码 + salt 通过 PBKDF2 派生,仅存内存
  3. EncryptedStorage 密钥链路改造:security.py 不再从文件读取密钥,改为从 KeyHolder 获取,KeyHolder 不可用时降级为文件读取(兼容未加密启动)
  4. 新增数据库引导和迁移:bootstrap_db.py(首次启动初始化)和 migration.py(加密迁移脚本)
  5. 自选股排序功能:watchlist_service.py 新增 sort_orders 参数,支持批量添加时指定排序
  6. 知识库分类增加 sample_queries 字段:schemas.py 新增列,用于存储分类示例查询
  7. 前端全面优化:Chat 组件(消息列表/新闻分析/步骤进度)、Credit 面板(支付弹窗)、通知系统、策略回测、自选股面板等 20+ 组件修改
  8. Skills 更新:TDX 系列(K线/指标/板块/选股)、新闻分析(东方财富/新浪/同花顺源)、研究技能(财务分析/编排器)均有修改
  9. 新增文档:EventBus 架构设计、工作流元能力设计、LLM 元能力与工具清单、数据库定时备份设计

关键代码/伪代码

SQLCipher 加密引擎 — 替换 aiosqlite

# 之前:aiosqlite 异步引擎(明文数据库)
# 问题:aiosqlite 忽略 creator 参数,无法传入 SQLCipher 连接
# 方案:同步 SQLCipher 引擎 + asyncio.to_thread 异步封装

# db.py 改造前
async_engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}")
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession)

# db.py 改造后
from finclaw.data.encrypted_engine import get_encrypted_engine

sync_engine = get_encrypted_engine(db_path)  # SQLCipher 引擎
SyncSessionLocal = sessionmaker(sync_engine, expire_on_commit=False)

# 异步封装:同步引擎 + to_thread
async def get_sync_session():
    session = SyncSessionLocal()
    try:
        yield session
    finally:
        session.close()

# 引擎延迟初始化:首次调用时才创建
# 避免模块导入时就要求密钥

KeyHolder — 密钥派生与管理

# 核心思路:用户密码 → PBKDF2 → SQLCipher 密钥
# 密钥只存在内存中,不落盘

CLASS KeyHolder:
    """密钥持有者 — 单例模式"""

    _instance = None

    @classmethod
    DEF get_instance(cls) -> KeyHolder:
        IF cls._instance IS None:
            RAISE RuntimeError("KeyHolder 未初始化")
        RETURN cls._instance

    @classmethod
    DEF initialize(cls, password: str, salt: bytes):
        # PBKDF2 派生密钥
        key = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode('utf-8'),
            salt,
            iterations=600000  # OWASP 推荐
        )
        cls._instance = cls(key_hex=key.hex())

    DEF get_sqlcipher_key(self) -> str:
        # 返回 SQLCipher 格式的密钥
        RETURN f"x'{self._key_hex}'"

    DEF get_fernet_key(self) -> bytes:
        # 从 SQLCipher 密钥派生 Fernet 密钥(不同用途,不同密钥)
        fernet_raw = hashlib.sha256(
            self._key_hex.encode("utf-8") + b"_fernet"
        ).digest()
        RETURN base64.urlsafe_b64encode(fernet_raw)

EncryptedStorage — 密钥链路改造

# 改造前:密钥从文件读取
DEF _load_key(self):
    RETURN open(self.key_path, "rb").read()

# 改造后:优先从 KeyHolder 获取,降级为文件读取
DEF _load_key(self):
    TRY:
        FROM finclaw.infra.db_crypto IMPORT KeyHolder
        key_holder = KeyHolder.get_instance()
        # 从 SQLCipher 密钥派生 Fernet 密钥
        RETURN key_holder.get_fernet_key()
    EXCEPT Exception:
        # 降级:兼容未加密启动场景
        IF os.path.exists(self.key_path):
            RETURN open(self.key_path, "rb").read()
        RAISE

遇到的问题

  • aiosqlite 不支持 creator 参数:验证了 aiosqlite 会忽略传入的 creator,无法用它连接 SQLCipher。最终放弃异步引擎,改用同步引擎 + asyncio.to_thread,性能差异可忽略(本地 SQLite 本身就是文件 IO)
  • 密钥链路循环依赖:KeyHolder 在 db_crypto.py,EncryptedStorage 在 security.py,db.py 又依赖两者。解决方案:延迟导入(try: from finclaw.infra.db_crypto import KeyHolder),避免模块级循环引用
  • 引擎延迟初始化的时序问题:如果模块导入时就创建引擎,密钥还没准备好。解决方案:引擎延迟到首次 get_sync_sessioninit_db 调用时才创建
  • 前端 20+ 组件同步修改:Chat/Credit/Notification/Strategy/Watchlist 全动了,回归测试压力大。目前靠手动验证核心流程

明日计划

  • SQLCipher 加密端到端测试:验证加密数据库的创建、读写、迁移流程
  • 前端组件回归测试:重点验证 Chat 流式对话、Credit 支付、Watchlist 排序
  • 密钥丢失恢复方案:用户忘记密码时的数据恢复策略