Revert “feat: 登录页与首页新增一键安装到桌面按钮(PWA 安装提示 + 分步引导兜底)” This reverts commit be7fe621654dcbc26cf30cbde2efa9ae1e9406a8.
Revert “feat: 登录页与首页新增一键安装到桌面按钮(PWA 安装提示 + 分步引导兜底)”
This reverts commit be7fe621654dcbc26cf30cbde2efa9ae1e9406a8.
基于「体能训练动作模式数据库」(1,324 个动作 + GIF 动图)的 AI 对话训练平台。深色玻璃拟态 + Spline 3D 舞台,移动端优先,并适配刘海屏 / 挖孔屏 / 圆角屏。
AISmartTrainingPlatform/ ├── data/ # 现成动作数据库集(exercises.json / name_zh.json) ├── images/ # 1,324 张动作缩略图 ├── videos/ # 1,324 个动作 GIF ├── server/ # 后端(Node.js + Express + SQLite) │ ├── data/app.db # 首次启动自动从 data/exercises.json 导入生成 │ ├── src/ │ │ ├── index.js # 入口:API + 媒体静态资源 + 前端托管 │ │ ├── db.js / import.js # SQLite 初始化与数据导入(含难度派生) │ │ ├── routes/ # auth / exercises / sessions / chat │ │ └── ai/ # LLM 代理、系统提示词、JSON 归一化校验 │ ├── test-api.mjs # 接口冒烟测试(18 项) │ └── test-normalize.mjs # AI JSON 归一化单测(11 项) ├── web/ # 前端(React 18 + Vite + Tailwind CSS 3 + shadcn 结构 + framer-motion + Spline 3D) ├── scripts/ # 一次性数据转换脚本(body-paths 几何生成) └── src/ ├── components/ui/ # shadcn 基础组件(card / spotlight / splite) ├── components/ # AppShell / 动作卡片 / 详情抽屉 / 消息渲染 / BodyMap └── pages/ # 登录 / 首页 / 聊天 / 动作库 / 教练库 / 打卡 / 我的
# 1. 启动后端(首次启动自动导入 1,324 条动作到 SQLite) cd server copy .env.example .env # 填入 AI_BASE_URL / AI_API_KEY / AI_MODEL npm install npm start # http://localhost:3210 # 2. 启动前端(开发模式,局域网真机可访问) cd web npm install npm run dev # http://localhost:5173(已代理 /api 与 /media 到 3210) # 3. 生产部署(单端口) cd web && npm run build # 产物在 web/dist cd ../server && npm start # 直接访问 http://<局域网IP>:3210
PORT
JWT_SECRET
AI_BASE_URL
AI_API_KEY
AI_MODEL
ALLOWED_ORIGINS
PASSWORD_ENC_KEY
CAPTCHA_SECRET
页面上的 vX.Y.Z 由构建时从 Git 标签注入,不再手写:
vX.Y.Z
web/vite.config.js
git describe --tags --exact-match
1.13.0+2.abc1234
-dirty
.git
web/package.json
version
APP_VERSION
[version] 1.14.0 @ c507971 (tag v1.14.0)
发版流程:提交 → git tag -a v1.14.0 -m "..." → git push origin master v1.14.0。页面版本会自动跟随,只需保证服务器拉取时带上标签(deploy/update.sh 已包含 git fetch --tags --force)。
git tag -a v1.14.0 -m "..."
git push origin master v1.14.0
deploy/update.sh
git fetch --tags --force
{type, title, summary, exercises[{exercise_id, sets, reps, rest_seconds, tips}], ...}
exercise_id
warnings
prompt.js
coachFor
PATCH /api/coaches/:id
visibility=public
text
error
viewport-fit=cover
safe-area-inset-*
100dvh
visualViewport
tailwind.config.js
ink
fog
volt
touch-manipulation
components/ui/card.tsx
spotlight.tsx
splite.tsx
AppShell
ExerciseCard
ExerciseDetailSheet
MessageView
SessionDrawer
/robot/scene.splinecode
/spline/2.0.114/*.wasm
prod.spline.design
unpkg.com
robot-poster-wide.png
/assets
/robot
/spline
onLoad
public/robot/robot-poster.png
images/xxx.jpg
videos/xxx.gif
src/media.js
/media/...
系统提示词不注入全量 1,324 条目录,而是:
extractIntent
CANDIDATE_LIMIT
CANDIDATE_MIN_FILL
效果:提示词中目录部分体积缩减 >80%,且召回质量有单测保障(test-retrieval.mjs)。
test-retrieval.mjs
POST /api/chat/:id/messages/stream
Content-Type: text/event-stream
meta
{user}
reasoning
{text, full}
reasoning_content
delta
retry
{attempt, problems}
done
{assistant}
type: "error"
messages.thinking
api.chatStream
/api/chat/:id/messages
X-Accel-Buffering: no
MAX_WORKOUT_SECONDS
/api/workouts/stats
heatmap
volume / calories / duration / sessions
sessions > 0
server/src/training/muscles.js
target / muscle_group / secondary_muscles
NOTICE.md
web/scripts/convert-body-paths.mjs
web/src/lib/body-paths.js
BodyMap.jsx
三个子模块共用一套数据,独立页面 /growth(侧栏「训练档案」/ 我的 → 训练档案):
/growth
user_goals
growth_records
workouts
user_personas
source_hash
存储策略(避免重复):训练明细继续只存 workouts / workout_exercises / workout_sets,成长档案在读取时 UNION 合并,不复制落库;连续打卡复用 streaks,等级由 training/level.js 实时推导;user_personas 只保存摘要投影(summary/traits/stats)与输入指纹。
workouts / workout_exercises / workout_sets
streaks
training/level.js
上下文策略(AI 如何拿到数据):系统提示词不注入画像/档案原文,只注入:
goals.list
growth.records
growth.timeline
workouts.list
workouts.best
body.latest
streak
persona
overview
AI 判断需要个人数据时,整条回复输出 {"type":"data_request","requests":[...]};后端 chatService 识别后调用 ai/userData.js 执行(强制 user_id 隔离、行数裁剪),把结果作为一次性回执注入,再让 AI 输出正式方案。最多 2 轮、每轮最多 3 个接口,结果超过 6000 字符会截断。normalize.js 保留该内部协议,formatGuard 不会把它当最终消息展示。
{"type":"data_request","requests":[...]}
chatService
ai/userData.js
user_id
normalize.js
formatGuard
这样:与个人无关的问题(动作讲解、替代动作)零额外 token;需要个性化时才付出一次查询 + 一次重写的成本,且历史数据不会长期占据上下文。
HISTORY_MAX_MESSAGES
context_text
id 名称:组数×次数,休息Xs
HISTORY_TOKEN_BUDGET
estimateTokens
4 × (等级-1)²
/u/:id
announcement_deletes
GET/POST/DELETE /api/feedback
GET/PATCH /api/admin/feedback
版本号:web/src/version.js 的 APP_VERSION(与 web/package.json 同步,当前 1.1.0)展示在桌面侧栏底部、登录页与「我的」页脚,便于用户感知更新。
web/src/version.js
1.1.0
公告系统:
daily
AnnouncementCenter
announcement_reads
注册验证码:GET /api/auth/captcha 返回 SVG 图形验证码;服务端以 HMAC 无状态生成(5 分钟有效期、一次性消费),注册必须携带 captcha_id + captcha_code,用于抵御脚本批量注册。CAPTCHA_SECRET 建议部署时独立设置。
GET /api/auth/captcha
captcha_id
captcha_code
登录日志:每次登录(成功/失败)记录时间、IP、User-Agent 与失败原因;管理员可在用户列表直接看到「最近登录时间/IP」,并在编辑弹窗中查看该用户的登录记录与成功/失败统计(反向代理场景通过 X-Forwarded-For 取真实 IP)。
X-Forwarded-For
PWA:public/manifest.webmanifest + public/sw.js;HTML 导航网络优先(断网回退缓存壳),/assets 与图标缓存优先,/api、/media、/robot 一律直连网络。仅在 HTTPS / localhost 等安全上下文注册 Service Worker。
public/manifest.webmanifest
public/sw.js
/api
/media
/images/...
200 text/html
<img>
/media/images/...
/media/videos/...
/media/images/*
/media/videos/*
/media/*
/images/*
/videos/*
/images
/videos
/api/exercises/:id/media
cd server node test-retrieval.mjs # 检索层 16 项 node test-chat-flow.mjs # 对话联调 39 项(Mock LLM,含思考过程/教练人设,无需 API Key) node test-api.mjs # 接口冒烟 18 项(需先启动服务器) node test-normalize.mjs # JSON 归一化 11 项 node test-workouts-api.mjs # 打卡 API(含热图多口径 / 肌群聚合) node test-admin-controls.mjs # 后台权限(含教练库管理员权限) node test-announcements.mjs # 公告系统 / 注册验证码 / 登录日志(30 项) node test-feedback-admin.mjs # 反馈 / 管理员回复 / 私有教练开聊 / 管理员结束训练(24 项) node test-coach-identity.mjs # 教练更新保持 ID / 同名不同 ID / 会话绑定(14 项) node test-growth-pure.mjs # 训练档案:画像推导纯函数(14 项,无需启动服务) node test-levels-home.mjs # 等级 / 用户主页 / 时长上限 / 打卡监控 / 对话删除(32 项)
联调记录:server/test-records/chat-flow-report-2026-10-28.md
server/test-records/chat-flow-report-2026-10-28.md
版权所有:中国计算机学会技术支持:开源发展技术委员会 京ICP备13000930号-9 京公网安备 11010802047560号
AI 智训平台
基于「体能训练动作模式数据库」(1,324 个动作 + GIF 动图)的 AI 对话训练平台。深色玻璃拟态 + Spline 3D 舞台,移动端优先,并适配刘海屏 / 挖孔屏 / 圆角屏。
目录结构
快速开始
环境变量(server/.env)
PORTJWT_SECRETAI_BASE_URL/AI_API_KEY/AI_MODELALLOWED_ORIGINSPASSWORD_ENC_KEYCAPTCHA_SECRET版本号(与 Git tag 同步)
页面上的
vX.Y.Z由构建时从 Git 标签注入,不再手写:web/vite.config.js在 build 时执行git describe --tags --exact-match,HEAD 正好在 tag 上则显示该 tag;1.13.0+2.abc1234(最近 tag + 提交数 + 提交号);工作区有改动 → 追加-dirty;.git(源码包构建)→ 回退到web/package.json的version;CI 可用APP_VERSION环境变量强制指定;[version] 1.14.0 @ c507971 (tag v1.14.0)。发版流程:提交 →
git tag -a v1.14.0 -m "..."→git push origin master v1.14.0。页面版本会自动跟随,只需保证服务器拉取时带上标签(deploy/update.sh已包含git fetch --tags --force)。核心设计
{type, title, summary, exercises[{exercise_id, sets, reps, rest_seconds, tips}], ...};名称/GIF/肌群/分步要点等由后端按exercise_id从数据库回填,杜绝幻觉动作;不存在的 ID 自动剔除并写入warnings。prompt.js末尾另有输出前自检),同一需求下不同教练会给出不同的动作组合、组次与休息风格;coachFor严格校验公开可见或本人归属。PATCH /api/coaches/:id始终沿用原 ID,不允许生成同名副本;同名但真正不同的 agent 允许并存,以 ID 区分。创建时勾选「发布到广场」会直接写入visibility=public。text类型文本展示,前端永不崩溃;AI 未配置/超时/429 返回error类型消息(含友好提示)。viewport-fit=cover+safe-area-inset-*(顶栏 / 底栏 / 左右安全区)+100dvh+visualViewport监听软键盘。界面与设计系统
tailwind.config.js中的ink(深空底色)/fog(冷灰文字)/volt(训练信号绿)三组色阶;唯一强调色,避免通用 AI 紫蓝渐变。touch-manipulation+ touchend 兜底导航,降低部分设备上的点击丢失。聊天页切换为 100dvh 内部滚动 + 输入框贴底。会话抽屉采用同一套滚动机制。components/ui/card.tsx、spotlight.tsx、splite.tsx为 shadcn 结构组件;AppShell、ExerciseCard、ExerciseDetailSheet、MessageView、SessionDrawer全部复用同一套玻璃材质与按钮体系。/robot/scene.splinecode+/spline/2.0.114/*.wasm,不依赖prod.spline.design/unpkg.com);桌面首帧先显示透明宽幅海报robot-poster-wide.png,3D 就绪后淡入,重复打开首页不再空白等待;/assets构建产物 1 年 immutable,/robot、/spline30 天缓存。加载前检测 WebGL,9s 未就绪自动重挂一次、再 7s 失败降级为 OrbFallback;onLoad后校验 canvas 尺寸,WebGL 上下文丢失自动重建。移动端(<1024px)直接展示透明静态海报public/robot/robot-poster.png,完全不加载 3D 运行时/WASM,老旧手机也能即时显示且不卡顿。仅首页/登录页展示,打卡/我的不挂载 WebGL 背景。images/xxx.jpg/videos/xxx.gif,src/media.js统一映射到后端的/media/...;动作库网格悬停即播放 GIF,详情页优先 GIF、失败逐级回退。检索层(Top-N 候选注入)
系统提示词不注入全量 1,324 条目录,而是:
extractIntent从用户消息提取意图(身体部位/器械/目标肌群/难度 4 类中文词典 + 剩余词片段名称匹配),支持否定短语剔除(”我没有哑铃”不会把哑铃加入正向意图,”没有器械”映射为自重);CANDIDATE_LIMIT)注入提示词;不足 36 条(CANDIDATE_MIN_FILL)时按部位轮转 backfill,保证”全身/20分钟”类开放需求可跨部位召回;效果:提示词中目录部分体积缩减 >80%,且召回质量有单测保障(
test-retrieval.mjs)。流式输出方案(SSE)
POST /api/chat/:id/messages/stream(需 Bearer Token),Content-Type: text/event-stream。meta→{user}首帧确认用户消息;reasoning→{text, full}思考型模型的思考过程增量(DeepSeekreasoning_content/ OpenAI 风格reasoning,非思考模型不触发);delta→{text, full}AI 原文增量(前端实时展示生成过程,等宽字体+渐隐遮罩);retry→{attempt, problems}格式稽核打回,前端重置”生成中”面板;done→{assistant}归一化后的结构化 payload(与非流式完全一致,前端替换为方案卡片渲染)。**AI 调用失败时同样走done**,payload 为type: "error",避免前端误判为通道失败而降级重试、把同一句话重复入库;error→ 仅用于传输层异常(如 SSE 通道自身出错)。messages.thinking列(追加式迁移),历史消息可回看;流式期间展示可折叠”深入思考中”面板,正式回答开始后自动收起。done后切换为结构化渲染。api.chatStream解析 SSE;流式失败自动降级调用非流式端点/api/chat/:id/messages。X-Accel-Buffering: no禁用 Nginx 缓冲(公网部署友好)。打卡统计与肌群热区
MAX_WORKOUT_SECONDS)。到时自动冻结计时(惰性触发),后端存储与前端展示均按上限截断;历史异常数据在启动时自动修正。后台「打卡监控」Tab 可查看全部未结束训练,支持手动暂停或直接结束(保存并计入统计 / 放弃且不计入统计)。/api/workouts/stats的heatmap同时返回每日volume / calories / duration / sessions;前端支持容量/时长/热量切换,任何有打卡记录(sessions > 0)的日子至少点亮 1 级——纯自重或计时训练(容量为 0)绝不显示为”未训练”。server/src/training/muscles.js把动作数据集的target / muscle_group / secondary_muscles归并到 18 个可绘制肌群,按有效组数聚合(主目标权重 1、次级 0.4、热身组不计),一次遍历产出近 7 天 / 近 30 天 / 全部三个窗口。NOTICE.md),由web/scripts/convert-body-paths.mjs生成web/src/lib/body-paths.js(≈95KB,动态 import 懒加载);渲染组件BodyMap.jsx与色阶为自研实现。训练档案模块(用户个性化数据)
三个子模块共用一套数据,独立页面
/growth(侧栏「训练档案」/ 我的 → 训练档案):user_goalsgrowth_records+workouts系列表合并读取user_personas(source_hash判断是否过期)存储策略(避免重复):训练明细继续只存
workouts / workout_exercises / workout_sets,成长档案在读取时 UNION 合并,不复制落库;连续打卡复用streaks,等级由training/level.js实时推导;user_personas只保存摘要投影(summary/traits/stats)与输入指纹。上下文策略(AI 如何拿到数据):系统提示词不注入画像/档案原文,只注入:
goals.list、growth.records、growth.timeline、workouts.list、workouts.best、body.latest、streak、persona、overview);AI 判断需要个人数据时,整条回复输出
{"type":"data_request","requests":[...]};后端chatService识别后调用ai/userData.js执行(强制user_id隔离、行数裁剪),把结果作为一次性回执注入,再让 AI 输出正式方案。最多 2 轮、每轮最多 3 个接口,结果超过 6000 字符会截断。normalize.js保留该内部协议,formatGuard不会把它当最终消息展示。这样:与个人无关的问题(动作讲解、替代动作)零额外 token;需要个性化时才付出一次查询 + 一次重写的成本,且历史数据不会长期占据上下文。
上下文压缩策略
HISTORY_MAX_MESSAGES),且受 token 预算约束context_text:方案标题+概要+每个动作的id 名称:组数×次数,休息Xs,≤1800 字符;坏 JSON 降级消息用原文截断HISTORY_TOKEN_BUDGET)+ 当前消息;从最新往回整轮累加,超预算丢弃最旧整轮(至少保留当前轮)estimateTokens:CJK 字符 ≈1 token/字,非 CJK ≈1 token/4 字符(保守高估,宁可少带不超限)context_text保持可追溯等级与用户主页
4 × (等级-1)²,前期快、后期慢,满级 100。「我的」页面展示等级进度、训练总时长、提问次数。/u/:id):排行榜中的每位用户可点击进入主页,展示公开资料(UID、等级、训练总时长、提问次数、打卡次数、总容量、最近训练),不含电话/邮箱等隐私字段。announcement_deletes,仅影响自己,不影响他人)。用户反馈
GET/POST/DELETE /api/feedback与GET/PATCH /api/admin/feedback。版本、公告与安全
版本号:
web/src/version.js的APP_VERSION(与web/package.json同步,当前1.1.0)展示在桌面侧栏底部、登录页与「我的」页脚,便于用户感知更新。公告系统:
version(版本更新,可标注版本号)/daily(日常平台公告);AnnouncementCenter);确认状态写入announcement_reads;注册验证码:
GET /api/auth/captcha返回 SVG 图形验证码;服务端以 HMAC 无状态生成(5 分钟有效期、一次性消费),注册必须携带captcha_id+captcha_code,用于抵御脚本批量注册。CAPTCHA_SECRET建议部署时独立设置。登录日志:每次登录(成功/失败)记录时间、IP、User-Agent 与失败原因;管理员可在用户列表直接看到「最近登录时间/IP」,并在编辑弹窗中查看该用户的登录记录与成功/失败统计(反向代理场景通过
X-Forwarded-For取真实 IP)。PWA:
public/manifest.webmanifest+public/sw.js;HTML 导航网络优先(断网回退缓存壳),/assets与图标缓存优先,/api、/media、/robot一律直连网络。仅在 HTTPS / localhost 等安全上下文注册 Service Worker。媒体加载容错(图片/GIF)
images/xxx.jpg/videos/xxx.gif,前端旧实现只加前导斜杠得到/images/...,会命中 SPA 回退并返回200 text/html,<img>解码失败——这就是“图片永远加载不出来”的原因。现在src/media.js统一映射为/media/images/...、/media/videos/...,并追加版本参数击穿历史错误缓存。/media/images/*、/media/videos/*精确目录 →/media/*智能兜底(按 basename + 扩展名在两个目录解析)→ 404 JSON;同时保留/images/*、/videos/*兼容别名,便于已缓存旧 URL 的客户端恢复。/api、/media、/images、/videos;任何静态资源后缀(jpg/png/gif/webp/js/css…)未命中时直接 404,绝不再返回 200 HTML。/api/exercises/:id/media(服务端读取文件并返回 base64 data URL)→ 内置 SVG 占位。API 通道是对抗浏览器缓存 / 代理 / 扩展拦截的终极兜底。测试
联调记录:
server/test-records/chat-flow-report-2026-10-28.md