feat(conversation): TTS 投递与 WebSocket 管线;客户端播放门禁与会话页联动;COS 键与迁移脚本调整
This commit is contained in:
@@ -14,12 +14,17 @@ refresh_tokens / segments / sms_verification_codes / users(见仓库内历史
|
||||
|
||||
2) 目标库已执行 ``alembic upgrade head``(含 pgvector 与当前 ORM 表)。
|
||||
|
||||
3) 运行::
|
||||
3) 运行(仓库内可用 ``uv run python scripts/...``)::
|
||||
|
||||
cd api && uv run python scripts/migrate_legacy_to_current.py \\
|
||||
python3 migrate_legacy_to_current.py \\
|
||||
--legacy-url postgresql://postgres:postgres@localhost:5432/life_echo_legacy \\
|
||||
--target-url postgresql://postgres:postgres@localhost:5432/life_echo
|
||||
|
||||
**仅服务器 + psycopg**:本脚本不依赖项目内其它包,可复制单文件到机器上执行::
|
||||
|
||||
pip install 'psycopg[binary]' # 或 python3 -m pip install --user 'psycopg[binary]'
|
||||
python3 migrate_legacy_to_current.py --legacy-url ... --target-url ...
|
||||
|
||||
说明:
|
||||
- 不会创建 stories / memory_* / conversation_messages 等旧库中不存在的表数据;
|
||||
- chapters:content → canonical_markdown;按 user_id 关联该用户唯一 book 填 book_id(若无书则为 NULL);
|
||||
@@ -31,32 +36,44 @@ refresh_tokens / segments / sms_verification_codes / users(见仓库内历史
|
||||
|
||||
若目标库已有用户且手机号与某条 legacy 用户冲突(同号不同 id),会自动跳过该 legacy 用户及其 books/chapters/
|
||||
conversations 等关联行,避免违反 ``users.phone`` 唯一约束。新生产库一般为空库,不会触发。
|
||||
|
||||
**宿主机上跑脚本(数据库在 Docker Compose 里)**:`.env` 里常见主机名 `postgres`,在容器外无法解析。
|
||||
可直接把 URL 写成 `...@127.0.0.1:5432/...`,或使用 `--db-host 127.0.0.1` 自动替换两个 URL 中的主机名(端口不变)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(_ROOT))
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from psycopg import Connection, connect
|
||||
from psycopg.rows import dict_row
|
||||
from psycopg.types.json import Json
|
||||
|
||||
from app.core.logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OnConflict = Literal["upsert", "skip"]
|
||||
|
||||
|
||||
def _replace_url_host(url: str, new_host: str) -> str:
|
||||
"""将 postgresql URL 中的主机名替换为 new_host(保留用户、密码、端口、库名)。"""
|
||||
u = urlparse(url)
|
||||
if not u.netloc or "@" not in u.netloc:
|
||||
return url
|
||||
auth, hostport = u.netloc.rsplit("@", 1)
|
||||
if ":" in hostport:
|
||||
_old_host, port = hostport.split(":", 1)
|
||||
new_netloc = f"{auth}@{new_host}:{port}"
|
||||
else:
|
||||
new_netloc = f"{auth}@{new_host}"
|
||||
return urlunparse((u.scheme, new_netloc, u.path, u.params, u.query, u.fragment))
|
||||
|
||||
|
||||
def _open(url: str) -> Connection:
|
||||
return connect(url, autocommit=False)
|
||||
|
||||
@@ -76,7 +93,7 @@ def _legacy_user_ids_skipped_for_phone(
|
||||
if owner is not None and owner != r["id"]:
|
||||
skipped.add(r["id"])
|
||||
logger.warning(
|
||||
"skip legacy user {} phone={} (target user id={})",
|
||||
"skip legacy user %s phone=%s (target user id=%s)",
|
||||
r["id"],
|
||||
r["phone"],
|
||||
owner,
|
||||
@@ -678,11 +695,27 @@ def main() -> None:
|
||||
action="store_true",
|
||||
help="Connect and print row counts only; no writes.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--db-host",
|
||||
metavar="HOST",
|
||||
default=None,
|
||||
help=(
|
||||
"Replace hostname in both URLs (e.g. 127.0.0.1 when running on the host "
|
||||
"while Postgres is published from Docker; keeps user/password/port/dbname)."
|
||||
),
|
||||
)
|
||||
args = p.parse_args()
|
||||
on_conflict: OnConflict = args.on_conflict # type: ignore[assignment]
|
||||
|
||||
legacy = _open(args.legacy_url)
|
||||
target = _open(args.target_url)
|
||||
legacy_url = args.legacy_url
|
||||
target_url = args.target_url
|
||||
if args.db_host:
|
||||
legacy_url = _replace_url_host(legacy_url, args.db_host)
|
||||
target_url = _replace_url_host(target_url, args.db_host)
|
||||
logger.info("using db host %s (from --db-host)", args.db_host)
|
||||
|
||||
legacy = _open(legacy_url)
|
||||
target = _open(target_url)
|
||||
try:
|
||||
if args.dry_run:
|
||||
with legacy.cursor(row_factory=dict_row) as cur:
|
||||
@@ -699,14 +732,14 @@ def main() -> None:
|
||||
):
|
||||
cur.execute(f"SELECT COUNT(*) AS c FROM {t}")
|
||||
c = cur.fetchone()["c"]
|
||||
logger.info("legacy {} rows={}", t, c)
|
||||
logger.info("legacy %s rows=%s", t, c)
|
||||
logger.info("dry-run done")
|
||||
return
|
||||
|
||||
skip_users = _legacy_user_ids_skipped_for_phone(legacy, target)
|
||||
if skip_users:
|
||||
logger.info(
|
||||
"skip {} legacy users due to phone already owned in target",
|
||||
"skip %d legacy users due to phone already owned in target",
|
||||
len(skip_users),
|
||||
)
|
||||
|
||||
@@ -724,9 +757,9 @@ def main() -> None:
|
||||
target.commit()
|
||||
|
||||
logger.info(
|
||||
"migration committed: users={} books={} memoir_states={} "
|
||||
"conversations={} segments={} orders={} refresh_tokens={} "
|
||||
"sms={} chapters={} memoir_images={}",
|
||||
"migration committed: users=%s books=%s memoir_states=%s "
|
||||
"conversations=%s segments=%s orders=%s refresh_tokens=%s "
|
||||
"sms=%s chapters=%s memoir_images=%s",
|
||||
n_users,
|
||||
n_books,
|
||||
n_memoir,
|
||||
|
||||
Reference in New Issue
Block a user