ericwyuan
|
2d9a2be185
|
docs: 文档/配置/测试同步 NAS :8124 生产现状,清除 Node.js 与甲骨文残留
背景:文档停在 Node.js 时代或甲骨文 8123 部署,与生产(NAS :8124 + Flask +
auth-hub + ai-gateway)严重脱节,曾导致凭旧记忆误判'无线上环境'。
- CLAUDE.md 重写:技术栈/结构/命令/部署事实/关键坑(F7 button、UTC 日期、
429 退避以 DB 为准、迁移幂等、AI 生成耗时)
- docs/ARCHITECTURE.md 重写为 Flask 蓝图+services+可插拔数据层 + NAS 部署
- docs/DEVELOPMENT.md 重写为 Flask/CRA 开发指南 + push.sh 部署流程
- docs/REQUIREMENTS.md:部署条目改 NAS 8124;补 auth-hub/AI 教练/新修复
- docs/AUTH_HUB_INTEGRATION.md 新增(补 .env.example 悬空引用)
- README.md:技术栈/DB/auth-hub/API 清单/部署节修正
- backend/config.py 与 .env.example:AUTH_HUB_REDIRECT_URI 默认 8123→8124,
MariaDB 注释 Oracle→NAS
- tests:GatewayCourtesy 并发测试对齐 MAX_CONCURRENT(AI_JOB_CONCURRENCY=2);
conftest 禁用 create_app 后台队列线程,修整库测试 flaky(585 passed)
|
2026-09-02 19:34:32 +08:00 |
|
ericwyuan
|
a2d6a5c57f
|
fix(ai): 队列会把共用的网关打到 502,加并发上限和间隔
排查生产上一直生成不出来,发现网关在返 502。上去看:机器好好的、systemd
说 active、5100 端口在监听——但它是 `gunicorn -w 1 --threads 4`,全部并发
就四个,而且 fam-edge 和摄像头项目也在用同一个。
我们这边一个请求占一个线程 2~5 分钟,NVIDIA 链重试起来最坏十七分钟(它自己
README 已知问题 #3)。而我写的 worker 是跑完一个立刻拉下一个,同步后还有八
个 scope 排队——等于拿满线程不撒手。这个 502 大概率是我打出来的,而且顺带
把另外两个项目也打下线了。
- 并发按整个部署计算,不是每个 gunicorn worker 一个:claim 前先数全局
running(两个 worker 各跑「一个」就是两个并发)
- 每跑完一个任务停 20 秒,不只是空闲时才停
- 两个都可用环境变量调,注释里写清楚调大的代价是什么
补齐的历史数据晚二十分钟到没有任何人受影响;网关不响应是三个项目一起受影响。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 15:46:42 +08:00 |
|
ericwyuan
|
c57c930949
|
feat(ai): AI 教练 —— 晨间简报、运动处方、趋势归因与 Copilot
数值全部在服务端算好再交给模型,模型只做解读。让模型从 CSV 里自己推
z 分数,它算错的次数足以让简报引用图表反驳它的数字。
- services/insights.py:z 分数(28 天个人基线,且**排除当天**——用一个
值参与算出来的均值去衡量它自己,会把真实离群点摊平)、13 个月趋势斜率
(按序数日期最小二乘,手表放充电器上一周不会压缩 x 轴)、近 7 天活动量
对比。
- services/coach.py:三套提示词 + 回复解析,每套都配一个规则引擎版本。
网关一次生成要几分钟,上游被限流时给一个朴素的答案,好过给一张空卡片。
- services/ai.py:多轮 chat()、SSE stream()、complete()/stream_chat(),
以及 extract_json()——上游是推理模型,可见输出以思维链开头,所以从末尾
倒着找最后一个配平的 JSON(字符串感知,扛得住引号里的 } 和转义引号)。
- 接口 briefing / trend-insight / copilot(SSE),缓存表 ai_insights。
- 前端:今日页晨报卡(后台生成 + 轮询升级)、全局 Copilot 浮窗、指标详情
页归因面板。features.ai 打开。
实测(对着自建 ai-gateway):晨报一次 273 秒,缓存命中 18 毫秒——所以简报
绝不能同步阻塞首屏。网关的流式通道比阻塞通道更不可靠:同一条提示词流式
139 秒后返回「所有模型均不可用」,阻塞则成功,因此 stream_chat() 在流式零
输出时对同一模型退回非流式重试。Copilot 实测 TTFB 9ms、全程 40 秒。
顺带修两处:refresh 原来只跳过缓存读、不删行,导致「重新生成」后的轮询读
到旧行、看到 cached 就停了,用户一直盯着他刚要求替换掉的那段字;基线零方差
时原来返回 z=0.0,把「和每一条观测都不同」标成「完全正常」,改为 z=null。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-01 13:57:35 +08:00 |
|
ericwyuan
|
9503fca370
|
feat(auth): 接入 auth-hub 统一登录,网页登录与 Garmin 同步彻底分离
网页身份改由 auth-hub 做 OAuth2 + PKCE 单点登录,本地邮箱/密码登录与注册整条链路删除
(routes/auth.py、auth.py 的密码哈希、config.py 的 ALLOW_REGISTRATION)。Garmin 账号绑定/
同步保持完全独立、可选:routes/garmin.py 不再直接查 users 表,Garmin 邮箱回退统一走新增
的 services/garmin.py::get_remembered_email()(优先读 garmin_tokens 当前绑定,兼容早期账号
落在 users.garmin_email 的历史值),彻底把「你是谁」和「你绑没绑 Garmin」两件事拆开。
- db.py: users 表新增 auth_hub_sub/auth_hub_username,MIGRATIONS 补上这两列(此前遗漏导致
已存在的生产 MariaDB 表永远不会自动加列);同时把历史遗留的 garmin_email/
garmin_password_hash NOT NULL 约束在线迁移为可空,因为新账号不再在注册时收集这些字段。
- routes/auth.py: 修掉 /callback 路由重复拼接 /api/auth 前缀导致 404 的 bug。
- client: LoginPage 去掉本地登录/注册标签页,只保留 auth-hub 统一登录;登录成功/失败后都
用 history.replaceState 清理地址栏,修掉 Framework7 browserHistory 读取
/auth/callback?code=... 导致「找不到页面」的问题。
- 新增 test_auth_hub_client.py 锁定 find_or_create_user 按 auth_hub_sub 幂等——生产上曾经因为
这个函数在没有该测试保护时被测试触发,误建过一个空账号,靠手工核对 health_data 计数才发现。
- 生产 auth-hub 侧另行为该项目注册了正式 client(未随本次提交变更,凭证只存在服务器 .env)。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
2026-08-31 23:12:17 +08:00 |
|
ericwyuan
|
ac99c1342d
|
docs: 迁移收尾——部署信息同步到 Oracle 新机 129.146.26.249
- backend/.env.example: AI 网关地址 129.146.203.203 → 129.146.26.249;
MariaDB 注释段更新为生产实际(garmin 专用账号 @ 127.0.0.1:3306,socket 已不用)
- README: 生产数据层/数据库说明改为 Oracle 新机本地 MariaDB 10.3
- docs/REQUIREMENTS: 部署目标 NAS → Oracle 新机;公网方式 frp 隧道 → gunicorn 直绑 8123
- docs/ARCHITECTURE: 部署架构改为真实生产栈(Flask+gunicorn+MariaDB+SPA)
|
2026-08-25 23:59:47 +08:00 |
|
ericwyuan
|
7ab150537d
|
[阶段10] 前端以 Framework7 重建,采用 iOS 原生形态
按要求废弃手写外壳,改用 Framework7 React(theme=ios)。参照 PeakWatch
的信息架构与卡片语言。
保留(这些是资产,不该重来):
- 数据层 services/api.ts、聚合 lib/aggregate.ts、参考区间 lib/ranges.ts
- 图表组件 Chart / Ring / Sparkline / BandBar / MetricCard / MetricStrip
- 经校验的配色令牌(色盲安全 + 对比度,浅深两档)
替换:
- 路由与外壳交给 F7:五个 Tab 各自独立导航栈,推入详情页不影响其他 Tab
- 页面转场、橡皮筋滚动、大标题折叠、半透明栏 —— 这些正是换框架的理由,
手写做不像
- 底部标签栏改用 F7 Toolbar,触控目标与安全区由框架处理
配色接入:
- 新增 f7theme.css 把我们的令牌映射到 F7 的 CSS 变量,让它的导航栏/
列表/面板与我们的图表同属一套设计,而不是两种视觉打架
- F7 的深色靠 .dark 类,我们的靠 data-theme,两者在 App 里同步切换
fix: 图标显示为原始名称(squ/hea/cale…)
- iconIos/iconMd 引用的是 framework7-icons 字体,没装就只会渲染出名字
其他:
- tsconfig moduleResolution 改为 bundler —— F7 用 exports 映射,
node 解析方式找不到它的类型
- 登录页不套 Tab 外壳,未登录时不该出现导航
桌面与手机都要好看:内容在宽屏收进 1100px 居中列并加密卡片列数,
窄屏走底部标签栏;两档都已实机核对。
bundle 190KB -> 399KB,是换取原生手感的代价。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-23 23:38:21 +08:00 |
|
ericwyuan
|
8882bf44a4
|
[阶段4.3] 接入自建 AI 网关,修复多模型层的四个真实缺陷
改用甲骨文机上已有的 ai-gateway (129.146.203.203:5100):它本身就
OpenAI 兼容,内部串联 nvidia/gemini/ollama 并轮换 4 个 Gemini key,
比在客户端自己串联更能吸收单厂商的配额和超时。回包里的 provider
字段透传为 meta.upstream,网关侧发生降级时前端也看得见。
fix(ai): 目录里两个 NVIDIA 模型 id 根本不存在
- qwen/qwen2.5-72b-instruct 和 deepseek-ai/deepseek-r1 是我凭印象写的,
实际 GET /v1/models 里没有,调用一律 404
- 改为该账号清单里确实存在的 nemotron-49b / mistral-large,
并在注释里写明 id 必须取自实时清单、不能猜
fix(ai): 请求被本机代理劫持导致网关不可达
- requests 默认读 HTTP_PROXY/ALL_PROXY,把发往甲骨文公网 IP 的请求
也塞进了 127.0.0.1:7897,120s 后超时
- 按 provider 区分:境外厂商(Gemini/NVIDIA)仍走代理,自建网关直连
(session.trust_env=False)
fix(ai): 承诺的按模型裁剪从未实现
- 模块注释写着 payload 按 (模型窗口, 天数预算) 取小者裁剪,但实际是
用全局预算构建一次 prompt 发给链上所有模型;365 天数据对 Gemini
的 1M 窗口无碍,却会撑爆 128k 的模型
- 新增 max_days_for(),在循环内按各模型窗口分别构建 prompt
fix(ai): 推理模型的思考过程吃光输出预算
- 网关首选 nemotron-3-ultra-550b 是推理模型,回答前先输出一段
chain-of-thought;默认 1024 tokens 全被思考占用,JSON 还没开始
就被截断
- max_tokens 改为可按 provider 声明,网关条目给 3000
fix(ai): 配置在 import 时被冻结
- DEFAULT_CHAIN/TIMEOUT/DAY_BUDGET 是模块级常量,改环境变量不生效,
且让开发机 .env 泄漏进测试进程(测试会读到真实 key 和链配置)
- 改为 default_chain()/default_timeout()/default_day_budget() 按调用读取
- conftest 增加 autouse fixture 清空全部 AI_* 变量,测试不再继承 .env
测试 (184 passed, 1 skipped):
- 新增 TestGatewayProvider: 透传 upstream、目标 URL/鉴权头、
token 失效时继续降级
- 新增 TestProxyPolicy: 境外厂商与自建端点的代理策略相反
- 新增 TestPerModelSizing: 128k 模型收到的 prompt 必须小于 1M 模型
- 新增 TestMaxTokens: 推理端点预算大于默认,且真正写进两种 payload
- 新增 TestLazyConfig: 改环境变量立即生效
- mock 目标从 requests.post 改为 requests.Session.post
实测: 网关链路可返回合法 JSON,但 nemotron-550B 排队较久(约 160s),
故 AI_TIMEOUT_SECONDS 默认调到 180。
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-23 17:44:51 +08:00 |
|
ericwyuan
|
c83340742c
|
[阶段4.1] AI 健康建议 - 多模型可切换 + 大上下文 + 失败兜底
services/ai.py:
- 模型目录(catalog)按短 id 索引,业务代码不感知厂商
gemini-flash (Google, 1M 上下文)
llama-70b / qwen-72b / deepseek-r1 (NVIDIA NIM, 128k)
仅注册纯文本模型,不含视觉模型
- 两个 provider: GeminiProvider、OpenAICompatProvider
(后者兼容 NVIDIA NIM / Ollama / vLLM)
- 大上下文: 每日指标序列化为 CSV 而非 JSON,同样的数据 token 数约为
1/4,一整年历史仍远小于最小的 128k 窗口;按 AI_DAY_BUDGET 截断
- 兜底链: 首选模型超时/报错/返回无法解析的文本时自动降级到下一个,
meta.fallbackFrom 记录降级路径
- 响应解析容忍 markdown 代码块包裹和 JSON 前的多余句子
services/analysis.py:
- get_ai_recommendations(): 所有模型都失败时回落到规则引擎,
端点始终 200,meta.source 区分 ai / rules
routes/analysis.py:
- GET /api/analysis/models 列出模型及各自是否已配置密钥
- GET /api/analysis/ai-recommendations?model=&days=
tests/test_ai.py (59 通过, 全程 mock 不联网):
- prompt: 大预算截断保留最新的天、缺失指标不写成 "None"、
一年数据估算 token 数上界
- 解析: 代码块包裹/前置句子/单对象/非法 priority/空建议 等 7 种畸形输入
- provider: 超时、HTTP 4xx/5xx、响应结构异常均转为 AIError;
未配置密钥时不发出任何请求
- 兜底: gemini 超时后 llama 接管、首个成功则不再调用第二个
- 端点: /models 不泄漏 API key;无密钥时仍返回 200 + 规则建议
密钥一律从环境变量读取,.env.example 只留空占位符。
全量: 161 passed, 1 skipped
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-23 12:38:19 +08:00 |
|
ericwyuan
|
3b2d0697f0
|
[阶段1.1-1.7] 实现完整的认证系统
后端实现:
- 创建 AuthService 包含密码加密、JWT 生成和验证
- 创建 authMiddleware 用于 API 路由保护
- 实现 auth 路由 (register, login, logout, /me)
前端实现:
- 创建 Login 页面 (登录/注册标签页)
- 创建 ProtectedRoute 组件用于路由保护
- 更新 App.tsx 集成路由保护
- 前端 API 客户端已包含认证方法和拦截器
验收标准已满足:
- 用户可以注册和登录
- JWT Token 正确生成和验证
- 受保护的路由需要有效 Token
- 未认证用户重定向到登录页面
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
|
2026-08-23 12:25:33 +08:00 |
|