Bladeren bron

feat:修复README.md文档与一键启动代码问题

yangxiaolong 1 maand geleden
bovenliggende
commit
23d7cf65d3

+ 27 - 10
README.md

@@ -50,31 +50,48 @@
 
 如果尚未准备 MySQL 和 Redis,请先阅读 [Docker 基础环境使用说明](database/Docker基础环境使用说明.md),并使用其中的 `database/compose.yaml`。
 
-项目只读取 `backend/.env`。确认其中的 MySQL、Redis、Milvus、模型服务和 LangSmith 配置后,在项目根目录按顺序运行:
+项目只读取 `backend/.env`。确认其中的 MySQL、Redis、Milvus、模型服务和 LangSmith 配置后,在项目根目录按下面的顺序执行。
 
-Windows PowerShell:
+### 1. 安装项目依赖
 
-```powershell
+Windows PowerShell、macOS 和 Linux 使用相同命令:
+
+```bash
 uv sync --project backend --locked
 pnpm --dir frontend install --frozen-lockfile
+```
+
+### 2. 初始化 MySQL 数据
+
+MySQL 数据来自 `database/mysql.sql`,其中已经包含前两个阶段继承数据和第三阶段所需业务数据。下面的命令会自动导入 `insurance_s3_core`、`insurance_s3_agent` 和 `insurance_s3_analytics`。
 
+Windows PowerShell:
+
+```powershell
 .\scripts\init-databases.ps1
-.\scripts\seed-knowledge.ps1
 ```
 
 macOS / Linux:
 
 ```bash
-uv sync --project backend --locked
-pnpm --dir frontend install --frozen-lockfile
-
 ./scripts/init-databases.sh
