Files
group_fqcd_jr/tools/seed_demo_data.py
T
lzf_0626 bfd3964ef8 feat(demo): 演示数据一键准备、启动脚本与演示流程文档
交付"别人能自己把系统跑起来做演示"所需的四件东西:

- tools/seed_demo_data.py      演示数据一键准备:10 步按依赖排序
  (此前散在 10 个脚本里,没人知道该跑哪些、按什么顺序跑,且知识库那步
  根本没有脚本、靠手工调接口)
- tools/seed_knowledge_demo.py 知识库演示素材灌入,幂等(先删同 source_file 再灌)
- start.ps1                    启动 API + Worker,含依赖检查与**自动刷新行情**
- docs/44-演示流程.md           8 个主线场景的照读流程 + 排障表 + 账号/命令速查

两条硬约束同时写进了脚本和文档:

1. **Worker 必须常驻**:没有它客服对话一直停在 queued(前端只显示"超时")、
   新知识不进 Milvus 且**没有任何报错**。
2. **行情有效期仅 15 分钟**(`trade_service.MAX_QUOTE_AGE`):超时后所有委托
   直接 503「行情已过期」,而系统**没有自动刷新机制**。故 start.ps1 启动时刷一次,
   文档另给"演示中途 503 时补刷、无需重启服务"的处置方法(已实测)。

实测证据:
- start.ps1 在 8099 完整启动,`/docs` 与 `/portal/` 均 HTTP 200(测试进程已清理)
- `tools/e2e_smoke_test.py` → 40/40 通过
- `tools/seed_demo_data.py --dry-run` → 10 步全部正常列出
- 文档中的下单命令实跑:510300 成交价 4.579、金额 457.90、手续费 0.05
2026-09-13 21:56:27 +08:00

