固化接口体检脚本,并交付双击即用的启动 bat
## tools/portal_api_check.py(新) 把 2026-09-13 那轮"照着接口内容把前端测一遍"的验证固化成可重复跑的脚本。 当时靠这套验证查出 6 个真缺陷,但它们只存在于一次会话里,下次改动没人会重跑: - `K002`/`K003` 是裸信封,前端按 `payload.data` 取 -> 知识库页显示"已入库 0 块 / 为空" - 委托/成交详情页照着建表字段写,而接口返回视图没带那些列 -> 5 行永远显示 `--` - 配置项/路由规则的 `PUT` 要 `If-Match`,却没有端点能返回该 digest -> 首次编辑必然 409 - 回复模板 `scene` 只校验长度不校验枚举 -> 非法值撞数据库 CHECK、冒成 500 - 路由规则表单固定 `max_attempts=2` 且 `fallbacks` 为空 -> 后端必然 422 - 模型端点手填 ID -> 未激活的 ID 直接 422 与 `e2e_smoke_test.py` 分工互补:后者测**业务链路通不通**,本脚本测**接口契约对不对** (状态码、信封形状、字段名与前端期望是否一致)。 三档,默认只跑第一档: - 默认只读 41 项(不动数据) - `--write` 加测写操作:配置项 ETag 链路、`max_attempts` 越界、知识库上传+失效、 回复模板非法枚举、敏感词、推广物料全链路 - `--dangerous` 再加测会改生效配置的操作(激活配置版本会整版本替换,默认不跑, 脚本内注明了恢复办法) 实测:只读 39/39、写入档 54/54 全绿。 ## 桌面一键启动(双击即用) 桌面 `启动金融Agent平台.bat` + 仓库 `启动平台.bat`,由 `tools/make_launcher_bat.py` 生成,只负责"双击"这一层,启动逻辑复用 `start.ps1` (不重复实现,避免两边漂移)。参数可透传:`-Port 8100` / `-NoBrowser` / `-SkipPriceSync`。 不要手写这个 bat:必须同时满足 **GBK 编码 + CRLF 换行 + 无 BOM**。生成器自己 读回来校验这三条。踩过的坑:用 `write_bytes` 直接落盘时没转 CRLF,cmd 对 LF-only 批处理会行边界错乱,把 `echo` 的说明文字当命令执行(实测报 `AT 命令已弃用`、`']' 不是内部或外部命令`)。 ## start.ps1 的三处修复 1. **解释器探测选错环境(真机双击失败的主因)** 原先只验证 `--version` 成功就选中,结果挑到一个 Python 3.10 环境: 本项目用了 `datetime.UTC`(3.11+)且依赖 `asyncmy`,启动当场 ImportError —— 而报错发生在**行情刷新**那一步,看起来像"行情源坏了"。 现在实测两项:**版本 >= 3.11** + `import fastapi, sqlalchemy, asyncmy, pydantic` 通过, 并在跳过时打印具体原因。 2. **`Select-Object -First 1` 掐断 native 管道** `& $exe -c ... 2>&1 | Select-Object -First 1` 拿到首个对象后停掉上游管道, 等于把进程掐了、`$LASTEXITCODE` 变脏,于是**每个候选都被误判成"无法执行"** (连装好的 jr_py313 也被跳过)。改为先整体接住输出、再在结果上取行。 3. **幂等 + 就绪探测 + Docker 自动拉起** 重复双击不再起第二个 API/Worker(API 按端口、Worker 按进程判断); 起完等 `/internal/health/ready` 真正应答才报成功;Milvus 不可达时尝试拉起 Docker Desktop 并最多等 60 秒(它不常驻,是"知识检索静默降级"最常见的原因)。 ## 实测 - `启动平台.bat` 重复执行:解释器正确选中 jr_py313、依赖检查全 OK、行情已刷新、 两个服务识别为已在跑并跳过、就绪探测 1 秒 - `-Port 8199` 冷启动:新起 API 窗口,8199 的 `/internal/health/ready` 与 `/portal/` 均 200, 测试后已清理 - `ruff check app tests tools alembic hq.py` -> All checks passed - `start.ps1` 语法解析通过(BOM 已保留)、bat 三项编码约束校验通过 文档同步:`AGENTS.md`(启动方式、bat 生成器、解释器探测两个坑)、 `docs/44-演示流程.md` §0.3/§0.4(双击启动、两条自检线的分工)。
This commit is contained in:
@@ -30,15 +30,23 @@
|
||||
.PARAMETER SkipPriceSync
|
||||
跳过启动时的行情刷新。仅在外部数据源不可用、或不想联网时使用。
|
||||
|
||||
.PARAMETER NoBrowser
|
||||
启动后不自动打开浏览器。
|
||||
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File start.ps1
|
||||
powershell -ExecutionPolicy Bypass -File start.ps1 -Port 8100
|
||||
|
||||
.NOTES
|
||||
本脚本是**幂等**的:重复执行不会起出第二个 API/Worker
|
||||
(端口被占用、或已存在 app.worker 进程时会跳过),可以放心反复双击桌面的启动 .bat。
|
||||
#>
|
||||
param(
|
||||
[int]$Port = 8000,
|
||||
[switch]$SkipChecks,
|
||||
[switch]$ApiOnly,
|
||||
[switch]$SkipPriceSync
|
||||
[switch]$SkipPriceSync,
|
||||
[switch]$NoBrowser
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -48,49 +56,161 @@ Set-Location $Root
|
||||
Write-Host "=== 金融 Agent 平台 · 启动 ===" -ForegroundColor Cyan
|
||||
Write-Host "工作目录:$Root"
|
||||
|
||||
# ---------------------------------------------------------------- 小工具
|
||||
# 用 .NET 的 TcpClient 探端口:比 Test-NetConnection 快一个量级
|
||||
# (后者每个端口要 1~5 秒,双击启动时全花在等待上),且不依赖 WinRM 相关服务。
|
||||
function Test-PortOpen {
|
||||
param([string]$TargetHost, [int]$TargetPort, [int]$TimeoutMs = 1500)
|
||||
$client = New-Object System.Net.Sockets.TcpClient
|
||||
try {
|
||||
$async = $client.BeginConnect($TargetHost, $TargetPort, $null, $null)
|
||||
if (-not $async.AsyncWaitHandle.WaitOne($TimeoutMs, $false)) { return $false }
|
||||
$client.EndConnect($async)
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
} finally {
|
||||
$client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
# Worker 没有监听端口,只能按命令行匹配进程。
|
||||
function Test-WorkerRunning {
|
||||
try {
|
||||
$procs = Get-CimInstance Win32_Process -Filter "Name like '%python%'" -ErrorAction SilentlyContinue
|
||||
return @($procs | Where-Object { $_.CommandLine -like '*app.worker*' }).Count -gt 0
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# Milvus 跑在 Docker 里,而 **Docker Desktop 不会常驻**:它没起来时 Milvus 必然不可达,
|
||||
# 后果是知识检索**静默降级**(客服答不上知识题、新灌的知识进不了向量库,且没有任何报错)。
|
||||
# 所以这里尝试把 Docker Desktop 拉起来,等 Milvus 端口出现为止。
|
||||
function Start-DockerIfPossible {
|
||||
$candidates = @(
|
||||
(Join-Path $env:ProgramFiles "Docker\Docker\Docker Desktop.exe"),
|
||||
(Join-Path ${env:ProgramFiles(x86)} "Docker\Docker\Docker Desktop.exe"),
|
||||
(Join-Path $env:LOCALAPPDATA "Docker\Docker Desktop.exe")
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
|
||||
if (-not $candidates) {
|
||||
Write-Host " [提示] 没找到 Docker Desktop:Milvus 无法自动拉起" -ForegroundColor DarkGray
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Host " [启动] Docker Desktop(Milvus 依赖它)…" -ForegroundColor Yellow
|
||||
Start-Process ($candidates | Select-Object -First 1) | Out-Null
|
||||
|
||||
# 冷启动通常 30~60 秒。最多等 60 秒,每 5 秒报告一次,避免看起来像卡死。
|
||||
for ($i = 1; $i -le 12; $i++) {
|
||||
Start-Sleep -Seconds 5
|
||||
if (Test-PortOpen "127.0.0.1" 19530) {
|
||||
Write-Host (" [OK] Milvus 已就绪(等待 {0} 秒)" -f ($i * 5)) -ForegroundColor Green
|
||||
return $true
|
||||
}
|
||||
Write-Host (" 等待 Milvus… 已等 {0} 秒" -f ($i * 5)) -ForegroundColor DarkGray
|
||||
}
|
||||
Write-Host " [警告] 等了 60 秒 Milvus 仍未就绪:本次启动继续,知识检索会降级" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 解释器
|
||||
# 逐个尝试:项目虚拟环境 → 常见 conda 环境 → PATH 上的 python。
|
||||
# 不写死任何一个,因为各人机器的环境不同(.venv 被 gitignore,不进仓库)。
|
||||
#
|
||||
# ⚠️ 判定标准不是「这个 exe 能跑」,而是「这个环境能跑起本项目」。
|
||||
# 这里踩过坑:原先只验证 `--version` 成功就选中,结果 PATH 上先撞到一个
|
||||
# Python 3.10 环境 —— 本项目用了 `datetime.UTC`(3.11+)且依赖 `asyncmy`,
|
||||
# 启动当场 ImportError,而且报错发生在**行情刷新**那一步,看上去像
|
||||
# 「行情源坏了」,很难联想到是解释器选错。
|
||||
# 所以这里实测两项:**版本 ≥ 3.11** + **关键依赖能导入**。
|
||||
$candidates = @(
|
||||
(Join-Path $Root ".venv\Scripts\python.exe"),
|
||||
"D:\conda\envs\jr_py313\python.exe",
|
||||
"$env:USERPROFILE\miniconda3\envs\jr_py313\python.exe",
|
||||
"$env:USERPROFILE\anaconda3\envs\jr_py313\python.exe",
|
||||
"python"
|
||||
)
|
||||
|
||||
$Python = $null
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -ne "python" -and -not (Test-Path $candidate)) { continue }
|
||||
|
||||
# ⚠️ 这里**不能**用 `... 2>&1 | Select-Object -First 1`。
|
||||
# `Select-Object -First 1` 拿到第一个对象后会停掉上游管道,而上游是 native 命令 ——
|
||||
# 等于把进程掐了,`$LASTEXITCODE` 随即变成非 0,于是**每个候选都被误判成"无法执行"**
|
||||
# (实测:连装得好好的 jr_py313 也被跳过,最后报"找不到 Python 环境")。
|
||||
# 先整体接住输出、再在结果上取行,才安全。
|
||||
$versionOutput = ""
|
||||
try {
|
||||
if ($candidate -eq "python") {
|
||||
$version = & python --version 2>&1
|
||||
} else {
|
||||
if (-not (Test-Path $candidate)) { continue }
|
||||
$version = & $candidate --version 2>&1
|
||||
}
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$Python = $candidate
|
||||
Write-Host "解释器:$candidate($version)" -ForegroundColor Green
|
||||
break
|
||||
}
|
||||
} catch { continue }
|
||||
$versionOutput = & $candidate -c "import sys;print('.'.join(map(str,sys.version_info[:3])))" 2>$null
|
||||
} catch {
|
||||
Write-Host " [跳过] $candidate —— 无法执行" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " [跳过] $candidate —— 无法执行" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
$version = "$versionOutput".Trim()
|
||||
|
||||
# 版本门槛:`app/` 里用了 datetime.UTC(3.11 起才有),3.10 必然导入失败。
|
||||
$parts = $version.Split(".")
|
||||
$major = [int]$parts[0]
|
||||
$minor = [int]$parts[1]
|
||||
if ($major -lt 3 -or ($major -eq 3 -and $minor -lt 11)) {
|
||||
Write-Host " [跳过] $candidate —— Python $version,本项目要求 3.11+" -ForegroundColor DarkYellow
|
||||
continue
|
||||
}
|
||||
|
||||
# 依赖门槛:只看版本号不够,环境可能没装 asyncmy / fastapi。
|
||||
# `$ErrorActionPreference = "Stop"` 下 native 命令写 stderr 会变成终止性错误,
|
||||
# 所以 try 住,把报错当"这个候选不合适",而不是让整个脚本崩掉。
|
||||
$probeOutput = ""
|
||||
try {
|
||||
$probeOutput = & $candidate -c "import fastapi, sqlalchemy, asyncmy, pydantic" 2>&1
|
||||
} catch {
|
||||
$probeOutput = "$_"
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$reason = ("$probeOutput" -split "`n" |
|
||||
Where-Object { $_ -match "Error" } | Select-Object -First 1)
|
||||
Write-Host " [跳过] $candidate —— Python $version 但依赖不全:$reason" -ForegroundColor DarkYellow
|
||||
continue
|
||||
}
|
||||
|
||||
$Python = $candidate
|
||||
Write-Host "解释器:$candidate(Python $version,依赖完整)" -ForegroundColor Green
|
||||
break
|
||||
}
|
||||
|
||||
if (-not $Python) {
|
||||
Write-Host "[失败] 找不到可用的 Python。请先建虚拟环境,或把解释器路径加进本脚本的候选列表。" -ForegroundColor Red
|
||||
Write-Host "`n[失败] 找不到能运行本项目的 Python 环境。" -ForegroundColor Red
|
||||
Write-Host " 需要 3.11+ 且装齐 fastapi / sqlalchemy / asyncmy / pydantic。" -ForegroundColor Red
|
||||
Write-Host " 可以先建虚拟环境:" -ForegroundColor Yellow
|
||||
Write-Host " python -m venv .venv" -ForegroundColor Yellow
|
||||
Write-Host " .venv\Scripts\pip install -e ." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 依赖检查
|
||||
if (-not $SkipChecks) {
|
||||
Write-Host "`n--- 依赖服务检查 ---" -ForegroundColor Cyan
|
||||
|
||||
# 先处理 Milvus:它跑在 Docker 里,而 Docker Desktop 不会常驻,
|
||||
# 是最常见的"平台看着正常、知识检索却悄悄降级"的原因。
|
||||
if (-not (Test-PortOpen "127.0.0.1" 19530)) {
|
||||
Start-DockerIfPossible | Out-Null
|
||||
}
|
||||
|
||||
$services = @(
|
||||
@{ Name = "MySQL"; HostName = "127.0.0.1"; Port = 3306; Required = $true },
|
||||
@{ Name = "Redis"; HostName = "127.0.0.1"; Port = 6379; Required = $false },
|
||||
@{ Name = "Milvus"; HostName = "127.0.0.1"; Port = 19530; Required = $false }
|
||||
)
|
||||
foreach ($service in $services) {
|
||||
$reachable = $false
|
||||
try {
|
||||
$reachable = (Test-NetConnection -ComputerName $service.HostName -Port $service.Port `
|
||||
-InformationLevel Quiet -WarningAction SilentlyContinue)
|
||||
} catch { $reachable = $false }
|
||||
$reachable = Test-PortOpen $service.HostName $service.Port
|
||||
|
||||
if ($reachable) {
|
||||
Write-Host (" [OK] {0,-7} {1}:{2}" -f $service.Name, $service.HostName, $service.Port) -ForegroundColor Green
|
||||
@@ -127,24 +247,53 @@ function Start-PlatformWindow {
|
||||
param([string]$Title, [string[]]$Arguments)
|
||||
$shell = if (Get-Command pwsh -ErrorAction SilentlyContinue) { "pwsh" } else { "powershell" }
|
||||
$command = "`$host.UI.RawUI.WindowTitle = '$Title'; & '$Python' " + ($Arguments -join " ")
|
||||
Start-Process -FilePath $shell -ArgumentList "-NoExit", "-Command", $command | Out-Null
|
||||
# 显式给 WorkingDirectory:`app.main:app` 与 `app.worker` 都是相对当前目录导入的,
|
||||
# 继承错目录会直接 ModuleNotFoundError。
|
||||
Start-Process -FilePath $shell -ArgumentList "-NoExit", "-Command", $command `
|
||||
-WorkingDirectory $Root | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "`n--- 启动进程 ---" -ForegroundColor Cyan
|
||||
Start-PlatformWindow -Title "平台 API :$Port" -Arguments @(
|
||||
"-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$Port"
|
||||
)
|
||||
Write-Host " [已启动] API 窗口(uvicorn,端口 $Port)" -ForegroundColor Green
|
||||
|
||||
if (-not $ApiOnly) {
|
||||
# 幂等:重复双击启动脚本不应该起出第二个 API / Worker。
|
||||
# API 用端口判断;Worker 没有端口,用进程命令行判断。
|
||||
if (Test-PortOpen "127.0.0.1" $Port 500) {
|
||||
Write-Host " [跳过] 端口 $Port 已有服务在跑(不重复启动 API)。若那不是本平台,请换端口:-Port 8100" -ForegroundColor Yellow
|
||||
} else {
|
||||
Start-PlatformWindow -Title "平台 API :$Port" -Arguments @(
|
||||
"-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$Port"
|
||||
)
|
||||
Write-Host " [已启动] API 窗口(uvicorn,端口 $Port)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if ($ApiOnly) {
|
||||
Write-Host " [跳过] Worker(-ApiOnly):客服对话会一直 queued、新知识不会进 Milvus" -ForegroundColor Yellow
|
||||
} elseif (Test-WorkerRunning) {
|
||||
Write-Host " [跳过] 已有 Worker 在跑(不重复启动)" -ForegroundColor Yellow
|
||||
} else {
|
||||
Start-PlatformWindow -Title "平台 Agent Worker" -Arguments @("-m", "app.worker")
|
||||
Write-Host " [已启动] Worker 窗口(Agent 对话 / 知识向量 / 记忆抽取)" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 就绪探测
|
||||
# 双击启动时最怕"窗口起了但其实没起来"。这里等 API 真正应答再报成功。
|
||||
Write-Host "`n--- 等待 API 就绪 ---" -ForegroundColor Cyan
|
||||
$ready = $false
|
||||
for ($i = 1; $i -le 30; $i++) {
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/internal/health/ready" `
|
||||
-TimeoutSec 2 -UseBasicParsing -ErrorAction Stop
|
||||
if ($response.StatusCode -eq 200) { $ready = $true; break }
|
||||
} catch { }
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if ($ready) {
|
||||
Write-Host " [OK] API 已就绪(约 $i 秒)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host " [跳过] Worker(-ApiOnly):客服对话会一直 queued、新知识不会进 Milvus" -ForegroundColor Yellow
|
||||
Write-Host " [警告] 等了 30 秒 API 仍未就绪:切到 API 窗口看报错(常见原因是数据库连不上)" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- 访问入口
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host "`n=== 访问入口 ===" -ForegroundColor Cyan
|
||||
Write-Host " 门户首页 http://127.0.0.1:$Port/portal/"
|
||||
Write-Host " 访客页 http://127.0.0.1:$Port/portal/guest/home/"
|
||||
@@ -160,8 +309,22 @@ Write-Host " 投顾 advisor_t / abc12345"
|
||||
Write-Host " 运营 offsite_t / offsite123"
|
||||
|
||||
Write-Host "`n提示:" -ForegroundColor Yellow
|
||||
Write-Host " · 首次使用先准备数据:python tools/seed_demo_data.py"
|
||||
Write-Host " · 首次使用先准备数据:python tools\seed_demo_data.py"
|
||||
Write-Host " · 行情有效期只有 15 分钟:演示中途下单若报 503,在新窗口重跑"
|
||||
Write-Host " python tools/sync_market_prices.py (立即生效,无需重启服务)"
|
||||
Write-Host " · 体检:python tools/e2e_smoke_test.py"
|
||||
Write-Host " python tools\sync_market_prices.py (立即生效,无需重启服务)"
|
||||
Write-Host " · 体检:python tools\e2e_smoke_test.py (业务链路,40 项)"
|
||||
Write-Host " python tools\portal_api_check.py (接口契约,41 项)"
|
||||
Write-Host " · 演示流程见 docs/44-演示流程.md"
|
||||
|
||||
# ---------------------------------------------------------------- 打开浏览器
|
||||
# 服务已经探测就绪,此时打开才不会看到"无法访问"。用户可以从 bat 传 -NoBrowser 关掉。
|
||||
if (-not $NoBrowser -and $ready) {
|
||||
try {
|
||||
Start-Process "http://127.0.0.1:$Port/portal/" | Out-Null
|
||||
Write-Host "`n已打开浏览器:http://127.0.0.1:$Port/portal/" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "`n(自动打开浏览器失败,请手动访问 http://127.0.0.1:$Port/portal/)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n=== 启动完成。服务在独立窗口里运行,关掉本窗口不影响它们。===" -ForegroundColor Cyan
|
||||
|
||||
Reference in New Issue
Block a user