neobot-storage 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neobot_storage-1.0.0/PKG-INFO +44 -0
- neobot_storage-1.0.0/README.md +31 -0
- neobot_storage-1.0.0/pyproject.toml +27 -0
- neobot_storage-1.0.0/pyproject.toml.orig +23 -0
- neobot_storage-1.0.0/src/neobot_storage/__init__.py +18 -0
- neobot_storage-1.0.0/src/neobot_storage/_retry.py +90 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/env.py +72 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/script.py.mako +27 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0001_initial_schema.py +81 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0002_lowercase_table_names.py +38 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0003_add_archive_memories.py +43 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0004_add_images_table.py +47 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0005_add_user_profile_refresh_fields.py +32 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0006_add_emojis_table.py +46 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0007_add_creator_images.py +52 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0008_add_user_avatar_analysis.py +27 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0009_add_emoji_use_count.py +27 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0010_add_favorability.py +27 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0011_add_image_source.py +22 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0012_add_scheduled_tasks.py +80 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0017_add_model_usage_records.py +43 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0018_rename_cost_usd_to_cny.py +34 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0019_add_cache_hit_tokens_to_usage.py +35 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0020_add_bilibili_links.py +40 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0021_unique_completed_scheduled_tasks_task_uuid.py +38 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0022_add_maintenance_runs.py +42 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0023_add_creator_image_gallery_no.py +54 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0024_add_creator_image_sequences.py +46 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0025_add_usage_cost_source.py +42 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0026_add_archive_snapshots.py +54 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0027_add_user_avatar_storage.py +58 -0
- neobot_storage-1.0.0/src/neobot_storage/alembic.ini +44 -0
- neobot_storage-1.0.0/src/neobot_storage/backup.py +87 -0
- neobot_storage-1.0.0/src/neobot_storage/engine.py +62 -0
- neobot_storage-1.0.0/src/neobot_storage/models.py +328 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/__init__.py +27 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/archive.py +422 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/archive_snapshot.py +160 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/creator_image.py +329 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/emoji.py +233 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/image.py +185 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/maintenance.py +72 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/memory.py +50 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/message.py +81 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/profile.py +122 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/scheduled_task.py +366 -0
- neobot_storage-1.0.0/src/neobot_storage/repositories/usage.py +128 -0
- neobot_storage-1.0.0/src/neobot_storage/uow.py +77 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: neobot-storage
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Storage layer for NeoBot — SQLAlchemy + Alembic
|
|
5
|
+
Author: wsrsq, tangtian
|
|
6
|
+
Author-email: wsrsq <wsrsq001@163.com>, tangtian <a14b@126.com>
|
|
7
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
8
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
9
|
+
Requires-Dist: alembic>=1.15.0
|
|
10
|
+
Requires-Dist: neobot-contracts
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# neobot-storage
|
|
15
|
+
|
|
16
|
+
SQLAlchemy 2.0 async storage layer for NeoBot. Implements the repository and unit-of-work ports defined in `neobot-contracts`.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from neobot_storage import create_engine, make_uow_factory
|
|
22
|
+
|
|
23
|
+
engine = create_engine("sqlite+aiosqlite:///neobot.db")
|
|
24
|
+
uow_factory = make_uow_factory(engine)
|
|
25
|
+
|
|
26
|
+
async with uow_factory() as uow:
|
|
27
|
+
await uow.messages.save_message(msg)
|
|
28
|
+
await uow.commit()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Migrations
|
|
32
|
+
|
|
33
|
+
Migrations are run automatically on app startup via `run_migrations(db_url)` (absolute path
|
|
34
|
+
is injected, so no database file is created inside the source tree).
|
|
35
|
+
|
|
36
|
+
To run the Alembic CLI manually, always pass an **absolute** database path:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
cd packages/storage/src/neobot_storage
|
|
40
|
+
alembic -x db_url=sqlite+aiosqlite:////absolute/path/to/neobot.db upgrade head
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`alembic.ini` intentionally leaves `sqlalchemy.url` empty; a relative URL there would create a
|
|
44
|
+
stray empty `neobot.db` in the working directory (which must never be committed).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# neobot-storage
|
|
2
|
+
|
|
3
|
+
SQLAlchemy 2.0 async storage layer for NeoBot. Implements the repository and unit-of-work ports defined in `neobot-contracts`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from neobot_storage import create_engine, make_uow_factory
|
|
9
|
+
|
|
10
|
+
engine = create_engine("sqlite+aiosqlite:///neobot.db")
|
|
11
|
+
uow_factory = make_uow_factory(engine)
|
|
12
|
+
|
|
13
|
+
async with uow_factory() as uow:
|
|
14
|
+
await uow.messages.save_message(msg)
|
|
15
|
+
await uow.commit()
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Migrations
|
|
19
|
+
|
|
20
|
+
Migrations are run automatically on app startup via `run_migrations(db_url)` (absolute path
|
|
21
|
+
is injected, so no database file is created inside the source tree).
|
|
22
|
+
|
|
23
|
+
To run the Alembic CLI manually, always pass an **absolute** database path:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd packages/storage/src/neobot_storage
|
|
27
|
+
alembic -x db_url=sqlite+aiosqlite:////absolute/path/to/neobot.db upgrade head
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`alembic.ini` intentionally leaves `sqlalchemy.url` empty; a relative URL there would create a
|
|
31
|
+
stray empty `neobot.db` in the working directory (which must never be committed).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "neobot-storage"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Storage layer for NeoBot — SQLAlchemy + Alembic"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
9
|
+
"aiosqlite>=0.20.0",
|
|
10
|
+
"alembic>=1.15.0",
|
|
11
|
+
"neobot-contracts",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[[project.authors]]
|
|
15
|
+
name = "wsrsq"
|
|
16
|
+
email = "wsrsq001@163.com"
|
|
17
|
+
|
|
18
|
+
[[project.authors]]
|
|
19
|
+
name = "tangtian"
|
|
20
|
+
email = "a14b@126.com"
|
|
21
|
+
|
|
22
|
+
[tool.uv.sources.neobot-contracts]
|
|
23
|
+
workspace = true
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["uv_build>=0.9.27,<0.10.0"]
|
|
27
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "neobot-storage"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Storage layer for NeoBot — SQLAlchemy + Alembic"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "wsrsq", email = "wsrsq001@163.com" },
|
|
8
|
+
{ name = "tangtian", email = "a14b@126.com" },
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"sqlalchemy[asyncio]>=2.0",
|
|
13
|
+
"aiosqlite>=0.20.0",
|
|
14
|
+
"alembic>=1.15.0",
|
|
15
|
+
"neobot-contracts",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.uv.sources]
|
|
19
|
+
neobot-contracts = { workspace = true }
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["uv_build>=0.9.27,<0.10.0"]
|
|
23
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""neobot_storage 公共 API。"""
|
|
2
|
+
|
|
3
|
+
from neobot_storage.backup import backup_sqlite_database
|
|
4
|
+
from neobot_storage.engine import create_engine, run_migrations, sqlite_url
|
|
5
|
+
from neobot_storage.models import Base, MaintenanceRunRecord, ModelUsageRecord
|
|
6
|
+
from neobot_storage.uow import SqlAlchemyUnitOfWork, make_uow_factory
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"create_engine",
|
|
10
|
+
"run_migrations",
|
|
11
|
+
"sqlite_url",
|
|
12
|
+
"backup_sqlite_database",
|
|
13
|
+
"Base",
|
|
14
|
+
"MaintenanceRunRecord",
|
|
15
|
+
"ModelUsageRecord",
|
|
16
|
+
"SqlAlchemyUnitOfWork",
|
|
17
|
+
"make_uow_factory",
|
|
18
|
+
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""针对 SQLite 瞬时锁错误的重试辅助工具。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import random
|
|
7
|
+
from typing import Awaitable, Callable, Iterator, TypeVar
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
# SQLite returns this error code / message when it cannot acquire a write lock
|
|
12
|
+
_LOCKED_MESSAGES = (
|
|
13
|
+
"database is locked",
|
|
14
|
+
"database table is locked",
|
|
15
|
+
"locking protocol",
|
|
16
|
+
"deadlock",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _iter_exception_chain(exc: BaseException) -> Iterator[BaseException]:
|
|
21
|
+
"""按顺序产出 *exc* 及其 ``__cause__``/``__context__`` 异常链,循环安全。"""
|
|
22
|
+
seen: set[int] = set()
|
|
23
|
+
link: BaseException | None = exc
|
|
24
|
+
while link is not None and id(link) not in seen:
|
|
25
|
+
seen.add(id(link))
|
|
26
|
+
yield link
|
|
27
|
+
link = link.__cause__ or link.__context__
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_locked_error(exc: Exception) -> bool:
|
|
31
|
+
# PendingRollbackError 的 message 内嵌原始 flush 异常
|
|
32
|
+
# ("Original exception was: ... database is locked"),
|
|
33
|
+
# 沿 __cause__/__context__ 链匹配可覆盖该场景,且不会对
|
|
34
|
+
# 普通含 "rolled back" 字样的错误过度匹配。
|
|
35
|
+
for link in _iter_exception_chain(exc):
|
|
36
|
+
msg = str(link).casefold()
|
|
37
|
+
if any(pattern in msg for pattern in _LOCKED_MESSAGES):
|
|
38
|
+
return True
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def retry_on_lock(
|
|
43
|
+
coro_factory: Callable[[], Awaitable[T]],
|
|
44
|
+
*,
|
|
45
|
+
max_retries: int = 3,
|
|
46
|
+
base_delay: float = 0.1,
|
|
47
|
+
max_delay: float = 2.0,
|
|
48
|
+
on_retry: Callable[[], Awaitable[None]] | None = None,
|
|
49
|
+
) -> T:
|
|
50
|
+
"""执行 *coro_factory*,当 SQLite 报告数据库被锁定时,
|
|
51
|
+
按指数退避 + 随机抖动重试。
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
coro_factory : callable returning awaitable
|
|
56
|
+
每次调用都返回新协程的工厂函数(以便重试)。
|
|
57
|
+
max_retries : int
|
|
58
|
+
重试次数上限,超过后抛出最后一次异常。
|
|
59
|
+
base_delay : float
|
|
60
|
+
初始延迟(秒),每次重试翻倍。
|
|
61
|
+
max_delay : float
|
|
62
|
+
延迟上限。
|
|
63
|
+
on_retry : optional callable returning awaitable
|
|
64
|
+
每次重试尝试前(退避延迟之后)调用。失败的 flush
|
|
65
|
+
会让 SQLAlchemy 会话处于已回滚状态,可传入类似
|
|
66
|
+
``session.rollback`` 的调用以恢复会话;重试的事务体
|
|
67
|
+
将在全新事务上重新执行。
|
|
68
|
+
|
|
69
|
+
Raises
|
|
70
|
+
------
|
|
71
|
+
重试全部耗尽时抛出最后遇到的异常。
|
|
72
|
+
"""
|
|
73
|
+
last_exc: Exception | None = None
|
|
74
|
+
for attempt in range(max_retries + 1):
|
|
75
|
+
try:
|
|
76
|
+
return await coro_factory()
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
last_exc = exc
|
|
79
|
+
if not _is_locked_error(exc):
|
|
80
|
+
raise
|
|
81
|
+
if attempt >= max_retries:
|
|
82
|
+
break
|
|
83
|
+
delay = min(base_delay * (2**attempt), max_delay)
|
|
84
|
+
jitter = random.uniform(0, delay * 0.25)
|
|
85
|
+
await asyncio.sleep(delay + jitter)
|
|
86
|
+
if on_retry is not None:
|
|
87
|
+
await on_retry()
|
|
88
|
+
|
|
89
|
+
# All retries exhausted
|
|
90
|
+
raise last_exc # type: ignore[misc]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""面向异步 SQLAlchemy 的 Alembic 环境。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from logging.config import fileConfig
|
|
7
|
+
|
|
8
|
+
from alembic import context
|
|
9
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
10
|
+
|
|
11
|
+
from neobot_storage.models import Base
|
|
12
|
+
|
|
13
|
+
config = context.config
|
|
14
|
+
if config.config_file_name:
|
|
15
|
+
fileConfig(config.config_file_name)
|
|
16
|
+
|
|
17
|
+
target_metadata = Base.metadata
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_database_url() -> str:
|
|
21
|
+
"""解析数据库 URL:优先 ini(由 run_migrations 注入),其次命令行 -x db_url=...。
|
|
22
|
+
|
|
23
|
+
不回退到相对路径,避免 alembic CLI 在源码目录生成 neobot.db 这类构建产物。
|
|
24
|
+
"""
|
|
25
|
+
url = config.get_main_option("sqlalchemy.url") or ""
|
|
26
|
+
if not url:
|
|
27
|
+
url = context.get_x_argument(as_dictionary=True).get("db_url", "")
|
|
28
|
+
if not url:
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
"未配置数据库 URL:应用启动时由 run_migrations(db_url) 注入;"
|
|
31
|
+
"命令行请使用 alembic -x db_url=sqlite+aiosqlite:///<绝对路径>/neobot.db upgrade head"
|
|
32
|
+
)
|
|
33
|
+
return url
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run_migrations_offline() -> None:
|
|
37
|
+
context.configure(
|
|
38
|
+
url=_resolve_database_url(),
|
|
39
|
+
target_metadata=target_metadata,
|
|
40
|
+
literal_binds=True,
|
|
41
|
+
dialect_opts={"paramstyle": "named"},
|
|
42
|
+
)
|
|
43
|
+
with context.begin_transaction():
|
|
44
|
+
context.run_migrations()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def run_migrations_online() -> None:
|
|
48
|
+
engine = create_async_engine(_resolve_database_url())
|
|
49
|
+
async with engine.connect() as conn:
|
|
50
|
+
await conn.run_sync(
|
|
51
|
+
lambda sync_conn: context.configure(
|
|
52
|
+
connection=sync_conn,
|
|
53
|
+
target_metadata=target_metadata,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
async with conn.begin():
|
|
57
|
+
await conn.run_sync(lambda _: context.run_migrations())
|
|
58
|
+
await engine.dispose()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if context.is_offline_mode():
|
|
62
|
+
run_migrations_offline()
|
|
63
|
+
else:
|
|
64
|
+
try:
|
|
65
|
+
asyncio.get_running_loop()
|
|
66
|
+
# 已在 event loop 中(如 app 启动时调用),用新线程跑
|
|
67
|
+
import concurrent.futures
|
|
68
|
+
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
69
|
+
pool.submit(asyncio.run, run_migrations_online()).result()
|
|
70
|
+
except RuntimeError:
|
|
71
|
+
# 没有 event loop(CLI 直接调用)
|
|
72
|
+
asyncio.run(run_migrations_online())
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence, Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
${imports if imports else ""}
|
|
15
|
+
|
|
16
|
+
revision: str = ${repr(up_revision)}
|
|
17
|
+
down_revision: Union[str, None] = ${repr(down_revision)}
|
|
18
|
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
|
19
|
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
${upgrades if upgrades else "pass"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade() -> None:
|
|
27
|
+
${downgrades if downgrades else "pass"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""初始表结构
|
|
2
|
+
|
|
3
|
+
Revision ID: 0001
|
|
4
|
+
Revises:
|
|
5
|
+
Create Date: 2026-03-21 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence, Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
revision: str = "0001"
|
|
16
|
+
down_revision: Union[str, None] = None
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
op.create_table(
|
|
23
|
+
"USER_DATA",
|
|
24
|
+
sa.Column("user_id", sa.String(), primary_key=True),
|
|
25
|
+
sa.Column("nick_name", sa.Text()),
|
|
26
|
+
sa.Column("relation_ship", sa.Text()),
|
|
27
|
+
sa.Column("profile", sa.Text()),
|
|
28
|
+
sa.Column("birthday", sa.Text()),
|
|
29
|
+
sa.Column("sex", sa.Text()),
|
|
30
|
+
sa.Column("city", sa.Text()),
|
|
31
|
+
sa.Column("country", sa.Text()),
|
|
32
|
+
sa.Column("labs", sa.Text()),
|
|
33
|
+
sa.Column("remark", sa.Text()),
|
|
34
|
+
sa.Column("age", sa.Integer()),
|
|
35
|
+
sa.Column("long_nick", sa.Text()),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
op.create_table(
|
|
39
|
+
"GROUP_DATA",
|
|
40
|
+
sa.Column("group_id", sa.String(), primary_key=True),
|
|
41
|
+
sa.Column("group_name", sa.Text()),
|
|
42
|
+
sa.Column("profile", sa.Text()),
|
|
43
|
+
sa.Column("is_quite", sa.Boolean(), server_default=sa.false()),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
op.create_table(
|
|
47
|
+
"EVENT_DATA",
|
|
48
|
+
sa.Column("event_id", sa.String(), primary_key=True),
|
|
49
|
+
sa.Column("event_message", sa.Text()),
|
|
50
|
+
sa.Column("embedded_data", sa.Text()),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
op.create_table(
|
|
54
|
+
"messages",
|
|
55
|
+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
|
56
|
+
sa.Column("event_id", sa.String(), nullable=False, unique=True),
|
|
57
|
+
sa.Column("conversation_kind", sa.String(), nullable=False),
|
|
58
|
+
sa.Column("conversation_id", sa.String(), nullable=False),
|
|
59
|
+
sa.Column("sender_id", sa.String(), nullable=False),
|
|
60
|
+
sa.Column("sender_name", sa.Text(), nullable=False),
|
|
61
|
+
sa.Column("text", sa.Text(), nullable=False),
|
|
62
|
+
sa.Column("occurred_at", sa.DateTime(), nullable=False),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
op.create_table(
|
|
66
|
+
"memories",
|
|
67
|
+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
|
68
|
+
sa.Column("conversation_kind", sa.String(), nullable=False),
|
|
69
|
+
sa.Column("conversation_id", sa.String(), nullable=False),
|
|
70
|
+
sa.Column("speaker_id", sa.String(), nullable=False),
|
|
71
|
+
sa.Column("content", sa.Text(), nullable=False),
|
|
72
|
+
sa.Column("created_at", sa.DateTime(), nullable=False),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def downgrade() -> None:
|
|
77
|
+
op.drop_table("memories")
|
|
78
|
+
op.drop_table("messages")
|
|
79
|
+
op.drop_table("EVENT_DATA")
|
|
80
|
+
op.drop_table("GROUP_DATA")
|
|
81
|
+
op.drop_table("USER_DATA")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""表名转小写
|
|
2
|
+
|
|
3
|
+
Revision ID: 0002
|
|
4
|
+
Revises: 0001
|
|
5
|
+
Create Date: 2026-03-21 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence, Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
revision: str = "0002"
|
|
15
|
+
down_revision: Union[str, None] = "0001"
|
|
16
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
17
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def upgrade() -> None:
|
|
21
|
+
# SQLite treats table names as case-insensitive, so we need a two-step rename
|
|
22
|
+
for old, tmp, new in [
|
|
23
|
+
("USER_DATA", "_tmp_user_data", "user_data"),
|
|
24
|
+
("GROUP_DATA", "_tmp_group_data", "group_data"),
|
|
25
|
+
("EVENT_DATA", "_tmp_event_data", "event_data"),
|
|
26
|
+
]:
|
|
27
|
+
op.rename_table(old, tmp)
|
|
28
|
+
op.rename_table(tmp, new)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def downgrade() -> None:
|
|
32
|
+
for old, tmp, new in [
|
|
33
|
+
("user_data", "_tmp_user_data", "USER_DATA"),
|
|
34
|
+
("group_data", "_tmp_group_data", "GROUP_DATA"),
|
|
35
|
+
("event_data", "_tmp_event_data", "EVENT_DATA"),
|
|
36
|
+
]:
|
|
37
|
+
op.rename_table(old, tmp)
|
|
38
|
+
op.rename_table(tmp, new)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""新增 archive_memories 表
|
|
2
|
+
|
|
3
|
+
Revision ID: 0003
|
|
4
|
+
Revises: 0002
|
|
5
|
+
Create Date: 2026-03-22 00:00:00.000000
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence, Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
revision: str = "0003"
|
|
16
|
+
down_revision: Union[str, None] = "0002"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
op.create_table(
|
|
23
|
+
"archive_memories",
|
|
24
|
+
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
|
25
|
+
sa.Column("table_name", sa.String(), nullable=False),
|
|
26
|
+
sa.Column("key", sa.String(), nullable=False),
|
|
27
|
+
sa.Column("value", sa.Text(), nullable=False),
|
|
28
|
+
sa.Column("tags", sa.Text()),
|
|
29
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
30
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
31
|
+
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
|
|
32
|
+
sa.UniqueConstraint("table_name", "key", name="uq_archive_memories_table_key"),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# 创建索引和唯一约束
|
|
36
|
+
op.create_index(
|
|
37
|
+
"ix_archive_memories_table_name_updated_at",
|
|
38
|
+
"archive_memories",
|
|
39
|
+
["table_name", "updated_at"],
|
|
40
|
+
)
|
|
41
|
+
def downgrade() -> None:
|
|
42
|
+
op.drop_index("ix_archive_memories_table_name_updated_at", "archive_memories")
|
|
43
|
+
op.drop_table("archive_memories")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""新增 images 表
|
|
2
|
+
|
|
3
|
+
Revision ID: 0004
|
|
4
|
+
Revises: 0003
|
|
5
|
+
Create Date: 2026-04-09
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
revision: str = "0004"
|
|
17
|
+
down_revision: Union[str, None] = "0003"
|
|
18
|
+
branch_labels = None
|
|
19
|
+
depends_on = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
op.create_table(
|
|
24
|
+
"images",
|
|
25
|
+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
26
|
+
sa.Column("file_hash", sa.String(), nullable=False),
|
|
27
|
+
sa.Column("source", sa.Text(), nullable=True),
|
|
28
|
+
sa.Column("mime_type", sa.String(), nullable=True),
|
|
29
|
+
sa.Column("original_width", sa.Integer(), nullable=True),
|
|
30
|
+
sa.Column("original_height", sa.Integer(), nullable=True),
|
|
31
|
+
sa.Column("processed_width", sa.Integer(), nullable=True),
|
|
32
|
+
sa.Column("processed_height", sa.Integer(), nullable=True),
|
|
33
|
+
sa.Column("analysis_text", sa.Text(), nullable=True),
|
|
34
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
35
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
36
|
+
sa.Column("version", sa.Integer(), nullable=False),
|
|
37
|
+
sa.PrimaryKeyConstraint("id"),
|
|
38
|
+
sa.UniqueConstraint("file_hash", name="uq_images_file_hash"),
|
|
39
|
+
)
|
|
40
|
+
op.create_index("ix_images_updated_at", "images", ["updated_at"])
|
|
41
|
+
op.create_index("ix_images_source", "images", ["source"])
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def downgrade() -> None:
|
|
45
|
+
op.drop_index("ix_images_source", table_name="images")
|
|
46
|
+
op.drop_index("ix_images_updated_at", table_name="images")
|
|
47
|
+
op.drop_table("images")
|
neobot_storage-1.0.0/src/neobot_storage/alembic/versions/0005_add_user_profile_refresh_fields.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""新增用户画像刷新字段
|
|
2
|
+
|
|
3
|
+
Revision ID: 0005
|
|
4
|
+
Revises: 0004
|
|
5
|
+
Create Date: 2026-04-12
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
revision: str = "0005"
|
|
17
|
+
down_revision: Union[str, None] = "0004"
|
|
18
|
+
branch_labels = None
|
|
19
|
+
depends_on = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
op.add_column("user_data", sa.Column("known_gender", sa.Text(), nullable=True))
|
|
24
|
+
op.add_column(
|
|
25
|
+
"user_data",
|
|
26
|
+
sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=True),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def downgrade() -> None:
|
|
31
|
+
op.drop_column("user_data", "fetched_at")
|
|
32
|
+
op.drop_column("user_data", "known_gender")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""新增 emojis 表
|
|
2
|
+
|
|
3
|
+
Revision ID: 0006
|
|
4
|
+
Revises: 0005
|
|
5
|
+
Create Date: 2026-04-24
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
revision: str = "0006"
|
|
17
|
+
down_revision: Union[str, None] = "0005"
|
|
18
|
+
branch_labels = None
|
|
19
|
+
depends_on = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
op.create_table(
|
|
24
|
+
"emojis",
|
|
25
|
+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
26
|
+
sa.Column("file_hash", sa.String(), nullable=False),
|
|
27
|
+
sa.Column("file_name", sa.String(), nullable=False),
|
|
28
|
+
sa.Column("file_path", sa.String(), nullable=False),
|
|
29
|
+
sa.Column("mime_type", sa.String(), nullable=True),
|
|
30
|
+
sa.Column("original_width", sa.Integer(), nullable=True),
|
|
31
|
+
sa.Column("original_height", sa.Integer(), nullable=True),
|
|
32
|
+
sa.Column("analysis_text", sa.Text(), nullable=True),
|
|
33
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
34
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
35
|
+
sa.Column("version", sa.Integer(), nullable=False),
|
|
36
|
+
sa.PrimaryKeyConstraint("id"),
|
|
37
|
+
sa.UniqueConstraint("file_hash", name="uq_emojis_file_hash"),
|
|
38
|
+
)
|
|
39
|
+
op.create_index("ix_emojis_updated_at", "emojis", ["updated_at"])
|
|
40
|
+
op.create_index("ix_emojis_file_name", "emojis", ["file_name"])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def downgrade() -> None:
|
|
44
|
+
op.drop_index("ix_emojis_file_name", table_name="emojis")
|
|
45
|
+
op.drop_index("ix_emojis_updated_at", table_name="emojis")
|
|
46
|
+
op.drop_table("emojis")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""新增 creator_images 表
|
|
2
|
+
|
|
3
|
+
Revision ID: 0007
|
|
4
|
+
Revises: 0006
|
|
5
|
+
Create Date: 2026-04-25
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from alembic import op
|
|
13
|
+
import sqlalchemy as sa
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
revision: str = "0007"
|
|
17
|
+
down_revision: Union[str, None] = "0006"
|
|
18
|
+
branch_labels = None
|
|
19
|
+
depends_on = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def upgrade() -> None:
|
|
23
|
+
op.create_table(
|
|
24
|
+
"creator_images",
|
|
25
|
+
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
|
26
|
+
sa.Column("image_id", sa.String(), nullable=False),
|
|
27
|
+
sa.Column("source", sa.String(), nullable=False),
|
|
28
|
+
sa.Column("file_hash", sa.String(), nullable=False),
|
|
29
|
+
sa.Column("file_path", sa.String(), nullable=False),
|
|
30
|
+
sa.Column("prompt", sa.Text(), nullable=True),
|
|
31
|
+
sa.Column("description", sa.Text(), nullable=True),
|
|
32
|
+
sa.Column("mime_type", sa.String(), nullable=True),
|
|
33
|
+
sa.Column("original_width", sa.Integer(), nullable=True),
|
|
34
|
+
sa.Column("original_height", sa.Integer(), nullable=True),
|
|
35
|
+
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
|
36
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
|
37
|
+
sa.Column("version", sa.Integer(), nullable=False),
|
|
38
|
+
sa.PrimaryKeyConstraint("id"),
|
|
39
|
+
sa.UniqueConstraint("image_id", name="uq_creator_images_image_id"),
|
|
40
|
+
)
|
|
41
|
+
op.create_index(
|
|
42
|
+
"ix_creator_images_source_updated_at",
|
|
43
|
+
"creator_images",
|
|
44
|
+
["source", "updated_at"],
|
|
45
|
+
)
|
|
46
|
+
op.create_index("ix_creator_images_file_hash", "creator_images", ["file_hash"])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def downgrade() -> None:
|
|
50
|
+
op.drop_index("ix_creator_images_file_hash", table_name="creator_images")
|
|
51
|
+
op.drop_index("ix_creator_images_source_updated_at", table_name="creator_images")
|
|
52
|
+
op.drop_table("creator_images")
|