139 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""一键准备演示数据:按依赖顺序跑齐所有 seed 脚本。
## 为什么需要它
演示数据此前散在 10 个脚本里,**顺序有讲究**(账号 → 口令 → 账户 → 行情 → 配置 → 知识),
而且有一个环节(知识库素材)根本没有脚本、靠手工调接口。换台机器接手时没人知道该跑哪些、
按什么顺序跑。本脚本是唯一入口。
## 顺序与依赖
| # | 步骤 | 为什么在这个位置 |
|---|---|---|
| 1 | 账号与权限 | 后面所有步骤都要用到这些账号 |
| 2 | 演示口令 | 依赖 1 建出的账号 |
| 3 | 客户账户与持仓 | 客户页面与下单的前提 |
| 4 | 场内行情 | **下单的硬前置**;必须在演示前跑,行情会过期 |
| 5 | 风控预警样本 | 风控页面要有东西可看 |
| 6 | 投顾演示数据 | 投顾工作台要有方案可看 |
| 7-9 | 各类发布配置 | Agent 工具白名单与提示词,缺了客服/风控会"失败关闭" |
| 10 | 知识库素材 | 客服答得出问题的前提 |
## ⚠️ 两个必须知道的点
1. **最后一步与行情都需要 Agent Worker 才会真正生效**:知识入库只写 MySQL + 投 outbox 事件,
向量由 Worker 消费事件后写 Milvus;没有 Worker 时现象是"客服照旧答不上",且**没有报错**。
所以跑完本脚本**必须**再起 Worker(`start.ps1` 会一起起)。
2. **第 2 步不是幂等的**:`set_user_password.py` 重跑等于**重设密码**(bcrypt 每次加盐不同)。
这是有意的(改密就该覆盖),但要知道它不是"已存在就跳过"。
## 用法
python tools/seed_demo_data.py # 全跑
python tools/seed_demo_data.py --from 4 # 从第 4 步开始(重跑行情等)
python tools/seed_demo_data.py --only 4,10 # 只跑指定步骤
python tools/seed_demo_data.py --list # 只列步骤
"""
from __future__ import annotations
import argparse
import subprocess
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr]
#: (标题, 脚本相对路径, 备注)
STEPS: tuple[tuple[str, str, str], ...] = (
("账号与权限", "tools/seed_test_rbac.py", "五个演示角色 + 权限号段 9001-9046"),
("演示口令", "tools/set_user_password.py", "⚠️ 非幂等:重跑等于重设密码"),
("客户账户与持仓", "tools/seed_sim_account_demo.py", "客户 9001 开 10 万虚拟资金 + 持仓"),
("场内行情", "tools/sync_market_prices.py", "⚠️ 下单硬前置;行情会过期,演示前必跑"),
("风控预警样本", "tools/seed_risk_alert_demo_data.py", "三条不同状态的演示预警"),
("投顾演示数据", "tools/seed_advisor_demo.py", "投顾工作台要展示的方案与归属"),
("风控 Agent 白名单", "tools/publish_risk_agent_config.py", "缺了风控助手工具会失败关闭"),
("客服配置白名单", "tools/publish_customer_service_config.py", "缺了客服工具会失败关闭"),
("客服闲聊提示词", "tools/publish_chitchat_prompt.py", "话术走发布配置,不硬编码"),
("知识库素材", "tools/seed_knowledge_demo.py", "⚠️ 需要 Worker 才会写进 Milvus"),
)
def run_step(index: int, title: str, script: str, note: str, *, dry_run: bool) -> bool:
path = PROJECT_ROOT / script
print()
print("=" * 88)
print(f"[{index}/{len(STEPS)}] {title} —— {note}")
print(f" {script}")
print("=" * 88)
if not path.exists():
print(f"[跳过] 脚本不存在:{path}")
return False
if dry_run:
print("[dry-run] 未执行")
return True
started = time.perf_counter()
# 刻意**不捕获输出**(继承 stdio):既不吞掉子脚本的报错,
# 也避免在受限环境里因 piped stdio 失败。
result = subprocess.run([sys.executable, str(path)], check=False, cwd=str(PROJECT_ROOT))
elapsed = time.perf_counter() - started
ok = result.returncode == 0
print(f"[{'完成' if ok else '失败'}] {title} 用时 {elapsed:.1f}s 退出码 {result.returncode}")
return ok
def main() -> int:
parser = argparse.ArgumentParser(description="一键准备演示数据")
parser.add_argument("--list", action="store_true", help="只列步骤")
parser.add_argument("--from", dest="start", type=int, default=1, help="从第几步开始")
parser.add_argument("--only", default="", help="只跑这些步骤(逗号分隔,如 4,10)")
parser.add_argument("--dry-run", action="store_true", help="只打印将执行什么")
args = parser.parse_args()
if args.list:
print(f"共 {len(STEPS)} 步:")
for index, (title, script, note) in enumerate(STEPS, 1):
print(f" {index:>2}. {title:<20} {script:<48} {note}")
return 0
only = {int(part) for part in args.only.split(",") if part.strip()} if args.only else None
def wanted(index: int) -> bool:
return index in only if only is not None else index >= args.start
selected = [(index, *step) for index, step in enumerate(STEPS, 1) if wanted(index)]
if not selected:
print("没有匹配的步骤。")
return 1
print(f"准备演示数据:{len(selected)} 步" + ("(dry-run)" if args.dry_run else ""))
failed: list[str] = []
for index, title, script, note in selected:
if not run_step(index, title, script, note, dry_run=args.dry_run):
failed.append(title)
print()
print("=" * 88)
if failed:
print(f"有 {len(failed)} 步失败:{'、'.join(failed)}")
print("先看上面的原始报错;多数失败是外部依赖没起来(MySQL / Redis / Docker)。")
return 1
print("全部完成。")
if not args.dry_run:
print(
"\n下一步:\n"
" 1. 起服务:powershell -ExecutionPolicy Bypass -File start.ps1\n"
" (它会把 API 与 **Agent Worker** 一起起 —— 没有 Worker,知识检索不到、\n"
" 客服对话会一直显示'超时')\n"
" 2. 验证:python tools/e2e_smoke_test.py"
)
return 0
if __name__ == "__main__":
sys.exit(main())