Skip to content

05 · 持久化层 ​

架构剖析第五篇:SQLite WAL + 文件树的分层存储、迁移系统、路径自愈机制。 对应 ARCHITECTURE.md §6。


§6.1 为什么选 SQLite ​

维度选型理由
数据量单用户,几千~几万行无需 Postgres/MySQL 的并发能力
运维零运维单文件、无服务进程
事务WAL 模式多进程安全(序列化写者)
隐私本地文件数据不出本机
并发check_same_thread=True + thread-local每线程独立连接,无锁竞争

§6.2 数据分层 ​

SQLite(workspace/familiars.db) ​

结构化数据:

表用途
agentsAgent 元信息
dialogues对话树索引
cron_jobsCron 作业
schedules日程
todos待办
reminders提醒
watch_jobs监控作业
skills技能 manifest
approvals权限/审批
rag_feedbackRAG 反馈
proactive_feedback主动消息反馈
broker_*Broker 元(v18+)
guild_*公会(v20+)
flow_*流实例(v25+)

文件树(workspace/) ​

workspace/
├── familiars.db                    # SQLite 主数据库
├── .bridge_token                   # Bridge 认证 token
├── .bridge.port                    # Bridge 实际端口
├── .bridge.pid                     # daemon pidfile
├── .bridge.log                     # daemon 日志(rotating)
├── .bridge.crt                     # 自签 HTTPS 证书
├── .bridge.key                     # 证书私钥
├── .salt                           # 加密盐(crypto.py)
├── .paths_aliases.json             # 外部目录别名
├── SECRET.json                     # 加密后的凭证
├── gpu_tier.json                   # GPU 分级缓存
├── info.json                       # OS 信息
├── USER.md                         # 用户画像(RAG 目标)
├── IDENTITY.md                     # 用户身份
├── AGENTS.md                       # 项目级 Agent 规则
├── tools.json                      # 工具 schema 合并源
├── user/                           # user scope 记忆
│   ├── profile.md
│   ├── preference.md
│   ├── relation.md
│   ├── knowledge.md
│   └── event.md
├── agents/
│   └── agent_<id>/
│       ├── IDENTITY.md             # Agent 身份
│       ├── dialogue_<n>/
│       │   ├── dialogue.json       # 对话树(Node 列表)
│       │   ├── turn_<k>/
│       │   │   └── plan.json       # 工具计划
│       │   ├── step_<k>.json       # 步骤记录
│       │   ├── dialogue/           # dialogue scope 记忆
│       │   │   ├── preference.md
│       │   │   ├── relation.md
│       │   │   ├── knowledge.md
│       │   │   ├── event.md
│       │   │   ├── state.md
│       │   │   └── directive.md
│       │   └── ephemeral/          # ephemeral scope 记忆
│       │       ├── state.md
│       │       └── directive.md
│       ├── identity/               # identity scope 记忆
│       │   ├── preference.md
│       │   ├── procedure.md
│       │   └── directive.md
│       ├── voice.wav               # 声音参考(数字人)
│       ├── client_portraits/       # 客户端肖像
│       └── reports/                # 报告(已弃用 → manager_review skill)
├── manager/                        # Agent 0(manager)数据
├── reminders/
│   └── inbox.jsonl                 # 提醒事件(SSE 流源)
├── rewriter/
│   └── prompt.md                   # 改写 prompt
├── reports/                        # 报告模板 + 系统 prompt
├── skills/                         # 技能定义
├── tool_skills/                    # 工具技能
├── external_kb/                    # 外部知识库
├── manager/brokers/                # 安装的 broker 应用
│   └── <broker_id>/
│       ├── manifest.json
│       ├── cover.png
│       └── ...
└── manager/flows/                  # 提交的流模板
    └── <template_id>/
        ├── template.json
        ├── layout.json
        ├── renderers/              # 自定义字段渲染器 .vue
        └── docs/                   # 设计/开发文档

§6.3 SQLite 连接管理 ​

6.3.1 connect() 工厂 ​

python
# src/storage/db.py

