From 1442f08d250caee1c6441e0b13b250415c5dbe5e Mon Sep 17 00:00:00 2001 From: hibiki233i Date: Wed, 22 Apr 2026 14:08:12 +0800 Subject: [PATCH 1/4] Stabilize packaged SQLite knowledge base initialization --- astrbot/core/db/__init__.py | 15 ++++++++++--- .../db/vec_db/faiss_impl/document_storage.py | 22 +++++++++++++++++-- astrbot/core/knowledge_base/kb_db_sqlite.py | 2 ++ main.py | 6 ++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 1800887fb0..0dcec9ccdb 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -6,6 +6,7 @@ from deprecated import deprecated from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.pool import NullPool from astrbot.core.db.po import ( ApiKey, @@ -42,11 +43,19 @@ def __init__(self) -> None: # write bursts from concurrent agent/metrics/session operations. is_sqlite = "sqlite" in self.DATABASE_URL 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..36b0aae03e 100644 --- a/astrbot/core/db/vec_db/faiss_impl/document_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/document_storage.py @@ -7,6 +7,7 @@ from sqlalchemy import Column, Text, bindparam from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool from sqlmodel import Field, MetaData, SQLModel, col, func, select, text from astrbot.core import logger @@ -60,8 +61,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 +94,23 @@ 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 using raw SQL for packaged-runtime stability.""" + await executor.execute( + text( + """ + CREATE TABLE IF NOT EXISTS documents ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + doc_id TEXT NOT NULL, + text TEXT NOT NULL, + metadata TEXT, + created_at DATETIME, + updated_at DATETIME + ) + """, + ), + ) + async def _initialize_fts5(self, executor) -> None: try: await self._create_fts5_table(executor, if_not_exists=True) @@ -197,6 +214,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..b2662756e0 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 @@ -42,6 +43,7 @@ def __init__(self, db_path: str | None = None) -> None: 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) From 72f2e5108fb03fedfe8116b4be7f5e3eb3376d6e Mon Sep 17 00:00:00 2001 From: hibiki233i <75414211+hibiki233i@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:29:01 +0800 Subject: [PATCH 2/4] Apply suggestion from @sourcery-ai[bot] Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- astrbot/core/knowledge_base/kb_db_sqlite.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/astrbot/core/knowledge_base/kb_db_sqlite.py b/astrbot/core/knowledge_base/kb_db_sqlite.py index b2662756e0..8063cf35ab 100644 --- a/astrbot/core/knowledge_base/kb_db_sqlite.py +++ b/astrbot/core/knowledge_base/kb_db_sqlite.py @@ -41,8 +41,6 @@ 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, ) From 28cbccb2e640475e480d1515f50676bb9ae568a3 Mon Sep 17 00:00:00 2001 From: hibiki233i <75414211+hibiki233i@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:32:44 +0800 Subject: [PATCH 3/4] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- astrbot/core/db/vec_db/faiss_impl/document_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 36b0aae03e..f27d3c942b 100644 --- a/astrbot/core/db/vec_db/faiss_impl/document_storage.py +++ b/astrbot/core/db/vec_db/faiss_impl/document_storage.py @@ -101,7 +101,7 @@ async def _ensure_documents_table(self, executor) -> None: """ CREATE TABLE IF NOT EXISTS documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, - doc_id TEXT NOT NULL, + doc_id TEXT NOT NULL UNIQUE, text TEXT NOT NULL, metadata TEXT, created_at DATETIME, From b45a02df9c7cb8212b39998fd3c0959c8d50a4e9 Mon Sep 17 00:00:00 2001 From: hibiki233i Date: Thu, 23 Apr 2026 14:50:38 +0800 Subject: [PATCH 4/4] fix: updating database URL handling and ensuring unique document IDs --- astrbot/core/db/__init__.py | 4 ++- .../db/vec_db/faiss_impl/document_storage.py | 29 ++++++++++++------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/astrbot/core/db/__init__.py b/astrbot/core/db/__init__.py index 0dcec9ccdb..0fa071916d 100644 --- a/astrbot/core/db/__init__.py +++ b/astrbot/core/db/__init__.py @@ -5,6 +5,7 @@ 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 @@ -41,7 +42,8 @@ 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, 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 f27d3c942b..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,9 +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 @@ -35,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) @@ -95,20 +97,25 @@ async def initialize(self) -> None: await conn.commit() async def _ensure_documents_table(self, executor) -> None: - """Create the document table using raw SQL for packaged-runtime stability.""" - await executor.execute( + """Create the document table from the SQLModel definition.""" + result = await executor.execute( text( """ - CREATE TABLE IF NOT EXISTS documents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - doc_id TEXT NOT NULL UNIQUE, - text TEXT NOT NULL, - metadata TEXT, - created_at DATETIME, - updated_at DATETIME - ) + 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: