mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 07:11:57 +00:00
2df591d05f
事件流里的输出是给画布看的截断预览,出具溯源需要完整证据。 新增 data/artifacts/ 内容寻址工件库(sha256 即地址,取回时复验哈希, 篡改即报错);节点完整输出与工具调用快照(query+结果集)自动落盘, 事件只携带工件 id。运行终态对全部事件计算清单哈希存入 Run, 事后对事件流的增删改会与它对不上。 graph_hash 剔除 viewport 后计算——挪画布不改变图的身份。 Run/Workflow/WorkflowVersion/Approval 预置分级与治理列, init_db 附带 SQLite 轻量列迁移。
96 lines
3.2 KiB
Python
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)
|