def connect(db_path: Optional[str] = None, *, run_migrations: bool = True) -> sqlite3.Connection:
    """Return a configured SQLite connection. Cached per-thread per-path."""
    path = db_path or get_default_db_path()
    cache: dict = getattr(_thread_local, "conns", None) or {}
    if path in cache:
        return cache[path]

    conn = sqlite3.connect(
        path,
        isolation_level=None,     # autocommit; 手动 BEGIN IMMEDIATE
        timeout=30.0,             # 等 writer 锁最多 30s
        check_same_thread=True,   # 每线程独立连接
    )
    conn.row_factory = sqlite3.Row
    _configure(conn)
    if run_migrations:
        _run_migrations(conn)

    cache[path] = conn
    _thread_local.conns = cache
    return conn

6.3.2 PRAGMA 配置 ​

python
def _configure(conn: sqlite3.Connection) -> None:
    cur = conn.cursor()
    cur.execute("PRAGMA journal_mode = WAL")        # Write-Ahead Logging
    cur.execute("PRAGMA synchronous = NORMAL")       # 平衡性能/安全
    cur.execute("PRAGMA foreign_keys = ON")          # 外键约束
    cur.execute("PRAGMA busy_timeout = 5000")        # 忙等待 5s
    cur.execute("PRAGMA temp_store = MEMORY")        # 临时表用内存

6.3.3 DB 路径解析 ​

python
def get_default_db_path() -> str:
    explicit = os.getenv("FAMILIARS_DB_PATH")
    if explicit:
        return explicit
    workspace = os.getenv("WORKSPACE_ROUTE") or os.getcwd()
    return str(Path(workspace) / "workspace" / "familiars.db")

FAMILIARS_DB_PATH=:memory: 常用于测试。

§6.4 迁移系统 ​

6.4.1 迁移器 ​

python
# src/storage/db.py
_MIGRATIONS_DIR = Path(__file__).parent / "migrations"
_MIGRATION_NAME_RE = re.compile(r"^v(\d+)__.+\.(sql|py)$")

def _run_migrations(conn: sqlite3.Connection) -> None:
    """按版本号顺序执行 pending migrations。"""

6.4.2 迁移清单(v1–v50) ​

版本文件类型说明
v1v1__init.sqlSQL初始 schema
v2v2__cron_jobs.sqlSQLCron 作业表
v3v3__rag_feedback.sqlSQLRAG 反馈
v4v4__memory_category.sqlSQL记忆类别
v5v5__schedules.sqlSQL日程表
v6v6__todos.sqlSQL待办表
v7v7__cron_runs.sqlSQLCron 运行记录
v8v8__schedules_status.sqlSQL日程状态
v9v9__reminders.sqlSQL提醒表
v10v10__proactive_feedback.sqlSQL主动消息反馈
v11v11__node_origin.sqlSQLNode origin 字段
v12v12__watch_jobs.sqlSQL监控作业
v13v13__recurrence.sqlSQL循环日程
v14v14__watch_jobs_metadata.sqlSQL监控作业元数据
v15v15__agent_turn_counter.sqlSQLAgent 轮次计数
v16v16__schedule_dedup_and_unique.pyPython日程去重 + 唯一约束
v17v17__cron_last_fired_and_feedback.sqlSQLCron 最近触发 + 反馈
v18v18__broker.sqlSQLBroker 层
v19v19__broker_workshop.sqlSQLBroker 工坊
v20v20__broker_local_package_dir.pyPythonBroker 本地包目录
v20v20__guild.sqlSQL公会系统
v21v21__broker_identity_assets.sqlSQLBroker 身份资产
v21v21__guild_task_type.sqlSQL公会任务类型
v22v22__broker_messages.sqlSQLBroker 消息
v23v23__broker_member_caps.sqlSQLBroker 成员能力
v24v24__broker_approvals.sqlSQLBroker 审批
v25v25__flow_core.pyPython流核心表
v26v26__flow_trace_index.sqlSQL流追踪索引
v27v27__guild_medal_images.sqlSQL公会勋章图片
v28v28__broker_author_crl.sqlSQLBroker 作者 CRL(证书吊销)
v29v29__broker_trusted_authors.sqlSQLBroker 可信作者
v30v30__broker_room_federation.sqlSQLBroker 房间联邦
v31v31__broker_thread_federation.sqlSQLBroker 线程联邦
v32v32__broker_message_federation.sqlSQLBroker 消息联邦
v33v33__guild_task_federation.sqlSQL公会任务联邦
v34v34__guild_medal_federation.sqlSQL公会勋章联邦
v35v35__guild_ledger_federation.sqlSQL公会账本联邦
v36v36__broker_user_consents.sqlSQLBroker 用户同意
v37v37__guild_scope_approvals.sqlSQL公会范围审批
v38v38__guild_task_lifecycle.sqlSQL公会任务生命周期
v39v39__guild_merge_requests.sqlSQL公会合并请求
v40v40__task_edit_history.sqlSQL任务编辑历史
v41v41__guild_modification_requests.sqlSQL公会修改请求
v41v41__message_reactions.pyPython消息反应
v42v42__guild_experience_reports.sqlSQL公会经验报告
v42v42__guild_modification_votes.sqlSQL公会修改投票
v43v43__guild_task_recall_votes.sqlSQL公会任务召回投票
v44v44__guild_scope_members.sqlSQL公会范围成员
v45v45__task_message_channels.sqlSQL任务消息通道
v46v46__guild_task_tags.sqlSQL公会任务标签
v47v47__demo_accounts.sqlSQL演示账户
v48v48__flow_pause.sqlSQLFlow 暂停
v49v49__user_auth.sqlSQL用户认证
v50v50__broker_role_requests.sqlSQLBroker 角色申请

