diff --git a/README.md b/README.md index b371aa6..6bf9b4b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,11 @@ - SMTP 配置、测试发信与 public IPv4 changed 邮件通知 - location recorder - poo recorder -- Home Assistant inbound / outbound integration +- Home Assistant inbound / outbound integration(REST 通道) - TickTick OAuth 与 action task 集成 +- **Modbus 设备采集**:通过 YAML profile(首个:SDM120 电表)按设备周期轮询 Modbus-TCP 网关,解码工程量并落通用读数表(`modbus_device` + `modbus_reading`) +- **MQTT + Home Assistant Discovery**:以可勾选方式把 Modbus 设备/工程量注册为 HA device/entity(含 binary_sensor online),state 周期发布;配置变更可重连重发 +- **前端侧边栏 + Energy 视图**:侧边导航替换顶栏;Energy 页管理 Modbus 设备、展示最新读数与 Recharts 走势图;Config 页 Accordion 分区展开;Expose 设置勾选 HA 可暴露实体 - pytest 测试与 OpenAPI 导出脚本 - Docker / Compose 部署入口 @@ -30,6 +33,9 @@ - public IPv4 当前状态与变化历史 - location 记录(`location` 表) - poo 记录(`poo_records` 表) +- Modbus 设备定义(`modbus_device` 表) +- Modbus 通用读数(`modbus_reading` 表,JSON payload) +- HA 实体暴露开关(`exposed_entity_toggle` 表) 配置层只保留一个数据库环境变量: @@ -41,7 +47,7 @@ python -m scripts.run_migrations ``` -该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(含 `location` / `poo_records` 表)。 +该命令会通过 Alembic 将 `app.db` 初始化或升级到最新 head(含全部表,包括 M5 新增的 `modbus_device`、`modbus_reading`、`exposed_entity_toggle`)。 ## 当前目录 @@ -49,7 +55,7 @@ python -m scripts.run_migrations - `app/`: FastAPI 应用代码(包含 JSON API、业务服务、数据模型) - `frontend/`: React SPA 前端(Vite + React + TypeScript + Mantine) -- `alembic_app/`: App DB 的 Alembic migration 环境(同时管理 `location` / `poo_records` 表) +- `alembic_app/`: App DB 的 Alembic migration 环境(管理所有表,含 M5 新增的 `modbus_device`、`modbus_reading`、`exposed_entity_toggle`) - `tests/`: pytest 测试 - `docs/`: 当前系统说明文档 - `scripts/`: 辅助脚本,例如 OpenAPI 导出 @@ -128,6 +134,7 @@ M2 用 React SPA 取代了原有 Jinja 服务端模板,由 FastAPI 同源托 - **Vite + React + TypeScript + Mantine**(组件库) - **TanStack Query**(数据请求/缓存) - **Leaflet / react-leaflet**(地图与热力图) +- **Recharts**(Energy 视图走势图,M5 引入) - **openapi-typescript + openapi-fetch**(类型化 API client,由 `openapi/openapi.json` 生成) ### 本地开发(前端) @@ -177,7 +184,7 @@ npm run build # 构建,确认产出 dist - App DB:`sqlite:///./data/app.db` - 数据目录:`./data/` -所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均通过单一 Alembic 链管理: +所有模型(auth / config / public_ip / location / poo / modbus / expose)共用同一个 `Base`,均通过单一 Alembic 链管理: - Alembic 环境:`alembic_app.ini` + `alembic_app/` - 统一 migration job:`python -m scripts.run_migrations` @@ -281,6 +288,84 @@ admin 可在 React SPA 设置页(`/config`)自选启用 RFC 6238 TOTP: TOTP issuer 标签(显示在 Authenticator 里)通过 `AUTH_TOTP_ISSUER` 环境变量配置(`.env` 部署级),默认回退 `app_name`。 +## M5 Modbus 设备采集 / Energy / MQTT + HA Discovery + +M5 给后端接入家庭 IoT 生态,新增通用 Modbus 采集链路(首个领域:能耗)、MQTT + Home Assistant Discovery 发布,以及前端侧边栏与 Energy 视图。 + +### 依赖 + +后端新增: +- `pymodbus`:Modbus-TCP 客户端(轮询电表等 slave 设备) +- `paho-mqtt`:MQTT 客户端(HA Discovery 与 state 发布) +- `pyyaml`:YAML profile 加载(设备协议声明式描述) + +前端新增: +- `recharts`:Energy 视图走势图 + +### Modbus 设备采集 + +采集链路采用两层分离:**YAML profile**(协议知识,随代码走)+ **`modbus_device` 数据库行**(部署/可配置信息)+ **`modbus_reading` 通用读数表**(JSON payload 遥测)。 + +- **profile**(如 `sdm120.yaml`)描述:读哪些寄存器(FC04 块读)、每个量的 key/unit/device_class/ha_component。纯协议知识,不含 unit_id / friendly_name 等部署项。 +- **`modbus_device` 行**:friendly_name、网关 host/port、Modbus slave `unit_id`(电表 Meter ID,设备面板可改故落 DB)、选用哪个 profile、采样周期、是否启用。 +- **`modbus_reading` 行**:device_id FK、recorded_at、payload(JSON,如 `{"voltage": 230.2, "current": 1.3, ...}`)。 +- 多设备可共享同一 profile(如两块 SDM120 共用 `sdm120` profile,各自独立 unit_id 和 friendly_name)。 +- APScheduler 后台 job 周期轮询所有 `enabled` 设备,更新 `last_poll_at` / `last_poll_ok`。全局开关 `MODBUS_POLLING_ENABLED`(CONFIG_FIELDS)。 + +**手工命令行试读**(不依赖 DB,最快验证网关连通性): + +```bash +# 按 profile 解码读一次(验证整套解码链路) +python -m scripts.modbus_cli read --host <网关IP> --port 502 --unit 1 --profile sdm120 + +# 手工指定请求内容(first-contact 验证,不依赖 profile) +python -m scripts.modbus_cli probe --host <网关IP> --port 502 --unit 1 --fc 4 --address 0x0000 --count 2 --decode float32 +``` + +CLI 工具为受控手工验证而设(设备需接市电),仅暴露读功能码(FC03/04),无写寄存器子命令。 + +### MQTT + Home Assistant Discovery + +后端作为 MQTT 发布方,把 Modbus 设备/工程量以 HA Discovery 协议自动注册为 device/entity: + +- 每个 Modbus 设备 = 一个 HA device(`unique_id` 锚定于设备 `uuid`,不随改名变) +- 各工程量 = sensor entity(device_class/unit 取自 YAML profile);另有 binary_sensor `online`(取 `last_poll_ok`) +- 每次轮询成功后推 state topic;连接成功或勾选变更时重发 retained discovery config +- `POST /api/config/mqtt/test`:试连 broker 并发布一条测试消息(可用 MQTT Explorer 验证链路) + +**Config 页配置流程**: +1. 在 `/config` 的「MQTT」section 填写 broker host/port/username/password,启用 `MQTT_ENABLED` +2. 点「发送测试消息」确认 broker 链路通 +3. 启用 `HA_DISCOVERY_ENABLED` +4. 在「Home Assistant Expose」面板勾选要暴露的实体,点「重新发布 discovery」 +5. 在 Home Assistant 确认对应 device/entity 出现 + +### API 端点(M5 新增) + +| 端点 | 用途 | +| --- | --- | +| `GET /api/modbus/devices` | 列出 Modbus 设备 | +| `POST /api/modbus/devices` | 新建设备 | +| `GET /api/modbus/devices/{uuid}` | 单个设备 | +| `PATCH /api/modbus/devices/{uuid}` | 修改设备(含 enable/disable)| +| `DELETE /api/modbus/devices/{uuid}` | 删除设备;有读数时 409 | +| `GET /api/modbus/devices/{uuid}/metrics` | 该设备 profile 的量目录(key/unit/device_class)| +| `GET /api/modbus/devices/{uuid}/latest` | 最新一条读数 payload | +| `GET /api/modbus/devices/{uuid}/readings` | 时间范围读数(start/end/limit),供走势图 | +| `POST /api/modbus/devices/{uuid}/test` | 即时试读(不落库) | +| `GET /api/modbus/profiles` | 列出可用 profile 名 + 描述 | +| `GET /api/expose` | 可暴露实体目录 + 勾选状态 + MQTT/Discovery 状态 | +| `PUT /api/expose` | 设置逐 key 暴露开关 | +| `POST /api/expose/republish` | 手动重发 discovery | +| `POST /api/config/mqtt/test` | 试连 broker 并发布测试消息 | + +### 前端视图(M5 新增) + +- **侧边栏**:把顶栏改为侧边导航(Home / Records / Energy / Config + 主题切换 + 注销),当前路由高亮,移动端可折叠。 +- **`/energy`(Energy 视图)**:设备 CRUD(新建/编辑/删除,删除有二次确认;有读数时引导改用禁用);最新读数卡片(字段标签/单位取自 profile metrics);时间序列走势图(Recharts,支持电压/电流/功率/电能,带时间范围选择)。 +- **Config 页 Accordion**:各大 config section 可独立折叠/展开;「Home Assistant Expose」面板按设备分组勾选可暴露实体、显示 MQTT/Discovery 连接状态、「重新发布 discovery」按钮。 +- SPA 路由新增 `/energy`。 + ## Config 持久化 当前 config 页面不会把修改写回 `.env`。 @@ -306,6 +391,9 @@ TOTP issuer 标签(显示在 Authenticator 里)通过 `AUTH_TOTP_ISSUER` 环 - SMTP 基础配置 - TickTick OAuth 配置 - Home Assistant 配置 +- MQTT broker 配置(`MQTT_ENABLED`、`MQTT_BROKER_HOST/PORT/USERNAME/PASSWORD`、`MQTT_TLS_ENABLED`) +- Home Assistant Discovery 配置(`HA_DISCOVERY_ENABLED`、`HA_DISCOVERY_PREFIX`) +- Modbus 采集配置(`MODBUS_POLLING_ENABLED`) 其中 SMTP password 与其他 secret 字段一致: diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index f06ac44..239da71 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -19,31 +19,36 @@ - `main.py` - FastAPI app factory - - lifespan + - lifespan(APScheduler 启停、MQTT 客户端起停、连接后触发 HA Discovery 发布) - 基础路由注册 - `config.py` - - 环境变量驱动的 settings + - 环境变量驱动的 settings(含 M5 新增的 MQTT/HA Discovery/Modbus 配置项) - `db.py` - 统一数据层:一个 `Base`、一个绑定 `app_database_url` 的 cached engine(SQLite WAL)、`get_engine` / `get_session_local` / `reset_db_caches` / `get_db_session` - `dependencies.py` - 通用依赖注入 - `api/` - HTTP routes - - `api/routes/api/`:JSON API(`/api/*` 前缀),供 React SPA 调用:会话/鉴权、配置读写、数据查询、记录 CRUD + - `api/routes/api/`:JSON API(`/api/*` 前缀),供 React SPA 调用:会话/鉴权、配置读写、数据查询、记录 CRUD、Modbus 设备 CRUD + readings + metrics + test(`/api/modbus/*`)、Expose 勾选 + 重发 discovery(`/api/expose`)、MQTT 测试连接(`/api/config/mqtt/test`) - 裸 ingestion 端点:`GET /public-ip/check`、`POST /homeassistant/publish`、`POST /poo/record`、`GET /poo/latest`、TickTick OAuth 等 - `models/` - SQLAlchemy models - - 所有模型(auth / config / public_ip / location / poo)共用同一个 `Base`,均落在单一 `app.db` 中 + - 所有模型(auth / config / public_ip / location / poo / modbus / expose)共用同一个 `Base`,均落在单一 `app.db` 中 + - M5 新增:`ModbusDevice`(设备部署层)、`ModbusReading`(通用遥测,JSON payload)、`ExposedEntityToggle`(HA 实体暴露开关) - `schemas/` - - Pydantic schemas + - Pydantic schemas(M5 新增 `modbus.py`、`expose.py`) - `services/` - 业务服务层 - 当前已迁入 config page 的 DB 持久化逻辑 - 当前已迁入 public IPv4 检查、状态持久化与变化通知逻辑 - 当前已迁入 SMTP 发信与测试发信逻辑 + - M5 新增:`modbus_poll.py`(采集 service,逐设备 poll + 落库 + 推 MQTT state)、`ha_discovery.py`(构建 HA Discovery payload、发布 retained config、发布 state) - `integrations/` - 外部系统适配层 - - 当前已迁入 Home Assistant outbound adapter + - Home Assistant outbound adapter(REST 通道,原有) + - M5 新增:`modbus/`(pymodbus 薄封装:`driver.py` 块读 + float32 解码;`profiles.py` YAML profile 加载/校验/解码;`profiles/sdm120.yaml` SDM120 协议声明) + - M5 新增:`mqtt.py`(paho-mqtt 长连接 `MqttManager`:lifespan 起/停、配置变更重连、`publish(topic, payload, retain)`) + - M5 新增:`expose.py`(通用 expose 框架:`ExposableEntity`、provider 注册表、`build_catalog`;Modbus provider 从 YAML profile 派生 sensor/binary_sensor 实体目录) - `static/` - 极简静态资源 @@ -76,6 +81,7 @@ React SPA 前端(M2 引入)。Vite + React + TypeScript + Mantine,由 Fast - `app_db_adopt.py`:App DB 接管 / 初始化 - `migrate_legacy_data.py`:一次性历史数据搬迁脚本 - `admin_cli.py`:Admin CLI 逃生通道(M4),见下方"登录加固"说明 +- `modbus_cli.py`(M5):Modbus 手工试读 CLI(`read`/`probe` 两个只读子命令),不依赖 DB,供受控手工验证链路连通性 ### `openapi/` @@ -93,12 +99,69 @@ M4 在基础 Argon2 + server-side session 鉴权之上叠加了三层防御: 详细说明:[`docs/auth.md`](./auth.md) +## M5 — 通用 Modbus 采集链路与 MQTT 通道 + +### Modbus 采集链路 + +``` +YAML profile(协议知识) modbus_device 行(部署信息,DB) + - 寄存器块/地址/类型 - host / port(网关 IP) + - 每量:key/unit/device_class - unit_id(Modbus slave 地址) + - ha_component - friendly_name / profile / poll_interval_s / enabled + │ │ + └────────────┬───────────────────────┘ + │ APScheduler job(轮询所有 enabled 设备) + │ driver.py: ModbusTcpClient → FC04 块读 → 大端 float32 解码 + │ profiles.py: decode(profile, registers) → dict[key → value] + ▼ + modbus_reading 行 + device_id · recorded_at · payload(JSON) + {"voltage": 230.2, "current": 1.3, "active_power": 295.0, ...} +``` + +**关键设计决策**: +- 命名分层:存储/采集/API 全部通用 `modbus_*`(`/api/modbus/devices`);面向用户的领域视图叫 **Energy**(第一个)。接入新设备型号只需新增 YAML profile,不改表/不改 API。 +- 读数为 JSON `payload`(无固定列),SQLite `json_extract` 在 DB 端做 AVG/GROUP BY(被 `(device_id, recorded_at)` 索引圈住)。 +- FK `ON DELETE RESTRICT`:有读数的设备拒删,引导改为 `enabled=false`。 +- 设备 `uuid`(uuid4 内部生成)是**稳定身份锚点**:API 路径键、HA Discovery `unique_id` 来源,不随 friendly_name 改变。 + +### 第二条 MQTT 通道(HA Discovery 发布) + +与已有 REST 通道(`app/integrations/homeassistant.py`、`POST /homeassistant/publish`)**并行、不冲突**: + +``` +MQTT 通道(M5 新增): + paho-mqtt MqttManager(lifespan 长连接,配置变更可重连) + │ + ├─► Discovery config(retained) + │ topic: ////config + │ 内容:device 块(identifiers=uuid)、state_topic、unique_id(uuid+key)、 + │ name(friendly_name)、device_class、unit_of_measurement、availability + │ 时机:连接成功时 / 目录或勾选变更时(全量重发); + │ 取消勾选时发空 payload(清除 entity) + │ + └─► State / Availability(非 retained) + 时机:每次轮询成功后推该设备所有 enabled entity 的最新值; + 周期兜底 job 重推所有 enabled entity + online topic + +expose 框架: + provider 动态产出 ExposableEntity 目录(元数据从 YAML profile 派生) + ExposedEntityToggle 表:只存逐 key 开关(default=disabled) + build_catalog(session) → 目录 + 勾选状态(合并所有 provider) +``` + +**HA entity 身份模型(Z2M 语义)**: +- `unique_id` = `f"{device.uuid}_{metric.key}"`(稳定,改名不变) +- `name` = `friendly_name`(改名重发 discovery,HA 显示名跟着变、历史不丢) +- 每设备除各 sensor entity 外,另有 `binary_sensor` `online`(取 `last_poll_ok`)——这是"不止 sensor"的体现 + ## 当前约束 - 当前数据库继续使用 SQLite - ~~当前不引入前后端分离~~ **已退役(M2)**:现为 React SPA + JSON `/api` 层,由 FastAPI 同源托管 - 当前不设计 Notion 模块 - 当前通知能力仍保持极小范围,不引入独立通知中心或多渠道抽象 +- Modbus 当前**仅 TCP**(Waveshare RTU↔TCP 网关),**只读**(FC03/04),不写设备寄存器 ## 关于 Notion diff --git a/docs/design/m5-iot-energy.md b/docs/design/m5-iot-energy.md index 06c3fce..444e736 100644 --- a/docs/design/m5-iot-energy.md +++ b/docs/design/m5-iot-energy.md @@ -461,7 +461,7 @@ Phase C(MQTT / Discovery,依赖 B 的设备数据与 provider 接口) - **Reviewer checklist**: PUT 只改 toggle 不误碰其它配置;republish 真触发 T11;类型化 client。 ### M5-T13 — 文档 + OpenAPI + roadmap 收尾 -- **Status**: `todo` · **Depends**: 全部 +- **Status**: `done` · **Depends**: 全部 - **Files**: `modify README.md`(Modbus/Energy/MQTT 段、新依赖)、`docs/roadmap.md`(M5 行 + 把"MQTT/IoT"从"下一阶段"毕业、新增 Modbus 采集方向)、`docs/architecture-overview.md`(新增 MQTT 通道与 Modbus 采集);`modify docs/design/README.md`(列入 m5);`run python scripts/export_openapi.py` 并提交 `openapi/` - **Acceptance criteria**: - [ ] 文档反映新链路;`git diff --exit-code openapi/` 无未提交差异。 diff --git a/docs/roadmap.md b/docs/roadmap.md index ee0772e..9041088 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,7 +2,7 @@ 本文档记录 `home-automation` 在 `v1.0.3` 之后的下一阶段规划。这一阶段不是小修补,而是几次较大的结构性改动:单库化、前端重写、以及远期的移动端试水。 -> 每个里程碑的**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)、M4 [`m4-login-hardening.md`](./design/m4-login-hardening.md)。这些文档为 Orchestrator→Implementer→Reviewer 的多模型流水线设计。 +> 每个里程碑的**可执行原子任务**展开在 [`docs/design/`](./design/README.md):M1 [`m1-db-consolidation.md`](./design/m1-db-consolidation.md)、M2 [`m2-frontend-v2.md`](./design/m2-frontend-v2.md)、M3 [`m3-token-mobile.md`](./design/m3-token-mobile.md)、M4 [`m4-login-hardening.md`](./design/m4-login-hardening.md)、M5 [`m5-iot-energy.md`](./design/m5-iot-energy.md)。这些文档为 Orchestrator→Implementer→Reviewer 的多模型流水线设计。 ## 当前基线(v1.0.3) @@ -37,7 +37,7 @@ | **M1** ✅ | 单库化地基 | 把三库合并成单一 `app.db`,清理散落数据层,删掉 Grafana | | **M2** ✅ | 前端 v2 | React SPA 取代 Jinja,承载 config + 可视化 + 记录增删改 | | **M4** ✅ | 登录加固 | 防爆破/指数退避 + CLI 逃生通道 + 可选 TOTP 二次验证(**先于 M5**) | -| **M5** | IoT / 能耗采集 | Modbus/Energy + MQTT/HA Discovery + 前端侧边栏 | +| **M5** ✅ | IoT / 能耗采集 | 通用 Modbus 采集(YAML profile + JSON readings)+ MQTT/HA Discovery + 前端侧边栏 + Energy 视图 | | **M3** | 开放与移动端(远期试水) | token 鉴权 + React Native 移动端 | 排序原则:**先清地基,再在干净结构上盖楼。** M2 的新 API 和 React 必须建立在合并后的单库之上;M4 是公网安全加固,在 M5 IoT 集成之前先堵住裸密码这个洞;M5 在安全基座就绪后再做 IoT 接入。 @@ -150,6 +150,32 @@ --- +## M5 — IoT 集成与能耗采集(✅ 已完成) + +### 目标 + +给后端接入家庭 IoT 生态,建立通用 Modbus 设备采集链路(首个领域:能耗),接入 MQTT + Home Assistant Discovery,并重构前端为侧边导航并新增 Energy 视图。 + +### 范围 + +- **两层数据模型(协议与部署分离)**:YAML profile(随代码走,声明协议知识:寄存器/解码/key/unit/ha_component)+ `modbus_device`(部署层 DB 行:friendly_name/host/port/unit_id/profile/poll_interval/enabled)+ `modbus_reading`(通用遥测:device_id + recorded_at + JSON payload)。多设备可共享同一 profile。 +- **通用 Modbus-TCP 采集**:pymodbus,APScheduler 后台 job 轮询所有 enabled 设备;per-device `last_poll_at`/`last_poll_ok`;全局开关 `MODBUS_POLLING_ENABLED`(CONFIG_FIELDS)。首个 profile:SDM120 单相电表。 +- **手工 CLI 试读**:`scripts/modbus_cli`(`read`/`probe` 两个只读子命令),供受控手工验证,不进自动化。 +- **MQTT + HA Discovery**:paho-mqtt 长连接;通用 expose 框架(provider 动态产出可暴露实体目录,元数据从 YAML profile 派生);`exposed_entity_toggle` 表存逐 key 开关(默认不暴露);每设备 = 一个 HA device,各量 = sensor entity + online binary_sensor;`unique_id` 锚定于设备 `uuid`(稳定,不随改名变);配置变更可重连重发 discovery。 +- **前端侧边栏**:`AppShell` 侧边导航(Home / Records / Energy / Config + 主题切换 + 注销),移动端可折叠,当前路由高亮。 +- **Energy 视图**(`/energy`):设备 CRUD;最新读数卡片(标签/单位取自 profile metrics 端点);Recharts 走势图(时间范围 + limit)。 +- **Config 页 Accordion**:各大 section 折叠/展开;「Home Assistant Expose」面板勾选可暴露实体 + 连接状态 + 重新发布按钮。 +- **API**(`/api/modbus/*` + `/api/expose` + `/api/config/mqtt/test`):完整 CRUD + readings + metrics + test + expose 勾选 + 重发 discovery。 +- `pytest`/`ruff`/`export_openapi` + 前端 `lint/typecheck/test/build` 全绿,`openapi/` 已入库。 + +### 命名决策(已锁定) + +存储/采集/API 全部使用通用 `modbus_*`(`/api/modbus/devices`),**不锁死"电表"**;面向用户的领域呈现叫 **Energy**。接入新 Modbus 设备型号只需新增 YAML profile,无需改表/改 API。 + +> 详细设计与任务卡:[`docs/design/m5-iot-energy.md`](./design/m5-iot-energy.md) + +--- + ## M3 — 开放与移动端(远期试水) ### 目标 @@ -169,13 +195,13 @@ ## 下一阶段:已确定要做(尚未拆解为任务卡) -> 这些是 M4 之后**已经定下来要做**的方向——区别于下面的 Future Ideas(仅备忘、未必做)。这里只记到 roadmap 粒度:确定**做什么、为什么**;具体排期、依赖与原子任务,等动手时再展开成 `docs/design/` 的任务卡。**先后顺序未定**,具体排期等动手时再定。 +> 这些是 M5 之后**已经定下来要做**的方向——区别于下面的 Future Ideas(仅备忘、未必做)。这里只记到 roadmap 粒度:确定**做什么、为什么**;具体排期、依赖与原子任务,等动手时再展开成 `docs/design/` 的任务卡。**先后顺序未定**,具体排期等动手时再定。 ### 1. 前端优化 **动机**:M2 的 React SPA 先把功能跑通,性能 / 体验层面的打磨还没做。这一项**确定要做,但具体优化什么还没定**。 -**范围(待定)**:方向先留空,想清楚再细化。可能的候选(仅占位、非承诺):打包体积与代码分割(M2 构建已提示存在 > 500 kB 的单 chunk)、首屏加载、热力图 / 地图的渲染性能、移动端适配、可访问性等。等确定具体目标后再拆任务卡。 +**范围(待定)**:方向先留空,想清楚再细化。可能的候选(仅占位、非承诺):打包体积与代码分割(M2/M5 构建已提示存在 > 500 kB 的单 chunk)、首屏加载、热力图 / 地图的渲染性能、移动端适配、可访问性等。等确定具体目标后再拆任务卡。 ### 2. 设置页生成 Long-lived Token(供 API 调用)