· XingAI Invest AI
保存偏好不是投资决策:在不破坏 Worker 边界的前提下追踪股票
金融决策系统里有两类完全不同的状态:
决策状态:
top signals、排名、风险、置信度、symbol overlays
偏好状态:
追踪股票、通知开关、自定义 slots
把它们混在一起会产生 bug。更糟的是,系统会悄悄在决策引擎之外开始“做决策”。
我们在给 XingAI Invest AI 增加 My Tracked Stocks 时遇到了这个问题。
用户希望添加 AVL、TSLL 这样的代码。表面上很简单,但第一版暴露了两个设计问题:
- 把“保存追踪股票”当成了需要 paid-style 认证的操作。
- 把 worker 缓存过期显示成了大型红色前端错误。
两者都能理解,但都不是正确的产品行为。
必须保住的边界
Invest AI 有一条硬规则:
Worker 计算决策。
FastAPI 读取缓存。
Frontend 渲染。
因此修复方案不能是:
- worker stale 时让 FastAPI 重新计算 top signals;
- 让 React 从 raw market fields 推断排名;
- 把追踪股票只存在浏览器里,导致 worker 无法读取。
追踪股票列表必须存在 SQLite,因为 worker 后续需要读取它来发送信号变化提醒。但保存一个 symbol 本身不是投资决策。
把问题拆成三条路径
最终我们把流程拆成三条路径:
追踪股票
用户偏好
SQLite
匿名或登录用户都可用
自定义 symbol slots
paid/admin 权限
需要认证
SQLite slot 列表
AI 信号排名
worker 决策输出
SQLite 缓存
FastAPI stale gate 保护
这个拆分就是架构。
匿名用户也需要后端身份键
产品支持匿名用户,所以给下面这个接口强制要求 Bearer token:
POST /api/v1/signal-tracking/AVL
会在线上失败:
Missing or invalid Authorization header
修复方案不是把数据存在 localStorage。修复方案是使用浏览器维度的后端 key:
X-Client-Id: <stable browser id>
前端对匿名用户发送 X-Client-Id,有登录 token 时再发送 Authorization: Bearer <jwt>。FastAPI 这样解析 user key:
if current_user:
user_id = current_user["user_id"]
else:
user_id = get_identifier_from_request(request) # 优先 X-Client-Id
事实源仍然是 SQLite:
user_notification_settings.tracked_symbols
浏览器 id 只是用来找到那一行数据。
Admin 权限是授权,不是计算
我们还需要让配置的运维邮箱在没有付费账单的情况下测试 paid custom symbol slots。
收窄后的修复是在 Fly secrets 里配置逗号分隔的运维邮箱白名单。它只授予 custom slot entitlement,不授予 FastAPI 计算信号的权力。
即使是 admin:
- FastAPI 可以写 slot list;
- FastAPI 可以选择缓存输出;
- FastAPI 可以返回
WORKER_DATA_STALE; - FastAPI 不可以拉 live market data 并现场生成决策。
这就是授权逻辑和决策逻辑的区别。
Worker stale 是一种 UX 状态
当 worker 缓存过期时,FastAPI 正确返回:
WORKER_DATA_STALE
Signals 页面最初把它显示成大型红色 “Failed to load signals”。但页面没有坏。追踪列表仍然有效。不可用的只是 worker 拥有的排名快照。
现在 UI 把它显示成降级状态:
AI signals are waiting for a fresh worker cache.
Your tracked stocks remain saved below.
Signal rankings will return after the worker writes a fresh decision snapshot.
不伪造排名。不在前端推断。不在请求路径重算。只是诚实地展示状态。
设计教训
用户保存 TSLL,这是偏好。
系统说 TSLL 是买入,这是决策。
worker stale,这是运行状态。
三者应该在不同地方处理。
SQLite preferences:
追踪股票、通知开关
SQLite decision cache:
top signals、排名、macro radar、overlays
FastAPI:
user key 解析、偏好 CRUD、缓存读取
Worker:
决策生成、stale 语义、提醒派发
Frontend:
渲染已保存偏好并解释缓存状态
小 bug 是 “AVL 添加后不显示”。
真正的设计教训是:用户偏好应该容易保存,但决策智能必须很难被伪造。
延伸阅读: XingAI Invest AI repo 中的 ADR-020。