# SmartLearning 情境感知智能英语学习系统 基于 PRD(`docs/smartlearing_prd_v1.01.txt`)实现的云端服务端,负责场景识别、智能对话、课程资源管理与终端设备 MQTT 指令下发,支撑「场景感知 → 意图分析 → 智能对话 → 学习反馈」闭环。 --- ## 目录 - [系统架构](#系统架构) - [项目结构](#项目结构) - [核心流程](#核心流程) - [MQTT 协议](#mqtt-协议) - [课程与任务清单](#课程与任务清单) - [HTTP API](#http-api) - [配置说明](#配置说明) - [本地开发](#本地开发) - [打包与部署](#打包与部署) - [常见问题](#常见问题) --- ## 系统架构 ``` ┌─────────────┐ MQTT (snlp/v1/...) ┌──────────────────┐ │ 学习终端 │ ◄──────────────────────────► │ SmartLearning │ │ (设备) │ HTTP API / SSE │ Server (本仓库) │ └─────────────┘ ◄──────────────────────────► └────────┬─────────┘ │ ┌────────▼─────────┐ │ data/ 课程资源 │ │ runtime/ 运行状态 │ └──────────────────┘ ``` | 组件 | 技术栈 | 职责 | |------|--------|------| | HTTP 服务 | FastAPI + Uvicorn | REST API、SSE 对话流、静态资源托管 | | MQTT 客户端 | paho-mqtt | 订阅终端上报、下发控制/任务指令 | | 课程数据 | JSON 文件 | 课程索引、章节视频清单、初始化任务 | | 运行状态 | JSON 持久化 | 记录已完成初始化的设备 ID | --- ## 项目结构 ``` smartlearning/ ├── docs/ │ └── smartlearing_prd_v1.01.txt # 产品需求与 MQTT 协议规范 ├── data/ # 课程与任务数据(同时作为静态资源根目录) │ ├── tasklist.json # 设备开机初始化任务清单 │ └── courses/ │ ├── index.json # 已发布课程索引(基础课程包) │ ├── course01/ │ │ ├── course.json # 课程视频清单 │ │ ├── courseTask.json # 单课程初始化任务(可选扩展) │ │ └── chapter01/resources/ # 视频文件目录 │ └── course02/ │ └── ... ├── server/ │ ├── app/ │ │ ├── main.py # 应用入口 │ │ ├── config.py # 配置(支持 .env) │ │ ├── routes/ # API 路由 │ │ ├── services/ # 业务逻辑 │ │ └── mqtt/ # MQTT 与指令下发 │ ├── runtime/ │ │ └── initialized_devices.json # 已初始化设备记录(运行时生成) │ ├── requirements.txt │ └── run.py # 启动脚本 └── README.md ``` --- ## 核心流程 ### 1. 设备首次上线(自动初始化) 终端开机后通过 MQTT 上报心跳 `rpt/state`,服务端检测到该 `device_id` 尚未初始化时,**自动**执行两阶段流程: ``` 终端上报 rpt/state(首次) │ ▼ ┌───────────────────────────────────────┐ │ 阶段 1:设备基础初始化(cmd/ctl) │ │ · SET_CLOCK 同步系统时钟 │ │ · SET_VOLUME 设置默认音量(50) │ │ · SET_POWER 唤醒设备 │ └───────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────┐ │ 阶段 2:基础课程资源同步(cmd/task) │ │ · UPDATE_MANIFEST 下发完整资源清单 │ │ · DOWNLOAD × N 逐文件下载资源包 │ │ (index.json 中所有课程) │ └───────────────────────────────────────┘ │ ▼ 写入 server/runtime/initialized_devices.json (同一设备不会重复触发,除非手动强制) ``` **自动触发条件(须同时满足):** 1. 收到 `snlp/v1/{device_id}/rpt/state` 2. `device_id` 不在本地已初始化记录中 3. MQTT 已连接 4. `AUTO_INIT_ENABLED=true`(默认开启) **不会自动触发的情况:** - 已初始化设备再次上报心跳 → 跳过 - MQTT 未连接 → 跳过(下次心跳可重试) - 可通过 `DELETE /api/v1/devices/initialized/{device_id}` 清除记录后重新触发 ### 2. 基础课程资源同步说明 `data/courses/index.json` 中列出的课程均视为**基础课程**,在设备初始化完成后自动同步到终端存储卡。 **终端存储路径规则:** ``` 设备路径 = {DEVICE_DATA_ROOT} + 服务端 data/ 下的相对路径 ``` 默认 `DEVICE_DATA_ROOT=/data`,示例: | 服务端文件 | 终端目标路径 | |-----------|-------------| | `data/courses/index.json` | `/data/courses/index.json` | | `data/courses/course01/course.json` | `/data/courses/course01/course.json` | | `data/courses/course01/chapter01/resources/001.mp3` | `/data/courses/course01/chapter01/resources/001.mp3` | 当前 `course01` + `course02` 共 **23 个文件**(索引 + 配置 + 任务清单 + 18 个视频占位路径)。 ### 3. 单课程初始化(手动,可选) 若需单独对某一门课程执行完整任务(含播放引导),可调用: ```http POST /api/v1/devices/course-init Content-Type: application/json { "device_id": "SN-88293021", "course_id": "course01" } ``` 该接口读取 `courseTask.json`,下发 `UPDATE_MANIFEST`、`DOWNLOAD`、`PLAY_MEDIA` 等指令,适用于选课后增量同步或重新拉取。 ### 4. 场景感知与对话 ``` 环境音/语音 → POST /api/v1/scene/analyze → 场景识别(置信度 < 0.7 走通用问候) ↓ 用户回复 → GET /api/v1/dialogue/stream (SSE) → 流式返回教学对话与反馈 ``` ### 5. 手动强制重新初始化 ```http POST /api/v1/devices/init Content-Type: application/json { "device_id": "SN-88293021", "force": true } ``` `force: false` 且设备已初始化时,返回 `status: "already_initialized"`,不会重复下发。 --- ## MQTT 协议 ### Topic 规则 ``` snlp/v1/{device_id}/{direct}/{msg_type} ``` | 方向 | Topic | 说明 | |------|-------|------| | 终端 → 云 | `snlp/v1/{device_id}/rpt/state` | 心跳/状态(cpu、电量、网络、模式) | | 终端 → 云 | `snlp/v1/{device_id}/rpt/event` | 事件(唤醒、ASR 结果等) | | 终端 → 云 | `snlp/v1/{device_id}/rpt/ack` | 指令执行回执 | | 终端 → 云 | `snlp/v1/{device_id}/rpt/progress` | 异步任务进度(下载/OTA) | | 云 → 终端 | `snlp/v1/{device_id}/cmd/ctl` | 实时控制指令 | | 云 → 终端 | `snlp/v1/{device_id}/cmd/task` | 异步任务指令(下载资源包等) | ### 指令 Action 一览 | Action | 通道 | 说明 | |--------|------|------| | `SET_CLOCK` | ctl | 同步时间戳 | | `SET_VOLUME` | ctl | 设置音量 0–100 | | `SET_POWER` | ctl | 休眠/唤醒 | | `PLAY_MEDIA` | ctl | 播放本地媒体 | | `STOP_MEDIA` | ctl | 停止播放 | | `DOWNLOAD` | task | 异步下载文件到 `target_path` | | `UPDATE_MANIFEST` | task | 更新课程资源清单 | | `OTA_UPDATE` | task | 固件升级 | | `REBOOT` | ctl | 延时重启 | ### 下发指令 Payload 格式 ```json { "header": { "version": "1.0", "device_id": "SN-88293021", "req_id": "uuid", "timestamp": 1718365200, "ttl": 3600 }, "action": "DOWNLOAD", "payload": { "task_id": "uuid", "url": "http://your-server:9500/static/courses/course01/chapter01/resources/001.mp3", "md5": "", "target_path": "/data/courses/course01/chapter01/resources/001.mp3" } } ``` 终端执行后须回 `rpt/ack`;下载过程中通过 `rpt/progress` 上报进度。 --- ## 课程与任务清单 ### index.json — 基础课程索引 ```json { "courses": [ { "course_id": "course01", "config": "courses/course01/course.json" }, { "course_id": "course02", "config": "courses/course02/course.json" } ] } ``` 新增课程:在 `data/courses/` 下创建目录和 `course.json`,并登记到 `index.json`,设备重新初始化后自动同步。 ### course.json — 课程视频清单 ```json { "course_id": "course01", "course_name": "小智学习课程01", "chapters": [ { "chapter_id": "chapter01", "chapter_name": "第一章", "audios": [ { "id": "001", "title": "课程001", "path": "courses/course01/chapter01/resources/001.mp3" } ] } ] } ``` `path` 为相对于 `data/` 的路径,须与实际文件位置一致。 ### tasklist.json — 设备开机任务 定义设备基础初始化步骤(时钟、音量、唤醒)。基础课程资源包由服务端在初始化完成后**自动**通过 `cmd/task` 下发,无需在此文件中配置。 ### courseTask.json — 单课程任务(可选) 定义某一门课程的扩展初始化流程(如下载 + 播放引导),通过 `course-init` API 手动触发。 --- ## HTTP API 服务默认地址:`http://localhost:9500` 交互式文档:`http://localhost:9500/docs` ### 健康检查 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/health` | 服务状态与 MQTT 连接状态 | ### 场景与对话 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/api/v1/scene/analyze` | 场景识别 | | GET | `/api/v1/dialogue/stream` | SSE 对话流(参数:`dialogue_id`、`user_text`、`scene_prompt`、`difficulty`) | ### 课程与初始化 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/v1/courses` | 课程索引 | | GET | `/api/v1/courses/base-manifest` | 预览基础课程同步资源列表 | | GET | `/api/v1/courses/{course_id}` | 课程详情 | | GET | `/api/v1/courses/{course_id}/manifest` | 单课程资源清单 | | GET | `/api/v1/courses/{course_id}/tasks` | 单课程任务清单 | | GET | `/api/v1/tasklist` | 设备初始化任务清单 | | POST | `/api/v1/devices/init` | 手动触发设备初始化(支持 `force`) | | POST | `/api/v1/devices/course-init` | 单课程初始化 | ### 设备管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/v1/devices` | 在线设备列表(来自 MQTT 上报) | | GET | `/api/v1/devices/initialized` | 已初始化设备记录 | | GET | `/api/v1/devices/{device_id}` | 设备状态 | | GET | `/api/v1/devices/{device_id}/init-status` | 初始化状态 | | DELETE | `/api/v1/devices/initialized/{device_id}` | 清除初始化记录 | | GET | `/api/v1/devices/{device_id}/acks` | 指令回执历史 | | GET | `/api/v1/devices/{device_id}/events` | 事件历史 | | POST | `/api/v1/devices/{device_id}/cmd` | 手动下发 ctl 指令 | | POST | `/api/v1/devices/{device_id}/task` | 手动下发 task 指令 | ### 静态资源 ``` GET /static/{path} ``` 示例:`/static/courses/course01/chapter01/resources/001.mp3` 终端 `DOWNLOAD` 指令中的 `url` 指向此地址,请确保 `BASE_URL` 配置为终端可访问的公网/内网地址。 ### 请求示例 **场景识别:** ```bash curl -X POST http://localhost:9500/api/v1/scene/analyze \ -H "Content-Type: application/json" \ -d '{ "timestamp": 1718365200, "audio_blob": "", "device_id": "SN-88293021", "context_hints": { "scene": "dining", "confidence": 0.9 } }' ``` **对话流(SSE):** ```bash curl -N "http://localhost:9500/api/v1/dialogue/stream?user_text=Good%20morning&difficulty=A2" ``` **预览基础课程同步清单:** ```bash curl http://localhost:9500/api/v1/courses/base-manifest ``` --- ## 配置说明 在 `server/` 目录下创建 `.env` 文件(推荐,避免将密钥写入代码): ```env # HTTP 服务 HOST=0.0.0.0 PORT=9500 BASE_URL=http://your-public-ip:9500 # 课程数据目录(默认指向项目根 data/) # DATA_DIR=E:/smartlearning/data # MQTT MQTT_BROKER=your-mqtt-broker MQTT_PORT=1883 MQTT_USERNAME=your-username MQTT_PASSWORD=your-password MQTT_CLIENT_ID=snlp-server MQTT_ENABLED=true # 初始化 AUTO_INIT_ENABLED=true DEVICE_DATA_ROOT=/data # 场景识别 SCENE_CONFIDENCE_THRESHOLD=0.7 ``` | 变量 | 默认值 | 说明 | |------|--------|------| | `HOST` | `0.0.0.0` | 监听地址 | | `PORT` | `9500` | 监听端口 | | `BASE_URL` | `http://localhost:9500` | 资源下载 URL 前缀,**部署时必须改为终端可访问的地址** | | `DATA_DIR` | `../data` | 课程数据目录 | | `MQTT_BROKER` | — | MQTT Broker 地址 | | `MQTT_PORT` | — | MQTT 端口 | | `MQTT_ENABLED` | `true` | 设为 `false` 可仅运行 HTTP API | | `AUTO_INIT_ENABLED` | `true` | 是否自动触发设备初始化 | | `DEVICE_DATA_ROOT` | `/data` | 终端存储卡根路径 | ### 已初始化设备记录 路径:`server/runtime/initialized_devices.json` ```json { "devices": { "SN-88293021": { "initialized_at": 1781701381, "source": "auto", "mode": "idle", "courses_synced": true, "courses_sync_at": 1781701381, "courses_download_count": 23 } } } ``` --- ## 本地开发 ### 环境要求 - Python 3.10+ - 可访问的 MQTT Broker(自动初始化依赖 MQTT) ### 安装与启动 ```bash cd server pip install -r requirements.txt # 创建 .env 并填写配置(见上文) # copy .env.example .env # 如有示例文件 python run.py ``` 或使用 uvicorn 直接启动(支持热重载): ```bash cd server uvicorn app.main:app --host 0.0.0.0 --port 9500 --reload ``` ### 验证 ```bash # 健康检查 curl http://localhost:9500/health # 课程索引 curl http://localhost:9500/api/v1/courses # 基础课程同步清单 curl http://localhost:9500/api/v1/courses/base-manifest ``` 访问 `http://localhost:9500/docs` 查看完整 API 文档。 --- ## 打包与部署 ### 方式一:直接部署(推荐小规模/内网) **1. 打包项目** 在开发机上将项目目录打包(排除缓存与虚拟环境): ```bash # Linux / macOS cd /path/to tar czvf smartlearning-server.tar.gz \ --exclude='server/__pycache__' \ --exclude='server/.venv' \ --exclude='server/runtime/*.json' \ smartlearning/ # Windows PowerShell Compress-Archive -Path E:\smartlearning -DestinationPath smartlearning-server.zip ``` 上传到服务器后解压: ```bash tar xzvf smartlearning-server.tar.gz cd smartlearning/server ``` **2. 创建虚拟环境并安装依赖** ```bash python3 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -r requirements.txt ``` **3. 配置环境变量** ```bash cp .env.example .env # 或手动创建 vim .env # 填写 BASE_URL、MQTT 等 ``` > **重要:** `BASE_URL` 必须设置为终端能访问到的地址(公网 IP 或域名),否则 `DOWNLOAD` 指令中的资源 URL 无效。 **4. 准备课程资源** 将视频文件放入 `data/courses/` 对应目录,确保与 `course.json` 中 `path` 一致。 **5. 使用 systemd 守护进程(Linux)** 创建 `/etc/systemd/system/smartlearning.service`: ```ini [Unit] Description=SmartLearning Server After=network.target [Service] Type=simple User=www-data WorkingDirectory=/opt/smartlearning/server EnvironmentFile=/opt/smartlearning/server/.env ExecStart=/opt/smartlearning/server/.venv/bin/python run.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` ```bash sudo systemctl daemon-reload sudo systemctl enable smartlearning sudo systemctl start smartlearning sudo systemctl status smartlearning ``` **6. Nginx 反向代理(可选)** ```nginx server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:9500; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_buffering off; # SSE 对话流需要关闭缓冲 } } ``` 若使用 HTTPS,配置 TLS 证书并将 `BASE_URL` 改为 `https://your-domain.com`。 --- ### 方式二:Docker 部署 在项目根目录创建 `Dockerfile`(如尚未存在,可参考以下内容): ```dockerfile FROM python:3.12-slim WORKDIR /app COPY server/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY server/ ./server/ COPY data/ ./data/ WORKDIR /app/server ENV HOST=0.0.0.0 ENV PORT=9500 ENV DATA_DIR=/app/data EXPOSE 9500 CMD ["python", "run.py"] ``` `docker-compose.yml` 示例: ```yaml services: smartlearning: build: . ports: - "9500:9500" env_file: - server/.env volumes: - ./data:/app/data:ro - ./server/runtime:/app/server/runtime restart: unless-stopped ``` 构建与启动: ```bash docker compose build docker compose up -d docker compose logs -f ``` --- ### 方式三:Windows 服务部署 **使用 NSSM 注册为 Windows 服务:** ```powershell # 安装 NSSM 后执行 nssm install SmartLearning "E:\smartlearning\server\.venv\Scripts\python.exe" "E:\smartlearning\server\run.py" nssm set SmartLearning AppDirectory "E:\smartlearning\server" nssm set SmartLearning AppEnvironmentExtra "BASE_URL=http://your-ip:9500" nssm start SmartLearning ``` --- ### 生产环境检查清单 | 检查项 | 说明 | |--------|------| | `BASE_URL` | 终端可通过此地址下载 `/static/` 资源 | | MQTT 连通 | `/health` 返回 `"mqtt_connected": true` | | 防火墙 | 放行 HTTP 端口(9500 或 Nginx 80/443) | | 课程文件 | `data/courses/` 下视频文件已就位 | | 持久化 | `server/runtime/` 目录需可写(初始化记录) | | 密钥安全 | MQTT 账号密码放在 `.env`,勿提交到版本库 | | 进程守护 | 使用 systemd / Docker `restart` 策略 | --- ### 升级与回滚 **升级步骤:** ```bash # 1. 备份运行状态 cp server/runtime/initialized_devices.json /backup/ # 2. 更新代码 git pull # 或替换解压包 # 3. 更新依赖 cd server && pip install -r requirements.txt # 4. 重启服务 sudo systemctl restart smartlearning # 或 docker compose restart ``` **新增课程后:** 无需重启服务;已初始化设备需清除记录或 `force: true` 重新初始化方可同步新课程。 ```bash curl -X DELETE http://localhost:9500/api/v1/devices/initialized/SN-88293021 # 设备下次上报 state 时将自动重新初始化并同步全部基础课程 ``` --- ## 常见问题 **Q: 设备上报了 state,但没有收到初始化指令?** 1. 检查 `/health` 中 `mqtt_connected` 是否为 `true` 2. 检查该设备是否已在 `initialized_devices.json` 中 3. 确认 `AUTO_INIT_ENABLED=true` 4. 用 `GET /api/v1/devices/initialized` 查看记录 **Q: 终端下载资源失败?** 1. 确认 `BASE_URL` 是终端网络可达的地址,不是 `localhost` 2. 确认视频文件已上传到 `data/courses/` 对应路径 3. 用浏览器或 curl 访问 `BASE_URL/static/courses/...` 验证 **Q: 如何只同步基础课程,不重复做时钟/音量初始化?** 当前设计为一体化流程。若仅需补发资源,可手动调用: ```bash # 通过内部 API 下发(需自行扩展端点)或直接向 MQTT 发布 DOWNLOAD 指令 curl -X POST http://localhost:9500/api/v1/devices/SN-88293021/task \ -H "Content-Type: application/json" \ -d '{"header":{"device_id":"SN-88293021","timestamp":0},"action":"DOWNLOAD","payload":{"url":"...","target_path":"..."}}' ``` 或清除初始化记录后让设备重新走完整流程。 **Q: MQTT Broker 暂时不可用,HTTP API 还能用吗?** 可以。设置 `MQTT_ENABLED=false` 或 Broker 连接失败时,HTTP API 仍正常运行,但自动初始化与指令下发不可用。 --- ## 相关文档 - 产品需求与 MQTT 详细规范:`docs/smartlearing_prd_v1.01.txt` - API 交互式文档:启动服务后访问 `/docs` --- ## 版本 - 服务端版本:1.0.0 - 协议版本:snlp/v1 http://localhost:9500/api/v1/courses/course01/manifest http://localhost:9500/api/v1/courses/base-manifest # 第一步:清除该设备的初始化记录 curl.exe -X DELETE http://124.222.109.197:2101/api/v1/devices/initialized/498e93f3-cdc9-4362-b32a-d2fc10e69c12 Invoke-WebRequest -Uri "http://124.222.109.197:2101/api/v1/devices/initialized/498e93f3-cdc9-4362-b32a-d2fc10e69c12" -Method Delete # 第二步:强制重新初始化(含全部基础课程同步) Invoke-RestMethod -Uri "http://124.222.109.197:2101/api/v1/devices/init" -Method Post -ContentType "application/json" -Body '{"device_id": "498e93f3-cdc9-4362-b32a-d2fc10e69c12", "force": true}'