"""手工重建客户画像:中期记忆 → 长期事实 → 画像 + 版本快照。 用法: ```powershell # 重建一个客户 python tools/rebuild_profile.py 9001 # 重建全部有记忆的客户 python tools/rebuild_profile.py --all ``` 为什么需要这个入口:画像组装目前没有自动触发(记忆写入后不会立刻重建画像), 在把它接上 Worker 事件之前,这是唯一的触发方式,也便于运维排查"画像为什么没更新"。 关于输出里的 `reason=profile_row_not_opened`:客户尚未开户时不会创建画像行 (`trade_account` 等身份字段是 NOT NULL,属注册/开户流程所有),这是**正确行为**而非失败。 """ import argparse import asyncio import sys from sqlalchemy import select from app.infrastructure.db import SessionFactory from app.model.memory import MemoryUnit from app.service.profile_assembly_service import ProfileAssemblyService sys.stdout.reconfigure(errors="replace") async def rebuild_one(customer_id: int) -> None: async with SessionFactory() as session: async with session.begin(): outcome = await ProfileAssemblyService(session).rebuild(customer_id) print(f"\n== 客户 {customer_id} ==") if outcome.get("profile") is None: print(f" 未重建画像:{outcome.get('reason')}" f"(尚未开户;已提升事实 {len(outcome.get('promoted_keys') or [])} 条)") else: print(f" 提升事实:{outcome.get('promoted_keys')}") print(f" 画像内容:{outcome.get('profile')}") print(f" 生成依据:{outcome.get('generation_basis')}") async def main() -> int: parser = argparse.ArgumentParser(description="重建客户画像") parser.add_argument("customer_id", nargs="?", type=int, help="客户 id") parser.add_argument("--all", action="store_true", help="重建全部有记忆的客户") args = parser.parse_args() if args.all: async with SessionFactory() as session: rows = list(await session.scalars( select(MemoryUnit.customer_id).distinct() )) targets = [int(row) for row in rows] if not targets: print("没有任何客户有记忆数据,无需重建") return 0 print(f"将重建 {len(targets)} 个客户:{targets}") for customer_id in targets: await rebuild_one(customer_id) return 0 if args.customer_id is None: parser.print_help() return 2 await rebuild_one(args.customer_id) return 0 sys.exit(asyncio.run(main()))