From 140ac20281b04687278cfdc8900e41d2650b6cbe Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Wed, 9 Sep 2026 10:47:18 +0800 Subject: [PATCH 1/4] fix(avatar): prevent stale deployment images --- digital-avatar-app/Dockerfile | 9 ++ digital-avatar-app/backend/Dockerfile | 7 ++ digital-avatar-app/backend/main.py | 31 +++++- .../backend/tests/test_health.py | 33 ++++++ digital-avatar-app/docker-compose.yml | 35 ++++-- .../docs/H5_PRODUCTION_DEPLOYMENT.md | 29 +++-- .../scripts/verify-deployment.py | 105 ++++++++++++++++++ 7 files changed, 228 insertions(+), 21 deletions(-) create mode 100644 digital-avatar-app/backend/tests/test_health.py create mode 100644 digital-avatar-app/scripts/verify-deployment.py diff --git a/digital-avatar-app/Dockerfile b/digital-avatar-app/Dockerfile index 6f2bfe9..c202011 100644 --- a/digital-avatar-app/Dockerfile +++ b/digital-avatar-app/Dockerfile @@ -1,12 +1,16 @@ # 构建阶段:安装依赖并打包 H5 FROM node:18-alpine AS build +ARG APP_GIT_SHA=unknown +ARG APP_BUILD_TIME=unknown + WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . +RUN printf '{"gitSha":"%s","buildTime":"%s"}\n' "$APP_GIT_SHA" "$APP_BUILD_TIME" > public/version.json RUN npm run build # 运行阶段:nginx 托管静态资源并反向代理 /api 到后端 @@ -14,6 +18,11 @@ RUN npm run build # 新版 nginx(>=1.31) 用 pwrite 写 pid 文件会被拦导致致命退出;1.28 用 write() 可正常启动。 FROM nginx:1.28-alpine +ARG APP_GIT_SHA=unknown +ARG APP_BUILD_TIME=unknown +LABEL org.opencontainers.image.revision=${APP_GIT_SHA} \ + org.opencontainers.image.created=${APP_BUILD_TIME} + COPY --from=build /app/dist /usr/share/nginx/html # 覆盖 nginx 默认主配置(含唯一可写的 pid /tmp/nginx.pid,规避受限容器内 /run 不可写导致反复重启) COPY nginx.conf /etc/nginx/nginx.conf diff --git a/digital-avatar-app/backend/Dockerfile b/digital-avatar-app/backend/Dockerfile index 2db7f71..5db6c42 100644 --- a/digital-avatar-app/backend/Dockerfile +++ b/digital-avatar-app/backend/Dockerfile @@ -1,5 +1,12 @@ FROM python:3.10-slim +ARG APP_GIT_SHA=unknown +ARG APP_BUILD_TIME=unknown +ENV APP_GIT_SHA=${APP_GIT_SHA} \ + APP_BUILD_TIME=${APP_BUILD_TIME} +LABEL org.opencontainers.image.revision=${APP_GIT_SHA} \ + org.opencontainers.image.created=${APP_BUILD_TIME} + WORKDIR /app # 后端依赖(fastapi/uvicorn/sqlalchemy/pypdf/python-docx/openpyxl 等)均为纯 Python wheel, diff --git a/digital-avatar-app/backend/main.py b/digital-avatar-app/backend/main.py index 7bd8418..fe54432 100644 --- a/digital-avatar-app/backend/main.py +++ b/digital-avatar-app/backend/main.py @@ -1,13 +1,14 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -import os +import importlib.util import logging +import os from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.triggers.interval import IntervalTrigger -from database import init_db, SessionLocal +from database import engine, init_db, SessionLocal from models import Avatar, Authorization, Organization, TokenAccount, TokenPlan, User from fastapi.staticfiles import StaticFiles import routers.avatars @@ -54,7 +55,31 @@ app.mount("/api/files", StaticFiles(directory=UPLOAD_DIR), name="knowledge-files @app.get("/api/health") def health(): - return ok({"status": "ok"}) + checks = _runtime_checks() + return ok({ + "status": "ok" if all(checks.values()) else "degraded", + "gitSha": os.getenv("APP_GIT_SHA", "unknown"), + "buildTime": os.getenv("APP_BUILD_TIME", "unknown"), + "checks": checks, + }) + + +def _runtime_checks(): + return { + "database": _database_is_ready(), + "uploads": os.path.isdir(UPLOAD_DIR) and os.access(UPLOAD_DIR, os.W_OK), + "pdfOcr": importlib.util.find_spec("pymupdf") is not None, + } + + +def _database_is_ready(): + try: + with engine.connect() as connection: + connection.exec_driver_sql("SELECT 1") + return True + except Exception: + logger.exception("Database readiness check failed") + return False def seed(): diff --git a/digital-avatar-app/backend/tests/test_health.py b/digital-avatar-app/backend/tests/test_health.py new file mode 100644 index 0000000..3750aa5 --- /dev/null +++ b/digital-avatar-app/backend/tests/test_health.py @@ -0,0 +1,33 @@ +import main + + +def test_health_reports_release_and_runtime_capabilities(monkeypatch): + monkeypatch.setenv("APP_GIT_SHA", "test-sha") + monkeypatch.setenv("APP_BUILD_TIME", "2026-09-09T00:00:00Z") + monkeypatch.setattr(main, "_runtime_checks", lambda: { + "database": True, + "uploads": True, + "pdfOcr": True, + }) + + response = main.health() + + assert response["code"] == 200 + assert response["data"]["status"] == "ok" + assert response["data"]["gitSha"] == "test-sha" + assert response["data"]["buildTime"] == "2026-09-09T00:00:00Z" + assert response["data"]["checks"] == { + "database": True, + "uploads": True, + "pdfOcr": True, + } + + +def test_health_is_degraded_when_a_required_capability_is_missing(monkeypatch): + monkeypatch.setattr(main, "_runtime_checks", lambda: { + "database": True, + "uploads": True, + "pdfOcr": False, + }) + + assert main.health()["data"]["status"] == "degraded" diff --git a/digital-avatar-app/docker-compose.yml b/digital-avatar-app/docker-compose.yml index 4a9530f..d6c3339 100644 --- a/digital-avatar-app/docker-compose.yml +++ b/digital-avatar-app/docker-compose.yml @@ -1,42 +1,55 @@ # 会会数字分身 —— Docker 测试实例(独立端口,不干扰现有 :8088 huihui 部署) services: avatar-backend: - build: ./backend - image: avatar-test-backend:latest + build: + context: ./backend + args: + APP_GIT_SHA: ${APP_GIT_SHA:?APP_GIT_SHA must be the full release commit} + APP_BUILD_TIME: ${APP_BUILD_TIME:?APP_BUILD_TIME must be set} + image: avatar-test-backend:${APP_GIT_SHA} container_name: avatar-test-backend restart: unless-stopped env_file: - .env environment: - DATABASE_URL: sqlite:////data/avatar.db - UPLOAD_DIR: /data/uploads + DATABASE_URL: sqlite:////app/avatar.db + UPLOAD_DIR: /app/uploads CHAT_MODEL_CONFIG_URL: http://host.docker.internal:8000/api/ai-models/runtime/digital-avatar extra_hosts: - "host.docker.internal:host-gateway" volumes: - - avatar-data:/data + - ${AVATAR_DB_PATH:?AVATAR_DB_PATH must point to the persistent avatar.db}:/app/avatar.db + - ${AVATAR_UPLOAD_DIR:?AVATAR_UPLOAD_DIR must point to persistent uploads}:/app/uploads expose: - "8000" ports: - "8011:8000" # 仅用于直接调试 API;前端经内部网络访问,不走 host 端口 + healthcheck: + test: ["CMD", "python", "-c", "import json,urllib.request; d=json.load(urllib.request.urlopen('http://127.0.0.1:8000/api/health', timeout=5))['data']; assert d['status']=='ok' and all(d['checks'].values())"] + interval: 10s + timeout: 8s + retries: 12 + start_period: 20s networks: - avatar-net avatar-frontend: - build: . - image: avatar-test-frontend:latest + build: + context: . + args: + APP_GIT_SHA: ${APP_GIT_SHA:?APP_GIT_SHA must be the full release commit} + APP_BUILD_TIME: ${APP_BUILD_TIME:?APP_BUILD_TIME must be set} + image: avatar-test-frontend:${APP_GIT_SHA} container_name: avatar-test-frontend restart: unless-stopped ports: - "8099:80" # 浏览器访问 http://:8099 depends_on: - - avatar-backend + avatar-backend: + condition: service_healthy networks: - avatar-net networks: avatar-net: driver: bridge - -volumes: - avatar-data: diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index c25ef77..cceb2b5 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -61,8 +61,10 @@ WECHAT_VIRTUAL_PRODUCT_2=<100元套餐商品ID> WECHAT_VIRTUAL_PRODUCT_3=<1000元套餐商品ID> WECHAT_VIRTUAL_PRODUCT_4=<10000元套餐商品ID> -DATABASE_URL=sqlite:////data/avatar.db -UPLOAD_DIR=/data/uploads +AVATAR_DB_PATH=/srv/digital-avatar/data/avatar.db +AVATAR_UPLOAD_DIR=/srv/digital-avatar/data/uploads +DATABASE_URL=sqlite:////app/avatar.db +UPLOAD_DIR=/app/uploads CHAT_MODEL_CONFIG_URL=http:///api/ai-models/runtime/digital-avatar EMBEDDING_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 EMBEDDING_API_KEY= @@ -74,13 +76,17 @@ VISION_OCR_MODEL=qwen-vl-ocr VISION_MAX_OUTPUT_TOKENS=2048 VISION_TIMEOUT_SECONDS=90 VISION_TOKEN_RESERVE=12000 +APP_GIT_SHA=<本次发布的完整提交SHA> +APP_BUILD_TIME= CHAT_IMAGE_MAX_BYTES=8388608 CHAT_IMAGE_MAX_PIXELS=16000000 CHAT_ATTACHMENT_RETENTION_HOURS=24 CHAT_ATTACHMENT_CLEANUP_MINUTES=60 ``` -如生产 AI 配置中心不可用,还应提供当前项目支持的 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`CHAT_MODEL` 等兜底配置。`/data` 必须挂载持久卷,数据库与知识库文件不可存放在容器临时层。 +如生产 AI 配置中心不可用,还应提供当前项目支持的 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`CHAT_MODEL` 等兜底配置。数据库文件与上传目录必须从宿主机显式挂载,不能存放在容器临时层。 + +`AVATAR_DB_PATH` 和 `AVATAR_UPLOAD_DIR` 必须是已备份的宿主机绝对路径,编排缺少任一变量都会直接拒绝构建或启动,防止误挂空卷造成用户、分身或知识库“丢失”的假象。 `EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。 @@ -107,13 +113,22 @@ tar -C /srv/digital-avatar/data -czf "$BACKUP_DIR/uploads.tgz" uploads git fetch origin git checkout <已验收的提交SHA> cd digital-avatar-app -docker compose build --pull avatar-backend avatar-frontend -docker compose up -d avatar-backend avatar-frontend +export APP_GIT_SHA="$(git rev-parse HEAD)" +export APP_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +docker compose build --pull --no-cache avatar-backend avatar-frontend +docker compose up -d --force-recreate --wait avatar-backend avatar-frontend docker compose ps -curl -fsS http://127.0.0.1:8099/api/health +python3 scripts/verify-deployment.py \ + https://digital.99hui.com "$APP_GIT_SHA" \ + --backend-container avatar-backend \ + --frontend-container avatar-frontend \ + --expected-db-source /srv/digital-avatar/data/avatar.db \ + --expected-upload-source /srv/digital-avatar/data/uploads docker compose exec avatar-backend python -c 'import embeddings; v=embeddings.embed(["部署向量探针"]); print(len(v), len(v[0]))' ``` +Jenkins 必须以 `verify-deployment.py` 返回成功作为发布成功条件,不能只以镜像构建或容器启动成功作为条件。脚本会同时核对公网前后端 Git SHA、数据库可读、上传目录可写、PDF OCR 依赖和宿主机数据挂载;任意一项不一致都会返回非零状态并阻止发布标绿。镜像使用 Git SHA 标签,不再依赖可被旧缓存覆盖的 `latest`。 + 生产编排应把示例中的测试端口改为内网暴露,由统一 HTTPS 网关接入。后端暂时使用 SQLite,必须保持单实例写入;若扩展为多后端实例,应先迁移到 PostgreSQL,并把延迟接管任务改为共享队列。 ## 4. 网关要求 @@ -153,7 +168,7 @@ location /api/ { 5. 使用过期或伪造 token 时进入登录页并显示凭证失效,不得继续访问旧用户数据。 6. 分身聊天 SSE 逐段输出正常,Markdown 正常渲染,知识库优先级和积分扣费正常。 7. 开启 BOXIM 主动接管后保持在线,默认三分钟回复、自定义等待时间、已读回执、分身防回环和主人发言暂停均正常。 -8. 重建容器后数据库、头像、知识库文档仍存在,`/api/health` 返回成功。 +8. 重建容器后数据库、头像、知识库文档仍存在,`/api/health` 的 `gitSha` 与发布 SHA 一致,`database`、`uploads`、`pdfOcr` 三项检查均为 `true`。 9. `https://digital.99hui.com/api/health` 可访问,证书域名和有效期正确,HTTP 自动跳转 HTTPS。 10. 微信和支付宝各创建一笔最小套餐订单,未付款时积分不变;支付成功后回调到账一次,重复回调积分不重复增加。 11. 微信虚拟支付在沙箱环境完成下单、支付回调、查单兜底和退款回调;错误 OpenID、商品、环境或金额均被拒绝。 diff --git a/digital-avatar-app/scripts/verify-deployment.py b/digital-avatar-app/scripts/verify-deployment.py new file mode 100644 index 0000000..03860a2 --- /dev/null +++ b/digital-avatar-app/scripts/verify-deployment.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Fail a deployment unless frontend and backend run the expected release.""" + +import argparse +import json +import os +import subprocess +import sys +import urllib.request + + +def fetch_json(url): + with urllib.request.urlopen(url, timeout=20) as response: + if response.status != 200: + raise RuntimeError(f"{url} returned HTTP {response.status}") + return json.load(response) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("base_url", help="Public site URL, for example https://digital.99hui.com") + parser.add_argument("expected_sha", help="Full Git commit SHA being deployed") + parser.add_argument("--backend-container", help="Backend container name for image and mount checks") + parser.add_argument("--frontend-container", help="Frontend container name for image checks") + parser.add_argument("--expected-db-source", help="Required host source mounted as the SQLite database") + parser.add_argument("--expected-upload-source", help="Required host source mounted as the upload directory") + args = parser.parse_args() + + base_url = args.base_url.rstrip("/") + errors = [] + try: + health = fetch_json(f"{base_url}/api/health").get("data") or {} + except Exception as exc: + errors.append(f"cannot read backend release metadata: {exc}") + health = {} + try: + frontend = fetch_json(f"{base_url}/version.json") + except Exception as exc: + errors.append(f"cannot read frontend release metadata: {exc}") + frontend = {} + + if health.get("status") != "ok": + errors.append(f"backend status is {health.get('status')!r}") + failed_checks = [name for name, passed in (health.get("checks") or {}).items() if not passed] + if failed_checks: + errors.append("backend checks failed: " + ", ".join(failed_checks)) + if health.get("gitSha") != args.expected_sha: + errors.append(f"backend SHA is {health.get('gitSha')!r}") + if frontend.get("gitSha") != args.expected_sha: + errors.append(f"frontend SHA is {frontend.get('gitSha')!r}") + + if args.backend_container: + backend = inspect_container(args.backend_container, errors) + check_container_revision(backend, args.expected_sha, "backend", errors) + check_mount(backend, args.expected_db_source, "database", errors) + check_mount(backend, args.expected_upload_source, "uploads", errors) + elif args.expected_db_source or args.expected_upload_source: + errors.append("--backend-container is required when checking data mounts") + + if args.frontend_container: + frontend_container = inspect_container(args.frontend_container, errors) + check_container_revision(frontend_container, args.expected_sha, "frontend", errors) + + if errors: + print("Deployment verification failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + + print(f"Deployment verified: {args.expected_sha}") + print("Backend checks: database, uploads, pdfOcr") + return 0 + + +def inspect_container(name, errors): + try: + output = subprocess.check_output(["docker", "inspect", name], text=True) + return json.loads(output)[0] + except Exception as exc: + errors.append(f"cannot inspect container {name!r}: {exc}") + return {} + + +def check_container_revision(container, expected_sha, label, errors): + actual = ((container.get("Config") or {}).get("Labels") or {}).get( + "org.opencontainers.image.revision" + ) + if actual != expected_sha: + errors.append(f"{label} container image SHA is {actual!r}") + + +def check_mount(container, expected_source, label, errors): + if not expected_source: + return + expected = os.path.realpath(expected_source) + sources = { + os.path.realpath(mount.get("Source", "")) + for mount in container.get("Mounts") or [] + } + if expected not in sources: + errors.append(f"{label} mount source {expected!r} is not attached") + + +if __name__ == "__main__": + raise SystemExit(main()) From 9ce46cd883f999a2b7294ddb3705a37b4acb5446 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Wed, 9 Sep 2026 10:56:43 +0800 Subject: [PATCH 2/4] fix(avatar): support legacy deployment Python --- digital-avatar-app/scripts/verify-deployment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/digital-avatar-app/scripts/verify-deployment.py b/digital-avatar-app/scripts/verify-deployment.py index 03860a2..7a629b6 100644 --- a/digital-avatar-app/scripts/verify-deployment.py +++ b/digital-avatar-app/scripts/verify-deployment.py @@ -74,7 +74,9 @@ def main(): def inspect_container(name, errors): try: - output = subprocess.check_output(["docker", "inspect", name], text=True) + output = subprocess.check_output( + ["docker", "inspect", name], universal_newlines=True + ) return json.loads(output)[0] except Exception as exc: errors.append(f"cannot inspect container {name!r}: {exc}") From 4c152230aaa2dd37983088fa99882de74aced270 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Wed, 9 Sep 2026 10:58:09 +0800 Subject: [PATCH 3/4] build(avatar): preserve backend dependency cache --- digital-avatar-app/backend/Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/digital-avatar-app/backend/Dockerfile b/digital-avatar-app/backend/Dockerfile index 5db6c42..7b942e6 100644 --- a/digital-avatar-app/backend/Dockerfile +++ b/digital-avatar-app/backend/Dockerfile @@ -1,12 +1,5 @@ FROM python:3.10-slim -ARG APP_GIT_SHA=unknown -ARG APP_BUILD_TIME=unknown -ENV APP_GIT_SHA=${APP_GIT_SHA} \ - APP_BUILD_TIME=${APP_BUILD_TIME} -LABEL org.opencontainers.image.revision=${APP_GIT_SHA} \ - org.opencontainers.image.created=${APP_BUILD_TIME} - WORKDIR /app # 后端依赖(fastapi/uvicorn/sqlalchemy/pypdf/python-docx/openpyxl 等)均为纯 Python wheel, @@ -14,6 +7,13 @@ WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --timeout 120 --retries 10 -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt +ARG APP_GIT_SHA=unknown +ARG APP_BUILD_TIME=unknown +ENV APP_GIT_SHA=${APP_GIT_SHA} \ + APP_BUILD_TIME=${APP_BUILD_TIME} +LABEL org.opencontainers.image.revision=${APP_GIT_SHA} \ + org.opencontainers.image.created=${APP_BUILD_TIME} + COPY . . # 后端使用 SQLite(avatar.db 落在 /app 内),平铺结构以 `uvicorn main:app` 启动 From 5ce12771b96dd1dff966b40d2b095a713351168f Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Wed, 9 Sep 2026 11:06:03 +0800 Subject: [PATCH 4/4] fix(avatar): persist SQLite WAL across releases --- digital-avatar-app/docker-compose.yml | 9 +++++---- digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md | 12 ++++++------ digital-avatar-app/scripts/verify-deployment.py | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/digital-avatar-app/docker-compose.yml b/digital-avatar-app/docker-compose.yml index d6c3339..9a58e3f 100644 --- a/digital-avatar-app/docker-compose.yml +++ b/digital-avatar-app/docker-compose.yml @@ -12,14 +12,15 @@ services: env_file: - .env environment: - DATABASE_URL: sqlite:////app/avatar.db - UPLOAD_DIR: /app/uploads + DATABASE_URL: sqlite:////data/db/avatar.db + UPLOAD_DIR: /data/uploads CHAT_MODEL_CONFIG_URL: http://host.docker.internal:8000/api/ai-models/runtime/digital-avatar extra_hosts: - "host.docker.internal:host-gateway" volumes: - - ${AVATAR_DB_PATH:?AVATAR_DB_PATH must point to the persistent avatar.db}:/app/avatar.db - - ${AVATAR_UPLOAD_DIR:?AVATAR_UPLOAD_DIR must point to persistent uploads}:/app/uploads + # Mount the directory, not only avatar.db: SQLite WAL/SHM files must survive recreation. + - ${AVATAR_DB_DIR:?AVATAR_DB_DIR must contain the persistent avatar.db}:/data/db + - ${AVATAR_UPLOAD_DIR:?AVATAR_UPLOAD_DIR must point to persistent uploads}:/data/uploads expose: - "8000" ports: diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index cceb2b5..b6e0f57 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -61,10 +61,10 @@ WECHAT_VIRTUAL_PRODUCT_2=<100元套餐商品ID> WECHAT_VIRTUAL_PRODUCT_3=<1000元套餐商品ID> WECHAT_VIRTUAL_PRODUCT_4=<10000元套餐商品ID> -AVATAR_DB_PATH=/srv/digital-avatar/data/avatar.db +AVATAR_DB_DIR=/srv/digital-avatar/data/db AVATAR_UPLOAD_DIR=/srv/digital-avatar/data/uploads -DATABASE_URL=sqlite:////app/avatar.db -UPLOAD_DIR=/app/uploads +DATABASE_URL=sqlite:////data/db/avatar.db +UPLOAD_DIR=/data/uploads CHAT_MODEL_CONFIG_URL=http:///api/ai-models/runtime/digital-avatar EMBEDDING_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 EMBEDDING_API_KEY= @@ -86,7 +86,7 @@ CHAT_ATTACHMENT_CLEANUP_MINUTES=60 如生产 AI 配置中心不可用,还应提供当前项目支持的 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`CHAT_MODEL` 等兜底配置。数据库文件与上传目录必须从宿主机显式挂载,不能存放在容器临时层。 -`AVATAR_DB_PATH` 和 `AVATAR_UPLOAD_DIR` 必须是已备份的宿主机绝对路径,编排缺少任一变量都会直接拒绝构建或启动,防止误挂空卷造成用户、分身或知识库“丢失”的假象。 +`AVATAR_DB_DIR` 和 `AVATAR_UPLOAD_DIR` 必须是已备份的宿主机绝对路径,编排缺少任一变量都会直接拒绝构建或启动,防止误挂空卷造成用户、分身或知识库“丢失”的假象。SQLite 必须挂载整个数据库目录,不能只挂载 `avatar.db` 单文件,否则 `avatar.db-wal` 和 `avatar.db-shm` 会留在容器临时层,换容器后可能出现数据状态回退。 `EMBEDDING_API_URL` 同时支持 OpenAI 兼容基础地址(如上面的 `/v1`)和完整的 `/v1/embeddings` 地址,后端会统一请求 `/embeddings`。发布后必须在后端容器内执行一次最小向量探针,确认返回向量数量和维度,而不能只检查 `/api/health`。 @@ -103,7 +103,7 @@ App 与 H5 积分充值使用会会支付体系的 `payment-v3/payment/pay`, ```bash BACKUP_DIR="backups/$(date +%Y%m%d-%H%M%S)" mkdir -p "$BACKUP_DIR" -cp /srv/digital-avatar/data/avatar.db "$BACKUP_DIR/" +cp /srv/digital-avatar/data/db/avatar.db "$BACKUP_DIR/" tar -C /srv/digital-avatar/data -czf "$BACKUP_DIR/uploads.tgz" uploads ``` @@ -122,7 +122,7 @@ python3 scripts/verify-deployment.py \ https://digital.99hui.com "$APP_GIT_SHA" \ --backend-container avatar-backend \ --frontend-container avatar-frontend \ - --expected-db-source /srv/digital-avatar/data/avatar.db \ + --expected-db-source /srv/digital-avatar/data/db \ --expected-upload-source /srv/digital-avatar/data/uploads docker compose exec avatar-backend python -c 'import embeddings; v=embeddings.embed(["部署向量探针"]); print(len(v), len(v[0]))' ``` diff --git a/digital-avatar-app/scripts/verify-deployment.py b/digital-avatar-app/scripts/verify-deployment.py index 7a629b6..52867e4 100644 --- a/digital-avatar-app/scripts/verify-deployment.py +++ b/digital-avatar-app/scripts/verify-deployment.py @@ -22,7 +22,7 @@ def main(): parser.add_argument("expected_sha", help="Full Git commit SHA being deployed") parser.add_argument("--backend-container", help="Backend container name for image and mount checks") parser.add_argument("--frontend-container", help="Frontend container name for image checks") - parser.add_argument("--expected-db-source", help="Required host source mounted as the SQLite database") + parser.add_argument("--expected-db-source", help="Required host directory mounted for SQLite and its WAL files") parser.add_argument("--expected-upload-source", help="Required host source mounted as the upload directory") args = parser.parse_args()