-./scripts/seed-knowledge.sh
 ```
 
-以上命令依次完成后端依赖安装、前端依赖安装、MySQL 数据导入和 Milvus 知识库初始化。
+SQL 已经包含完整表结构和初始数据,因此首次初始化不需要再执行迁移、种子或阶段继承命令。
+
+### 3. 初始化 Milvus 知识库
+
+Windows PowerShell:
 
-MySQL 数据来自 `database/mysql.sql`,其中已经包含前两个阶段继承数据和第三阶段所需业务数据。`init-databases` 脚本会自动导入 `insurance_s3_core`、`insurance_s3_agent` 和 `insurance_s3_analytics`,因此首次初始化不需要再单独执行迁移、种子或阶段继承命令。
+```powershell
+.\scripts\seed-knowledge.ps1
+```
+
+macOS / Linux:
+
+```bash
+./scripts/seed-knowledge.sh
+```
 
 ## 启动
 

+ 15 - 1
backend/src/zbt/infrastructure/milvus/knowledge_index.py

@@ -20,6 +20,8 @@ class MilvusKnowledgeIndex:
         self._embedding_model: Any | None = None
         self._client_lock = Lock()
         self._embedding_model_lock = Lock()
+        self._collection_lock = Lock()
+        self._collection_ready = False
 
     def warmup(self) -> None:
         """预连接 Milvus,并在后台把本地 BGE-M3 权重装入内存。"""
@@ -151,8 +153,20 @@ class MilvusKnowledgeIndex:
         return cast(list[list[float]], raw_vectors)
 
     def _ensure_collection(self, client: MilvusClient) -> None:
-        if client.has_collection(self._collection_name):
+        if self._collection_ready:
             return
+        with self._collection_lock:
+            if self._collection_ready:
+                return
+            if not client.has_collection(self._collection_name):
+                self._create_collection(client)
+            client.load_collection(
+                collection_name=self._collection_name,
+                timeout=30.0,
+            )
+            self._collection_ready = True
+
+    def _create_collection(self, client: MilvusClient) -> None:
         schema = MilvusClient.create_schema(auto_id=False, enable_dynamic_field=False)
         schema.add_field(
             field_name="id",

+ 15 - 1
backend/src/zbt/infrastructure/milvus/memory_index.py

@@ -25,6 +25,8 @@ class MilvusCustomerMemoryIndex(CustomerMemoryIndex):
         self._embedding_model: Any | None = None
         self._client_lock = Lock()
         self._embedding_model_lock = Lock()
+        self._collection_lock = Lock()
+        self._collection_ready = False
 
     def upsert(self, memory: CustomerMemory) -> None:
         try:
@@ -134,8 +136,20 @@ class MilvusCustomerMemoryIndex(CustomerMemoryIndex):
         return cast(list[list[float]], raw_vectors)
 
     def _ensure_collection(self, client: MilvusClient) -> None:
-        if client.has_collection(self._collection_name):
+        if self._collection_ready:
             return
+        with self._collection_lock:
+            if self._collection_ready:
+                return
+            if not client.has_collection(self._collection_name):
+                self._create_collection(client)
+            client.load_collection(
+                collection_name=self._collection_name,
+                timeout=30.0,
+            )
+            self._collection_ready = True
+
+    def _create_collection(self, client: MilvusClient) -> None:
         schema = MilvusClient.create_schema(
             auto_id=False,
             enable_dynamic_field=False,

+ 2 - 0
database/Docker基础环境使用说明.md

@@ -35,6 +35,8 @@ docker compose -f database/compose.yaml ps
 
 当 MySQL 和 Redis 的状态均为 `healthy` 后,再执行根目录 `README.md` 中的首次初始化命令。
 
+`docker compose ps` 还应显示 MySQL 的 `0.0.0.0:3306->3306/tcp` 端口映射。如果没有该映射,通常是本机已有 MySQL 占用了 3306。请先停止本机原有 MySQL 服务,再重新执行上面的 Docker 启动命令;不要让本机 MySQL 与 Docker MySQL 同时占用 3306。
+
 ## 4. backend/.env 配置
 
 使用本 compose 文件时,`backend/.env` 中的公共连接配置应与下面保持一致:

+ 1 - 1
frontend/apps/admin/package.json

@@ -4,7 +4,7 @@
   "version": "0.1.0",
   "type": "module",
   "scripts": {
-    "dev": "vite --host 127.0.0.1 --port 5174",
+    "dev": "vite --host 127.0.0.1 --port 5174 --strictPort",
     "build": "vue-tsc -b && vite build",
     "typecheck": "vue-tsc -b"
   },

+ 1 - 1
frontend/apps/h5/package.json

@@ -4,7 +4,7 @@
   "version": "0.1.0",
   "type": "module",
   "scripts": {
-    "dev": "vite --host 127.0.0.1 --port 5173",
+    "dev": "vite --host 127.0.0.1 --port 5173 --strictPort",
     "build": "vue-tsc -b && vite build",
     "typecheck": "vue-tsc -b"
   },

+ 1 - 1
frontend/package.json

@@ -6,7 +6,7 @@
     "node": ">=22 <23"
   },
   "scripts": {
-    "dev": "pnpm --parallel --filter './apps/*' dev",
+    "dev": "pnpm --parallel --filter \"./apps/*\" dev",
     "dev:h5": "pnpm --filter @zbt/h5 dev",
     "dev:admin": "pnpm --filter @zbt/admin dev",
     "build": "pnpm --workspace-concurrency=1 --recursive build",

+ 6 - 0
scripts/build-analytics.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.build_analytics

+ 17 - 0
scripts/check-env.sh

@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+backend_root="$project_root/backend"
+
+[[ -f "$backend_root/.env" ]] || { echo "缺少 backend/.env。请先创建该文件并配置后端环境变量。" >&2; exit 1; }
+command -v uv >/dev/null 2>&1 || { echo "未找到 uv。" >&2; exit 1; }
+
+cd "$project_root"
+uv run --project backend python -c 'import sys; assert sys.version_info[:2] == (3, 12); print(sys.version)'
+
+command -v nc >/dev/null 2>&1 || { echo "未找到 nc,无法检查 MySQL 和 Redis 端口。" >&2; exit 1; }
+nc -z 127.0.0.1 3306 || { echo "MySQL 127.0.0.1:3306 未监听。" >&2; exit 1; }
+nc -z 127.0.0.1 6379 || { echo "Redis 127.0.0.1:6379 未监听。" >&2; exit 1; }
+
+echo "环境检查通过:uv、Python 3.12、MySQL、Redis。"

+ 8 - 3
scripts/dev.ps1

@@ -2,7 +2,11 @@
 param(
     # 仅用于自动化验证;正常启动时保持 0,由 Ctrl+C 结束。
     [ValidateRange(0, 3600)]
-    [int]$RunForSeconds = 0
+    [int]$RunForSeconds = 0,
+
+    # 首次启动可能需要加载本地嵌入模型,因此预留充足的冷启动时间。
+    [ValidateRange(10, 600)]
+    [int]$StartupTimeoutSeconds = 120
 )
 
 $ErrorActionPreference = "Stop"
@@ -109,7 +113,7 @@ function Wait-ForDevelopmentServices {
     param(
         [System.Diagnostics.Process]$FrontendProcess,
         [System.Diagnostics.Process]$BackendProcess,
-        [int]$TimeoutSeconds = 45
+        [int]$TimeoutSeconds = 120
     )
 
     $Deadline = (Get-Date).AddSeconds($TimeoutSeconds)
@@ -215,7 +219,8 @@ try {
 
     Wait-ForDevelopmentServices `
         -FrontendProcess $FrontendProcess `
-        -BackendProcess $BackendProcess
+        -BackendProcess $BackendProcess `
+        -TimeoutSeconds $StartupTimeoutSeconds
 
     Write-Host ""
     Write-Host "智保通开发服务已全部启动:" -ForegroundColor Green

+ 12 - 2
scripts/dev.sh

@@ -6,6 +6,7 @@ frontend_pid=""
 backend_pid=""
 cleanup_done=0
 run_for_seconds="${ZBT_DEV_RUN_SECONDS:-0}"
