Files
AgentLab/backend/app/db/base.py
T
ThearchyHelios 2df591d05f 工件库:内容寻址存储、节点输出与工具快照落盘、运行清单哈希
事件流里的输出是给画布看的截断预览,出具溯源需要完整证据。
新增 data/artifacts/ 内容寻址工件库(sha256 即地址,取回时复验哈希,
篡改即报错);节点完整输出与工具调用快照(query+结果集)自动落盘,
事件只携带工件 id。运行终态对全部事件计算清单哈希存入 Run,
事后对事件流的增删改会与它对不上。

graph_hash 剔除 viewport 后计算——挪画布不改变图的身份。
Run/Workflow/WorkflowVersion/Approval 预置分级与治理列,
init_db 附带 SQLite 轻量列迁移。
2026-09-14 10:10:20 +08:00

96 lines
3.2 KiB
Python

from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Any, AsyncIterator
from sqlalchemy import JSON, DateTime, MetaData, event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from app.core.config import settings
NAMING_CONVENTION = {
"ix": "ix_%(column_0_label)s",
"uq": "uq_%(table_name)s_%(column_0_name)s",
"fk": "fk_%(table_name)s_%(column_0_name)s",
"pk": "pk_%(table_name)s",
}
def utcnow() -> datetime:
return datetime.now(timezone.utc)
def new_id() -> str:
return uuid.uuid4().hex
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=NAMING_CONVENTION)
# 图定义、节点配置、事件负载都是任意形状的 JSON,统一映射到 SQLite 的 JSON 列
type_annotation_map = {dict[str, Any]: JSON, list[Any]: JSON}
class TimestampMixin:
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=utcnow, onupdate=utcnow
)
engine = create_async_engine(settings.db_url, echo=False, future=True)
@event.listens_for(engine.sync_engine, "connect")
def _set_sqlite_pragma(dbapi_conn, _record): # pragma: no cover - 驱动回调
"""WAL + 外键约束。WAL 让读写不互相阻塞,事件写入和前端轮询能并行。"""
cur = dbapi_conn.cursor()
cur.execute("PRAGMA journal_mode=WAL")
cur.execute("PRAGMA foreign_keys=ON")
cur.execute("PRAGMA busy_timeout=5000")
cur.close()
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
async def get_session() -> AsyncIterator[AsyncSession]:
"""FastAPI 依赖注入用的会话。"""
async with SessionLocal() as session:
yield session
# 轻量迁移:create_all 只建新表,不给已有表加列。SQLite 的 ADD COLUMN
# 便宜且带默认值回填,够用;等真需要改列类型再考虑 alembic。
_COLUMN_MIGRATIONS: list[tuple[str, str, str]] = [
("runs", "run_class", "TEXT DEFAULT 'exploratory'"),
("runs", "version", "INTEGER"),
("runs", "version_hash", "VARCHAR(64)"),
("runs", "manifest_hash", "VARCHAR(64)"),
("runs", "started_by", "VARCHAR(100)"),
("workflows", "status", "TEXT DEFAULT 'draft'"),
("workflows", "published_version", "INTEGER"),
("workflows", "published_by", "VARCHAR(100)"),
("workflow_versions", "graph_hash", "VARCHAR(64)"),
("approvals", "resolved_by", "VARCHAR(100)"),
]
def _migrate(conn) -> None: # pragma: no cover - 同步回调
from sqlalchemy import text
for table, column, ddl in _COLUMN_MIGRATIONS:
cols = {row[1] for row in conn.execute(text(f"PRAGMA table_info({table})"))}
if cols and column not in cols:
conn.execute(text(f"ALTER TABLE {table} ADD COLUMN {column} {ddl}"))
async def init_db() -> None:
from app.db import models # noqa: F401 确保模型已注册到 metadata
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
await conn.run_sync(_migrate)