Files
group_fqcd_jr/tools/seed_demo_data.py
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

182 lines
8.6 KiB
Python
Raw Permalink 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 | 历史净值 | 产品详情页净值走势图的数据源(`fin_nav_history`) |
| 6 | 风控预警样本 | 风控页面要有东西可看 |
| 7 | 投顾演示数据 | 投顾工作台要有方案可看 |
| 8-10 | 各类发布配置 | Agent 工具白名单与提示词,缺了客服/风控会"失败关闭" |
| 11 | 知识库素材 | 客服答得出问题的前提 |
| 12 | 风控扫描演示数据 | 造**上游**业务数据,让扫描器自己生成预警(客户 12001-12005) |
⚠️ **第 6 步与第 12 步是两套不同的风控演示数据,不是重复**:
- 第 6 步 `seed_risk_alert_demo_data.py` **直接摆好**三条不同状态的预警,保证风控页面
一打开就有东西可看;
- 第 12 步 `seed_risk_demo_data.py` **只造上游**(客户/产品/账户/持仓/交易/流水/登录/工单),
**刻意不写 `fin_risk_alert`**,让风控扫描器按规则生成 —— 演示时覆盖的是
**规则 → 证据 → 通知**完整链路,而不是"预先摆好的假预警"。
**第 12 步需要带参数跑**(它默认是只打印计划、不连库的干跑模式),所以它额外传
`--apply --dry-run-scan`:写入上游数据,然后跑一次规则干跑**并把结果回滚**,
顺带验证 5 个场景都真的命中预期规则。
## ⚠️ 三个必须知道的点
1. **最后一步与行情都需要 Agent Worker 才会真正生效**:知识入库只写 MySQL + 投 outbox 事件,
向量由 Worker 消费事件后写 Milvus;没有 Worker 时现象是"客服照旧答不上",且**没有报错**。
所以跑完本脚本**必须**再起 Worker(`start.ps1` 会一起起)。
2. **第 2 步不是幂等的**:`set_user_password.py` 重跑等于**重设密码**(bcrypt 每次加盐不同)。
这是有意的(改密就该覆盖),但要知道它不是"已存在就跳过"。
3. **第 12 步不依赖 Worker**:它调的是 `RiskRuleEngine.refresh_alerts()`(进程内),
不是通过事件队列;干跑部分结束会 `rollback()`,不会留下正式预警。
## 用法
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 --only 12 # 只准备风控扫描演示数据
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]
#: (标题, 脚本相对路径, 备注, 额外命令行参数)
#:
#: 第四个字段是为「默认行为不是写入」的脚本准备的:`seed_risk_demo_data.py` 不带参数时
#: 只打印计划、**不连数据库**,必须显式传 `--apply` 才会真正写入。不给它传参的话,
#: 加进本脚本只会打印一段"确认无误后使用 --apply 正式写入"然后什么也不做 ——
#: 看起来像成功,实际什么都没准备,这类"假成功"最难查。
STEPS: tuple[tuple[str, str, str, tuple[str, ...]], ...] = (
("账号与权限", "tools/seed_test_rbac.py", "演示角色 + 权限号段", ()),
("演示口令", "tools/set_user_password.py", "⚠️ 非幂等:重跑等于重设密码", ()),
("客户账户与持仓", "tools/seed_sim_account_demo.py", "客户 9001 开 10 万虚拟资金 + 持仓", ()),
("场内行情", "tools/sync_market_prices.py", "⚠️ 下单硬前置;行情会过期,演示前必跑", ()),
("历史净值", "tools/sync_nav_history.py", "产品详情页走势图的数据源(fin_nav_history)", ()),
("风控预警样本", "tools/seed_risk_alert_demo_data.py", "三条不同状态的演示预警", ()),
("投顾演示数据", "tools/seed_advisor_demo.py", "投顾工作台要展示的方案与归属", ()),
("风控 Agent 白名单", "tools/publish_risk_agent_config.py", "缺了风控助手工具会失败关闭", ()),
("知识库素材", "tools/seed_knowledge_demo.py", "⚠️ 需要 Worker 才会写进 Milvus", ()),
(
"风控扫描演示数据",
"tools/seed_risk_demo_data.py",
"上游业务数据(客户 12001-12005),让扫描器自己生成预警",
# 不带参数时它只打印计划、不连库;--dry-run-scan 跑完规则干跑会回滚,
# 所以净效果是「写入上游数据 + 顺带验证 5 个场景都命中预期规则」。
("--apply", "--dry-run-scan"),
),
)
def run_step(
index: int,
title: str,
script: str,
note: str,
extra_args: tuple[str, ...],
*,
dry_run: bool,
) -> bool:
path = PROJECT_ROOT / script
print()
print("=" * 88)
print(f"[{index}/{len(STEPS)}] {title} —— {note}")
# 把额外参数一并打出来:有些脚本不带参数是"只打印计划不写库",
# 手跑时能一眼看出这一步到底会不会落库。
print(" " + " ".join([script, *extra_args]))
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), *extra_args],
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, extra) in enumerate(STEPS, 1):
suffix = f" [{' '.join(extra)}]" if extra else ""
print(f" {index:>2}. {title:<20} {script:<48} {note}{suffix}")
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, extra in selected:
if not run_step(index, title, script, note, extra, 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())