05 · 持久化层
架构剖析第五篇:SQLite WAL + 文件树的分层存储、迁移系统、路径自愈机制。 对应
ARCHITECTURE.md§6。
§6.1 为什么选 SQLite
| 维度 | 选型 | 理由 |
|---|---|---|
| 数据量 | 单用户,几千~几万行 | 无需 Postgres/MySQL 的并发能力 |
| 运维 | 零运维 | 单文件、无服务进程 |
| 事务 | WAL 模式 | 多进程安全(序列化写者) |
| 隐私 | 本地文件 | 数据不出本机 |
| 并发 | check_same_thread=True + thread-local | 每线程独立连接,无锁竞争 |
§6.2 数据分层
SQLite(workspace/familiars.db)
结构化数据:
| 表 | 用途 |
|---|---|
agents | Agent 元信息 |
dialogues | 对话树索引 |
cron_jobs | Cron 作业 |
schedules | 日程 |
todos | 待办 |
reminders | 提醒 |
watch_jobs | 监控作业 |
skills | 技能 manifest |
approvals | 权限/审批 |
rag_feedback | RAG 反馈 |
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 conn6.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)
| 版本 | 文件 | 类型 | 说明 |
|---|---|---|---|
| v1 | v1__init.sql | SQL | 初始 schema |
| v2 | v2__cron_jobs.sql | SQL | Cron 作业表 |
| v3 | v3__rag_feedback.sql | SQL | RAG 反馈 |
| v4 | v4__memory_category.sql | SQL | 记忆类别 |
| v5 | v5__schedules.sql | SQL | 日程表 |
| v6 | v6__todos.sql | SQL | 待办表 |
| v7 | v7__cron_runs.sql | SQL | Cron 运行记录 |
| v8 | v8__schedules_status.sql | SQL | 日程状态 |
| v9 | v9__reminders.sql | SQL | 提醒表 |
| v10 | v10__proactive_feedback.sql | SQL | 主动消息反馈 |
| v11 | v11__node_origin.sql | SQL | Node origin 字段 |
| v12 | v12__watch_jobs.sql | SQL | 监控作业 |
| v13 | v13__recurrence.sql | SQL | 循环日程 |
| v14 | v14__watch_jobs_metadata.sql | SQL | 监控作业元数据 |
| v15 | v15__agent_turn_counter.sql | SQL | Agent 轮次计数 |
| v16 | v16__schedule_dedup_and_unique.py | Python | 日程去重 + 唯一约束 |
| v17 | v17__cron_last_fired_and_feedback.sql | SQL | Cron 最近触发 + 反馈 |
| v18 | v18__broker.sql | SQL | Broker 层 |
| v19 | v19__broker_workshop.sql | SQL | Broker 工坊 |
| v20 | v20__broker_local_package_dir.py | Python | Broker 本地包目录 |
| v20 | v20__guild.sql | SQL | 公会系统 |
| v21 | v21__broker_identity_assets.sql | SQL | Broker 身份资产 |
| v21 | v21__guild_task_type.sql | SQL | 公会任务类型 |
| v22 | v22__broker_messages.sql | SQL | Broker 消息 |
| v23 | v23__broker_member_caps.sql | SQL | Broker 成员能力 |
| v24 | v24__broker_approvals.sql | SQL | Broker 审批 |
| v25 | v25__flow_core.py | Python | 流核心表 |
| v26 | v26__flow_trace_index.sql | SQL | 流追踪索引 |
| v27 | v27__guild_medal_images.sql | SQL | 公会勋章图片 |
| v28 | v28__broker_author_crl.sql | SQL | Broker 作者 CRL(证书吊销) |
| v29 | v29__broker_trusted_authors.sql | SQL | Broker 可信作者 |
| v30 | v30__broker_room_federation.sql | SQL | Broker 房间联邦 |
| v31 | v31__broker_thread_federation.sql | SQL | Broker 线程联邦 |
| v32 | v32__broker_message_federation.sql | SQL | Broker 消息联邦 |
| v33 | v33__guild_task_federation.sql | SQL | 公会任务联邦 |
| v34 | v34__guild_medal_federation.sql | SQL | 公会勋章联邦 |
| v35 | v35__guild_ledger_federation.sql | SQL | 公会账本联邦 |
| v36 | v36__broker_user_consents.sql | SQL | Broker 用户同意 |
| v37 | v37__guild_scope_approvals.sql | SQL | 公会范围审批 |
| v38 | v38__guild_task_lifecycle.sql | SQL | 公会任务生命周期 |
| v39 | v39__guild_merge_requests.sql | SQL | 公会合并请求 |
| v40 | v40__task_edit_history.sql | SQL | 任务编辑历史 |
| v41 | v41__guild_modification_requests.sql | SQL | 公会修改请求 |
| v41 | v41__message_reactions.py | Python | 消息反应 |
| v42 | v42__guild_experience_reports.sql | SQL | 公会经验报告 |
| v42 | v42__guild_modification_votes.sql | SQL | 公会修改投票 |
| v43 | v43__guild_task_recall_votes.sql | SQL | 公会任务召回投票 |
| v44 | v44__guild_scope_members.sql | SQL | 公会范围成员 |
| v45 | v45__task_message_channels.sql | SQL | 任务消息通道 |
| v46 | v46__guild_task_tags.sql | SQL | 公会任务标签 |
| v47 | v47__demo_accounts.sql | SQL | 演示账户 |
| v48 | v48__flow_pause.sql | SQL | Flow 暂停 |
| v49 | v49__user_auth.sql | SQL | 用户认证 |
| v50 | v50__broker_role_requests.sql | SQL | Broker 角色申请 |
Python 迁移:v16、v20、v25、v41 是 .py 文件——允许复杂数据转换(去重、索引重建等)。迁移器按 v(\d+)__ 前缀提取版本号,同名文件 .sql 和 .py 只取其一(.py 优先)。
§6.5 路径自愈
6.5.1 问题
对话树中的绝对路径在以下情况失效:
- 仓库根漂移 —— 用户移动 repo 文件夹
- 外部目录别名 —— 用户重命名数据文件夹
如果 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-SHA256python
# 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-主动决策与礼物 →