+startup_timeout_seconds="${ZBT_DEV_STARTUP_TIMEOUT_SECONDS:-120}"
 
 die() {
     echo "错误:$*" >&2
@@ -96,7 +97,7 @@ http_ready() {
 }
 
 wait_for_services() {
-    local deadline=$((SECONDS + 45))
+    local deadline=$((SECONDS + startup_timeout_seconds))
 
     while (( SECONDS < deadline )); do
         kill -0 "$frontend_pid" 2>/dev/null || \
@@ -113,7 +114,7 @@ wait_for_services() {
         sleep 0.3
     done
 
-    die "开发服务在 45 秒内未全部就绪,请检查上方启动日志。"
+    die "开发服务在 ${startup_timeout_seconds} 秒内未全部就绪,请检查上方启动日志。"
 }
 
 case "$run_for_seconds" in
@@ -122,6 +123,15 @@ case "$run_for_seconds" in
         ;;
 esac
 
+case "$startup_timeout_seconds" in
+    ''|*[!0-9]*)
+        die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须是正整数。"
+        ;;
+    0)
+        die "ZBT_DEV_STARTUP_TIMEOUT_SECONDS 必须大于 0。"
+        ;;
+esac
+
 trap cleanup EXIT
 trap 'exit 130' INT
 trap 'exit 143' TERM

+ 36 - 0
scripts/export-stage3-sql.sh

@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+source_path="$project_root/database/zhibaotong-stage1-stage2-stage3-mysql.sql"
+target_path="$project_root/database/zhibaotong-stage3-mysql.sql"
+
+uv run --project "$project_root/backend" python - "$source_path" "$target_path" <<'PY'
+from pathlib import Path
+import sys
+
+source_path = Path(sys.argv[1])
+target_path = Path(sys.argv[2])
+content = source_path.read_text(encoding="utf-8")
+
+session_start_marker = "/*!40101 SET @OLD_CHARACTER_SET_CLIENT"
+first_database_marker = "-- Current Database: `insurance_s1_core`"
+stage3_marker = "-- Current Database: `insurance_s3_core`"
+
+session_start = content.find(session_start_marker)
+first_database_start = content.find(first_database_marker)
+stage3_start = content.find(stage3_marker)
+if min(session_start, first_database_start, stage3_start) < 0:
+    raise RuntimeError("Expected stage boundaries were not found in the unified SQL.")
+
+session_preamble = content[session_start:first_database_start]
+stage3_body = content[stage3_start:]
+header = """-- Zhibaotong Stage 3 MySQL initialization script
+-- Source: zhibaotong-stage1-stage2-stage3-mysql.sql
+-- Databases: insurance_s3_core, insurance_s3_agent, insurance_s3_analytics
+-- Warning: importing this file rebuilds same-named tables in these databases.
+
+"""
+target_path.write_text(header + session_preamble + stage3_body, encoding="utf-8", newline="")
+print(f"Generated: {target_path}")
+PY

+ 6 - 0
scripts/inherit-stage-one.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.inherit_stage_one

+ 9 - 0
scripts/migrate.sh

@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+
+uv run alembic -c alembic.ini --name alembic_core upgrade head
+uv run alembic -c alembic.ini --name alembic_agent upgrade head
+uv run alembic -c alembic.ini --name alembic_analytics upgrade head

+ 23 - 0
scripts/reset-dev-data.sh

@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+project_root="$(cd "$script_dir/.." && pwd)"
+expected="RESET insurance_s3"
+confirmation="${1:-}"
+
+echo "即将重建数据库:insurance_s3_core、insurance_s3_agent、insurance_s3_analytics"
+echo "即将删除 Redis 前缀:ins:s3:*"
+if [[ -z "${confirmation//[[:space:]]/}" ]]; then
+    read -r -p "请输入 $expected 继续: " confirmation
+fi
+[[ "$confirmation" == "$expected" ]] || { echo "确认文本不匹配,已取消。" >&2; exit 1; }
+
+cd "$project_root/backend"
+uv run alembic -c alembic.ini --name alembic_analytics downgrade base
+uv run alembic -c alembic.ini --name alembic_agent downgrade base
+uv run alembic -c alembic.ini --name alembic_core downgrade base
+uv run python -m zbt.commands.reset_cache
+"$script_dir/migrate.sh"
+"$script_dir/seed.sh"
+"$script_dir/build-analytics.sh"

+ 6 - 0
scripts/run-refund-worker.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run zbt-refund-worker

+ 6 - 0
scripts/seed.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.seed

+ 15 - 0
scripts/test.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+cd "$project_root/backend"
+uv sync --locked
+uv run pytest
+uv run ruff check .
+uv run mypy src/zbt
+
+cd "$project_root/frontend"
+pnpm install --frozen-lockfile
+pnpm typecheck
+pnpm build

+ 6 - 0
scripts/verify-seed.sh

@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$project_root/backend"
+uv run python -m zbt.commands.verify_seed