diff --git a/demo.ps1 b/demo.ps1 new file mode 100644 index 0000000..116409c --- /dev/null +++ b/demo.ps1 @@ -0,0 +1,220 @@ +<# +.SYNOPSIS + 客服 Agent 演示 · 前端一键启动(服务拉起 + 五项自检 + 打开演示页面) + +.DESCRIPTION + 真正的服务拉起交给同目录的 `start.ps1`(**幂等**:缺什么补什么,重复执行不会起出 + 第二个 API/Worker)。本脚本在它之上补三件事: + + 1. **刷新行情** —— 下单要求行情快照落在 **15 分钟**有效期内 + (`app/service/trade_service.py` 的 `MAX_QUOTE_AGE`),过期后所有委托直接 + 503「行情已过期」,而系统**没有自动刷新机制**;所以每次启动都刷一次。 + 2. **五项自检** —— 口径与 `客服agent\D2.5` §1 一致(容器 / 向量库 / Worker / 行情 / 向量库开关)。 + 3. **打开前端页面** —— 访客页(客服浮窗主战场)与客户登录页。 + + ⚠️ **登录限流**:`.env` 的 `RATE_LIMIT_*` 是 **10 次 / 60 秒**。演示时不要反复登录 —— + 连续登录会让后续用例整片报红,看起来像「客服坏了」,其实是限流。 + +.PARAMETER Port + API 端口,默认 8000(与 `start.ps1 -Port` 同义)。 + +.PARAMETER SkipStart + 不调 `start.ps1`,只做自检 + 开页面(服务已经在跑时用)。 + +.PARAMETER NoBrowser + 不自动打开浏览器。 + +.PARAMETER KeepPrices + 不刷新行情(只看自检结果时用)。 + +.EXAMPLE + 双击 `启动演示.bat` + powershell -ExecutionPolicy Bypass -File demo.ps1 -SkipStart -NoBrowser + +.NOTES + 本文件必须保存为 **UTF-8 with BOM**(理由同 `start.ps1`:缺 BOM 时 Windows PowerShell 5.1 + 按 ANSI 解析,中文会乱码并抛语法错误)。 +#> +param( + [int]$Port = 8000, + [switch]$SkipStart, + [switch]$NoBrowser, + [switch]$KeepPrices +) + +$ErrorActionPreference = "Continue" +$Root = $PSScriptRoot +Set-Location $Root + +$Python = Join-Path $Root ".venv\Scripts\python.exe" +if (-not (Test-Path $Python)) { $Python = "python" } +$Base = "http://127.0.0.1:$Port" +$script:Fails = 0 + +function Section($text) { Write-Host ""; Write-Host "--- $text ---" -ForegroundColor Cyan } +function Ok($text) { Write-Host " [OK] $text" -ForegroundColor Green } +function Warn($text) { Write-Host " [警告] $text" -ForegroundColor Yellow; $script:Fails++ } +function Bad($text) { Write-Host " [失败] $text" -ForegroundColor Red; $script:Fails++ } + +function Test-ApiRunning { + try { + $null = Invoke-WebRequest -Uri "$Base/internal/health/ready" -TimeoutSec 2 -UseBasicParsing -ErrorAction Stop + return $true + } catch { return $false } +} + +function Wait-ApiReady { + for ($i = 1; $i -le 30; $i++) { + if (Test-ApiRunning) { return $true } + Start-Sleep -Seconds 1 + } + return $false +} + +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host " 客服 Agent 演示 · 前端一键启动" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan +Write-Host "工作目录:$Root" +Write-Host "Python :$Python" + +$apiWasRunning = Test-ApiRunning + +# ---------------------------------------------------------------- 0. 拉起服务 +if ($SkipStart) { + Section "0 服务拉起(-SkipStart:跳过 start.ps1)" + if ($apiWasRunning) { Ok "API 已在跑,直接进入自检" } else { Bad "API 未就绪,且按 -SkipStart 未拉起服务" } +} else { + Section "0 服务拉起(调用 start.ps1,幂等)" + & (Join-Path $Root "start.ps1") -Port $Port -NoBrowser -SkipPriceSync + if ($LASTEXITCODE -eq 1) { Bad "start.ps1 判定依赖缺失(常见:MySQL 未启动)—— 详见上方输出" } +} + +Section "等待 API 就绪" +if (Wait-ApiReady) { Ok "API 已就绪:$Base/internal/health/ready" } +else { Bad "等了 30 秒仍未就绪:切到 API 窗口看报错(常见原因是数据库连不上)" } + +# ---------------------------------------------------------------- 1~5. 五项自检 +Section "自检 1/5 · 容器运行时(Milvus 三件套)" +if (Get-Command docker -ErrorAction SilentlyContinue) { + $containers = @(& docker ps --format "{{.Names}} {{.Status}}" 2>$null) + $milvus = @($containers | Where-Object { $_ -like "*milvus*" }) + if ($milvus.Count -ge 3) { Ok "milvus 容器 $($milvus.Count) 个在跑:$($milvus -join " | ")" } + elseif ($milvus.Count -gt 0) { Warn "只看到 $($milvus.Count) 个 milvus 容器(期望 3 个:standalone / etcd / minio)" } + else { Warn "没看到 milvus 容器:知识检索会静默降级,先起 Docker Desktop" } +} else { + Warn "没找到 docker 命令,本项跳过" +} + +Section "自检 2/5 · 向量库可查(三个集合计数)" +$probe = @' +import sys +sys.path.insert(0, '.') +from app.core.config import get_settings as g +from pymilvus import MilvusClient +client = MilvusClient(uri=g().resolved_milvus_uri) +for name in ("fin_faq_collection", "fin_product_collection", "fin_policy_collection"): + print(name, client.query(collection_name=name, filter='', output_fields=['count(*)'])) +'@ +# 经 stdin 送给 python,不走命令行参数:PowerShell 5.1 传原生命令参数时会吞掉内嵌双引号, +# 而这段探测代码必须保留字符串字面量(集合名与 output_fields)。 +$probeOut = $probe | & $Python - 2>&1 +if ($LASTEXITCODE -eq 0 -and (($probeOut | Out-String) -like "*count*")) { + Ok "三个集合可查:" + $probeOut | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } +} else { + Warn "向量库探测失败:知识题会走 E5b 兜底(答不出但不会编)" + $probeOut | Select-Object -Last 4 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } +} + +Section "自检 3/5 · Agent Worker 在跑" +$pythonProcs = Get-CimInstance Win32_Process -Filter "Name like '%python%'" -ErrorAction SilentlyContinue +$workerProcs = @($pythonProcs | Where-Object { $_.CommandLine -like "*app.worker*" }) +if ($workerProcs.Count -gt 0) { Ok "Worker 在跑(PID $($workerProcs[0].ProcessId))" } +else { Bad "没有 Worker:客服对话会一直 queued,前端显示「繁忙 / 超时」" } + +Section "自检 4/5 · 行情有效期(下单前置,15 分钟)" +if ($KeepPrices) { + Warn "按 -KeepPrices 跳过刷新:若距上次刷新超过 15 分钟,下单会 503" +} else { + $syncOut = & $Python "tools\sync_market_prices.py" 2>&1 + if ($LASTEXITCODE -eq 0) { + Ok "行情已刷新,15 分钟窗口从此刻重新计时" + $syncOut | Where-Object { $_ -like "*落库*" } | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } else { + Warn "行情刷新失败(退出码 $LASTEXITCODE):下单可能返回 503" + $syncOut | Select-Object -Last 5 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } + } +} + +Section "自检 5/5 · 本地向量库开关(应为空:非空会绕过服务端 Milvus)" +$switchOut = & $Python -c "import sys; sys.path.insert(0, '.'); from app.core.config import get_settings as g; print(repr(g().milvus_local_uri)); print(g().resolved_milvus_uri)" 2>&1 +if ($LASTEXITCODE -eq 0) { + $localUri = ($switchOut | Select-Object -First 1) + if ($localUri -eq "''") { Ok "milvus_local_uri 为空(符合预期);resolved = $($switchOut | Select-Object -Last 1)" } + else { Warn "milvus_local_uri 不为空:$localUri —— 演示前请清空该开关" } +} else { + Warn "配置读取失败" + $switchOut | Select-Object -Last 4 | ForEach-Object { Write-Host " $_" -ForegroundColor DarkGray } +} + +# ---------------------------------------------------------------- 演示入口 +$guestUrl = "$Base/portal/guest/home/" +$customerUrl = "$Base/portal/customer/login/" +$staffUrl = "$Base/portal/employee-console/login/" + +Section "演示入口" +Write-Host " 访客页(客服浮窗主战场) $guestUrl" +Write-Host " 客户登录 $customerUrl" +Write-Host " 员工登录(风控/运营/管理员)$staffUrl" +Write-Host " 投顾工作台 $Base/portal/employee-advisor/dashboard/" +Write-Host " 接口文档 $Base/docs" + +if ($NoBrowser) { + Write-Host " (-NoBrowser:未自动打开浏览器)" -ForegroundColor DarkGray +} elseif (Test-ApiRunning) { + try { + Start-Process $guestUrl | Out-Null + Start-Sleep -Milliseconds 600 + Start-Process $customerUrl | Out-Null + Ok "已打开两个页面(访客页 / 客户登录)" + } catch { + Warn "自动打开浏览器失败,请手动访问上面的地址" + } +} + +# ---------------------------------------------------------------- 账号与提醒 +Section "演示账号(实测可登录)" +Write-Host " 客户 cust_t / 123456" +Write-Host " 风控专员 risk_t / 666666" +Write-Host " 管理员 admin_t / 88888888" +Write-Host " 运营 offsite_t / offsite123" +Write-Host " 投顾 advisor_t / abc12345 (2026-09-20 恢复)" +Write-Host " review_t :登录返回 401 是设计如此(只作权限夹具)" -ForegroundColor DarkGray + +Section "开讲前必看" +Write-Host " · 登录限流 10 次 / 60 秒:不要反复登录,否则后续整片报红(假故障)" +Write-Host " · 行情 15 分钟:中途下单报 503 就在新窗口重跑" +Write-Host " & `"$Python`" tools\sync_market_prices.py" +Write-Host " · 演示台词与话术:客服agent\D2.5-客服Agent演示脚本与账号速查-2026-09-19.md" +Write-Host " · 答辩问答准备 :客服agent\D2.6-客服Agent答辩报告-2026-09-19.md" + +if ($apiWasRunning -and -not $SkipStart) { + Section "提示:服务已在跑,本次没有新开日志窗口" + Write-Host " 当前 API/Worker 可能是无窗口方式启动的。想要可见日志窗口,先停掉再双击本脚本:" + $stopCmd = @' +Get-CimInstance Win32_Process -Filter "Name like '%python%'" | Where-Object { $_.CommandLine -match 'uvicorn app.main:app|app.worker' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force } +'@ + Write-Host $stopCmd -ForegroundColor DarkGray +} + +Write-Host "" +Write-Host "============================================================" -ForegroundColor Cyan +if ($script:Fails -eq 0) { + Write-Host " 五项自检全过,演示环境就绪。" -ForegroundColor Green +} else { + Write-Host " 自检有 $script:Fails 项需要处理,见上方 [警告] / [失败] 行。" -ForegroundColor Yellow +} +Write-Host " 服务在独立窗口里运行,关掉本窗口不影响它们。" -ForegroundColor Cyan +Write-Host "============================================================" -ForegroundColor Cyan + +if ($script:Fails -gt 0) { exit 1 } else { exit 0 } diff --git a/启动演示.bat b/启动演示.bat new file mode 100644 index 0000000..d2acd71 --- /dev/null +++ b/启动演示.bat @@ -0,0 +1,40 @@ +@echo off +chcp 936 >nul +title ¿ͷþ Agent ÑÝʾ -- ǰ¶Ëһ¼üÆô¶¯ + +rem ============================================================ +rem ˫»÷±¾Îļþ£ºÀ­Æð·þÎñ -^> ÎåÏî×Լì -^> ´ò¿ªÑÝʾҳÃæ +rem Â߼­¶¼ÔÚͬĿ¼µÄ demo.ps1 À±¾Îļþֻ¸ºÔð°ѲÎÊý͸´«¹ýȥ¡£ +rem +rem ²ÎÊýʾÀý£º +rem Æô¶¯ÑÝʾ.bat -NoBrowser ²»×Զ¯´ò¿ªä¯ÀÀÆ÷ +rem Æô¶¯ÑÝʾ.bat -SkipStart ·þÎñÒÑÔÚÅܣ¬ֻ×Լì + ¿ªҳÃæ +rem Æô¶¯ÑÝʾ.bat -Port 8100 »»¶˿Ú +rem ============================================================ + +set "ROOT=%~dp0" +cd /d "%ROOT%" + +if not exist "%ROOT%demo.ps1" ( + echo [´íÎó] ÕҲ»µ½ demo.ps1£¬ÇëȷÈÏËüÓ뱾ÎļþÔÚͬһĿ¼¡£ + pause + exit /b 1 +) + +powershell -NoProfile -ExecutionPolicy Bypass -File "%ROOT%demo.ps1" %* +set "RC=%ERRORLEVEL%" + +echo. +echo ============================================================ +if "%RC%"=="0" ( + echo ÑÝʾ»·¾³¾ÍÐ÷¡£·þÎñÔڶÀÁ¢´°¿ÚÀïÔËÐУ¬¹صô±¾´°¿ڲ»ӰÏìËüÃǡ£ +) else ( + echo ×ԼìÓÐÏîĿÐèҪ´¦Àí£¨·µ»ØÂë %RC%£©£¬ÇëÏòÉϷ­¿´»Æɫ / ºìɫÐС£ +) +echo. +echo ³£Óò¹¾ÈÃüÁÔڱ¾Ŀ¼Ð¿ªÖնËִÐУ©£º +echo .venv\Scripts\python.exe tools\sync_market_prices.py ˢÐÂÐÐÇ飨15 ·ÖÖÓÓÐЧ£© +echo .venv\Scripts\python.exe tools\e2e_smoke_test.py ҵÎñÁ´·ðÑÌ +echo .venv\Scripts\python.exe tools\portal_api_check.py ½ӿÚÆõԼÌå¼ì +echo ============================================================ +pause