diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 1800887fb0..0fa071916d 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -5,7 +5,9 @@ from dataclasses import dataclass from deprecated import deprecated +from sqlalchemy.engine import make_url from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.pool import NullPool from astrbot.core.db.po import ( ApiKey, @@ -40,13 +42,22 @@ def __init__(self) -> None: # second write is attempted. Setting timeout=30 tells SQLite to # wait up to 30 s for the lock, which is enough to ride out brief # write bursts from concurrent agent/metrics/session operations. - is_sqlite = "sqlite" in self.DATABASE_URL + db_url = make_url(self.DATABASE_URL) + is_sqlite = db_url.get_backend_name() == "sqlite" connect_args = {"timeout": 30} if is_sqlite else {} + engine_kwargs = { + "echo": False, + "future": True, + "connect_args": connect_args, + } + if is_sqlite: + # Keep SQLite async engines off SQLAlchemy's default async queue + # pool so packaged runtimes don't depend on dialect-specific pool + # event support. + engine_kwargs["poolclass"] = NullPool self.engine = create_async_engine( self.DATABASE_URL, - echo=False, - future=True, - connect_args=connect_args, + **engine_kwargs, ) self.AsyncSessionLocal = async_sessionmaker( self.engine, diff --git a/astrbot/core/db/vec_db/faiss_impl/document_storage.py b/astrbot/core/db/vec_db/faiss_impl/document_storage.py index d0310d750a..f2c5ac48f4 100644 --- a/astrbot/core/db/vec_db/faiss_impl/document_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/document_storage.py @@ -5,8 +5,11 @@ from pathlib import Path from sqlalchemy import Column, Text, bindparam +from sqlalchemy.dialects import sqlite from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool +from sqlalchemy.schema import CreateTable from sqlmodel import Field, MetaData, SQLModel, col, func, select, text from astrbot.core import logger @@ -34,7 +37,7 @@ class Document(BaseDocModel, table=True): primary_key=True, sa_column_kwargs={"autoincrement": True}, ) - doc_id: str = Field(nullable=False) + doc_id: str = Field(nullable=False, unique=True) text: str = Field(nullable=False) metadata_: str | None = Field(default=None, sa_column=Column("metadata", Text)) created_at: datetime | None = Field(default=None) @@ -60,8 +63,7 @@ async def initialize(self) -> None: """Initialize the SQLite database and create the documents table if it doesn't exist.""" await self.connect() async with self.engine.begin() as conn: # type: ignore - # Create tables using SQLModel - await conn.run_sync(BaseDocModel.metadata.create_all) + await self._ensure_documents_table(conn) try: await conn.execute( @@ -94,6 +96,28 @@ async def initialize(self) -> None: await self._initialize_fts5(conn) await conn.commit() + async def _ensure_documents_table(self, executor) -> None: + """Create the document table from the SQLModel definition.""" + result = await executor.execute( + text( + """ + SELECT 1 + FROM sqlite_master + WHERE type='table' AND name=:table_name + LIMIT 1 + """, + ), + {"table_name": Document.__tablename__}, + ) + if result.scalar_one_or_none() is not None: + return + + create_table = CreateTable(Document.__table__, if_not_exists=True) # type: ignore[attr-defined] + + await executor.execute( + text(str(create_table.compile(dialect=sqlite.dialect()))) + ) + async def _initialize_fts5(self, executor) -> None: try: await self._create_fts5_table(executor, if_not_exists=True) @@ -197,6 +221,7 @@ async def connect(self) -> None: self.DATABASE_URL, echo=False, future=True, + poolclass=NullPool, ) self.async_session_maker = sessionmaker( self.engine, # type: ignore diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index 6a2cb5e0a8..8063cf35ab 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -4,6 +4,7 @@ from sqlalchemy import delete, func, select, text, update from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.pool import NullPool from sqlmodel import col, desc from astrbot.core import logger @@ -40,8 +41,7 @@ def __init__(self, db_path: str | None = None) -> None: self.engine = create_async_engine( self.DATABASE_URL, echo=False, - pool_pre_ping=True, - pool_recycle=3600, + poolclass=NullPool, ) # 创建会话工厂 diff --git a/main.py b/main.py index 14e0c23a81..f9cc272623 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,7 @@ download_dashboard, get_dashboard_version, ) +from astrbot.core.utils.runtime_env import is_packaged_desktop_runtime # noqa: E402 # 将父目录添加到 sys.path sys.path.append(Path(__file__).parent.as_posix()) @@ -50,7 +51,10 @@ def check_env() -> None: sys.path.insert(0, astrbot_root) site_packages_path = get_astrbot_site_packages_path() - if site_packages_path not in sys.path: + if not is_packaged_desktop_runtime() and site_packages_path not in sys.path: + # Packaged desktop runtime keeps shared plugin dependencies out of the + # global import path so bundled core libraries don't mix with user- + # installed wheels from ~/.astrbot/data/site-packages. sys.path.append(site_packages_path) os.makedirs(get_astrbot_config_path(), exist_ok=True)