Python 迁移:v16、v20、v25、v41 是 .py 文件——允许复杂数据转换(去重、索引重建等)。迁移器按 v(\d+)__ 前缀提取版本号,同名文件 .sql 和 .py 只取其一(.py 优先)。

§6.5 路径自愈 ​

6.5.1 问题 ​

对话树中的绝对路径在以下情况失效:

  1. 仓库根漂移 —— 用户移动 repo 文件夹
  2. 外部目录别名 —— 用户重命名数据文件夹

如果 DialogueManager._load_existing_dialogues 先于路径修复运行,LLM 的下一轮会读取过期路径作为上下文,导致无限循环 ls 或写文件到幽灵位置。

6.5.2 解决方案 ​

src/bridge/app.py 在 import 时、路由挂载前调用 align_all_paths():

python
# src/base/paths_align.py
def align_all_paths() -> dict:
    """处理两种路径漂移。
    
    返回 {"files_touched": int, "total_hits": int, "aliases_applied": [...]}
    """

机制 1:仓库根漂移(自动检测)

比较每个 dialogue.path 前缀与当前 _repo_root():

  • 不匹配 → 自动重写 dialogue.json + turn_*/plan.json + step_*.json
  • 记录到 workspace/.paths_aliases.json

机制 2:外部目录别名(手动注册)

bash
python -m src.tools.paths_migrate <old> <new> --record
  • 先 --dry-run 预览
  • --record 写入 .paths_aliases.json
  • 每次启动回放
  • 每个被改写的文件备份为 <path>.bak_<ts>

覆盖范围:

  • dialogue.json(节点内容 + metadata.tool_calls 参数/结果 + 附件)
  • turn_*/plan.json + step_*.json

永不改写:

  • .bridge.log、audit.log —— 保留真实历史记录

幂等性:0-hit 文件被跳过,稳定启动零开销。

§6.6 加密存储 ​

6.6.1 SECRET.json ​

凭证加密后存于 workspace/SECRET.json。加密链:

hostname + username + salt_bytes
    → HKDF (SHA-256, info="familiars-secrets-v1")
    → 32-byte Fernet key
    → AES-128-CBC + HMAC-SHA256
python
# src/base/crypto.py
_SALT_FILENAME = ".salt"
_KEY_INFO = b"familiars-secrets-v1"

def _derive_fernet_key() -> bytes:
    salt = _load_or_create_salt()   # os.urandom(32)
    identity = f"{platform.node()}|{os.getenv('USER', ...)}"
    hkdf = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=_KEY_INFO)
    raw_key = hkdf.derive(identity.encode("utf-8"))
    return base64.urlsafe_b64encode(raw_key)

安全模型:

  • salt 随机生成、不共享 → 没有机器身份 + salt 无法重建密钥
  • salt 文件本身无机器身份无用
  • 机器身份无 salt 无用
  • HKDF info 字符串 "familiars-secrets-v1" 是版本化密码原语 → 改它会使所有已存密钥失效

下一篇:06-主动决策与礼物 →

Familiars · 多 Agent AI 桌面应用