Files
MeowOS-For-R3P2/patch_v2/meowpanel/README.md
T
AskaEth d7bf62f448 fix(meowpanel §UTF-8): JSON 输出 UTF-8 加固——sA_str 统一消毒非法/截断字节,防任一上游坏字节拖垮整页
事故:amsd 的 am list 把「流媒体客户端」(18B) 截成 16B,腰斩「端」字(E7 AB AF→仅剩 E7) →
面板 ams_fill 把非法 UTF-8 嵌进 /api/overview → 浏览器 JSON.parse 拒收(HTTP 200 非 JSON)。
amsd 侧已修(8588a6b),面板侧自此防住任一上游再产坏字节,一个源的坏数据不拖垮整页。

- meowpanel.c: 新增 utf8_seq_len/utf8_seq_ok/utf8_valid_n/utf8_sanitize(C 无依赖);sA_str 收口为
  sA_str_n:逐字节校验 UTF-8,合法序列透传、非法/截断逐字节回退 '?'(保持长度);纯 ASCII 单遍直通。
- 覆盖点(全部经 sA_str/sA_str_n):ams_fill(pkg/name)、svcd_fill(name/exec/state/...)、audio_fill(app/fmt/route_mode)、
  /api/apps(app.json name/pkg/ver/exec)、字体名、通知字段、崩溃/日志中心字段、设置/主题串等。
- 两处日志内联转义(api_net_log、api_logs_read)改走 sA_str_n,日志行同样消毒。
- 原始 IPC JSON(perf/power/net,含 ssid)先 utf8_valid_n 校验:坏字节时 perf/power/net/status 降级
  available:false / 503,不嵌坏字节(api_overview 首页生命线不受影响;api_system network 段坏字节回退 hostname_local)。
- 宿主自测 `--selftest`:喂「流媒体客户\xE7」给 utf8_sanitize/sA_str/ams_fill 桩,断言 JSON 完整可解析 + 无非法字节,
  且不误改完整合法名「流媒体客户端」;`meowpanel --selftest` 通过。
- 验证:build_meowpanel.sh host / 交叉均 -Werror 0 warning;真实 ams_fill 桩复现事故 → 「流媒体客户?」JSON 合法。
- 后端 meowpanel(mipsel) md5: 820b24541ed69f2e27e4a301aff10c98(README 已校正)
2026-09-02 02:32:25 +08:00

503 lines
53 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MeowPanel 后端(`meowpanel.c`)
MeowOS(HiBy R3 Pro II 第三方系统,MIPS32 静态交叉编译)的**外置 Web 面板后端**:单进程、零第三方依赖的
C 静态二进制,内嵌 HTTP/JSON 服务器 + 前端静态托管 + 框架专项服务 IPC 客户端 + `settings.conf` 设置镜像。
- **定位**(`MEOWPANEL_DESIGN.md`):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件,由 `meow_svcd`
读 TF 卡 `MeowOS/services.d/meowpanel.conf` 启动、`meow_sysui` 监管。**不进固件**;无卡时系统核心
(launcher/sdk/设置/通知)绝不依赖它。
- **协议**:`devdocs/plan/MEOWPANEL_PROTOCOL.md`(HTTP/JSON 端点;本实现以「实现为准,协议/README 记录出入」)。
- **数据路径**(`SYSTEM_FRAMEWORK_SERVICES.md §2/§4`):读侧(`/api/{status,perf,system}`)经 `meow_svc_perf`
IPC(`MSG_PERF_QUERY→RESULT`)聚合,**绝不各自爬 /proc**(M2.73 起 perf 聚合含 `network` 段);动作侧(`/api/system/{reboot,shutdown,ota}`)经
`meow_svc_power` IPC(`MSG_POWER_ACTION→STATUS`),不再直连 `poweroff`/写 mtd5。两专项服务为**板载 svcd 托管**
(`trigger=boot`),拔卡保留,与无卡依赖最小化自洽。
- **⚠️ OTA 检查升级 ≠ 升级**:前端「检查升级」只读 `GET /api/system` 的 `ota{current,available,latest}`,**勿 `POST /api/system/ota`**(那是触发升级进升级模式)。
---
## 1. 编译
工具链:交叉编译器 `/home/Aska/Project/R3ProII/.tools/zig-mipsel-cc`(mipsel 32 位、musl 静态,可用)。
```bash
cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
./build_meowpanel.sh # 交叉编译 → meowpanel(mipsel 静态,firmware-ready)
./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试)
```
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
- 产出(本次构建实测):
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
体积约 **256 KB**(固件阈值内)。md5:`820b24541ed69f2e27e4a301aff10c98`(UTF-8 加固:`sA_str` 统一嵌 JSON 字符串做 UTF-8 消毒,非法/截断字节逐字节回退 `?`——amsd 曾把「流媒体客户端」截成 16B 腰斩「端」字导致浏览器拒收整页,面板侧防任一上游再产坏字节;`--selftest` 宿主回归;原始 IPC JSON(perf/power/net 含 ssid)先 `utf8_valid_n` 校验,坏字节降级不拖垮 /api/overview)。
**重防火墙注意**:运行中二进制先 `.new` 再 `mv`,切勿直接覆盖运行中的 `meowpanel`)。
- `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **148 KB**。
- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。鉴权用**内联 SHA-256**(拷贝自
`meow_verify/sha256.c` 公有领域参考写法,前缀 `mp_sha256_*` 避免符号冲突;**未改 `meow_verify/` 目录**)。
阶段 C 实时画面用**自研最小 baseline JPEG 编码器** `mp_jpeg.c`(顺序 DCT + 4:2:0 + 标准 Huffman 表 Annex K;
无 libm、零依赖,仅 malloc/realloc),质量 70 下 480×720 帧约 41 KB。
**字体库导入/导出(用户 2026-08-31 定案)**:与 `.mpk` 同构、**放弃 zip,改走 busybox `gzip -dc | tar` 管道**
(设备必带 tar/gzip,零新增依赖、不引入 zlib/miniz/自研 zip,见 §5.11)。
**性能**:设备为 mipsel muslsf **无 FPU**,初版逐块 `double` 矩阵 DCT 编码 480×720 一帧约 **3.96 s**(掉帧到 5-6 s/帧的根因)。
已改为**整数定点 DCT + 整数 YCbCr**(FSCALE=13,BT.601 256 系数,纯整型乘加),
实测编码降至约 **0.19 s(≈20×)**,单请求端到端(本机 localhost)约 0.20 s、LAN 约 0.22-0.31 s,
质量/体积与 double 版 PSNR 等价(主机 480×720 高频测试图两者差 <0.1 dB)。
- 头文件引用:`-I patch_v2/meow_launcherd`(`meow_ipc.h` 帧格式 / `m_perf_*`/`m_power_*`/`POWER_*`,
**header 为权威源**,结构体字段以它为准)。
## 2. 部署到 TF 卡
```bash
# 1) 后端 + 前端(web/index.html + meowpanel.css + meowpanel.js)放在同一目录
sudo mkdir -p /usr/data/mnt/sd_0/MeowOS/opt/MeowPanel
sudo cp patch_v2/meowpanel/meowpanel /usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/
sudo cp patch_v2/meowpanel/web/index.html /usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/
sudo cp patch_v2/meowpanel/web/meowpanel.css /usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/
sudo cp patch_v2/meowpanel/web/meowpanel.js /usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/
# 2) 服务配置(svcd 读 TF 卡 services.d 拉起)
sudo mkdir -p /usr/data/mnt/sd_0/MeowOS/services.d
sudo cp patch_v2/meowpanel/services.d/meowpanel.conf /usr/data/mnt/sd_0/MeowOS/services.d/
```
说明:
- 前端资源与后端**同目录**(后端 `GET /` → `index.html`,`GET /meowpanel.css`/`meowpanel.js` 同目录取)。
后端也支持 `GET /assets/<file>`(映射到同目录文件),前端当前用根相对路径。
- 服务启动后整卡由 `meow_svcd` 按 `trigger=boot` 拉起;`meow_sysui` 监管(restart/backoff/max_restart)。
- **无卡**:`MeowOS/services.d/` 不可读 → svcd 跳过本服务;系统核心照常。
## 3. 访问
浏览器(同局域网):
```
http://<host>.local:8080/
```
- mDNS 由板载 `meow_mdns` 应答 `<host>.local`。
- 端口默认 **8080**,可用 `-p PORT` / 环境变量 `MEOWPORT` / 与后端同目录 `meowpanel.conf`(`port=N`)覆盖。
(优先级:`-p` > `MEOWPORT` > `meowpanel.conf` > 默认 8080。)
- **访问级 PIN 鉴权(用户 2026-09-01 定案)**:访问面板即需登录(4 位 PIN,`panel.pin`,见 §5.9);未认证请求一律 401。
除 `/api/auth/{login,setup,logout,status}` 外全部 `/api/` 需有效会话(token 存面板进程内存,重启失效,3 天固定不滑动续期)。
- 上传/下载/管理类请求统一带 `X-MeowToken: <token>`(或 `Authorization: Bearer <token>` / `Cookie: meow_token=<token>`);
缺/错/过期 → 401 `UNAUTHORIZED`。**HTTP 明文(无 TLS),内网使用;请勿把 8080 暴露公网**。
---
## 4. 端点实现清单(对照 MEOWPANEL_PROTOCOL §2 端点总览)
### 已实现(返回 200/2xx 或语义错误码)
| 方法 | 路径 | 说明 | 实现 |
|---|---|---|---|
| GET | `/`、`/meowpanel.css`、`/meowpanel.js`、`/assets/<f>` | 静态托管 | ✅ |
| GET | `/api/screen.jpeg` | 实时画面:fb0 当前帧 → RGB565→JPEG(`?q=1-100` 质量,默认 70;`?delta=1` 开启增量帧,见 §5.10) | ✅(阶段 C) |
| POST | `/api/input/tap` | 触摸注入:`{x,y}`(480×720 原生坐标;write 进 goodix event1,本机实测可投递);本机触摸后 5s 内忽略注入 | ✅(阶段 C) |
| GET | `/api/auth/status` | 访问鉴权状态(auth_enabled/session/pin_set/legacy/locked/lock_remaining_ms) | ✅(公开) |
| POST | `/api/auth/setup` | 首次设置 4 位 PIN(仅未设 PIN 时;`{pin}` 4 位数字;已设→409) | ✅(公开;非法→422) |
| POST | `/api/auth/login` | PIN 登录发 token(`{pin}`;未设 PIN 时接受旧密码 `{password}` 迁移;错→401,连错 5 次→429 LOCKED) | ✅(公开) |
| POST | `/api/auth/logout` | 登出失效 token | ✅(公开;幂等) |
| GET | `/api/status` | 性能+系统(`meow_svc_perf` IPC subset=all 透传) | ✅ |
| GET | `/api/perf` | 仅性能(subset=perf 透传) | ✅ |
| GET | `/api/system` | perf IPC + boot/network/ota 局部补全(network 含蓝牙,无网口;`ota` 只读检查,勿误触升级) | ✅ |
| POST | `/api/system/reboot`、`/shutdown`、`/ota` | `meow_svc_power` IPC | ✅ |
| GET | `/api/settings` | 全量+元数据(items/count/groups) | ✅ |
| GET | `/api/settings/:key` | 单项 | ✅ |
| PUT | `/api/settings/:key` | 写单项(clamp/枚举校验) | ✅(越界/非法枚举→422) |
| POST | `/api/settings` | 批量写 | ✅ |
| POST | `/api/settings/restore` | 恢复默认 | ✅ |
| GET | `/api/theme` | 读主题 | ✅ |
| PUT | `/api/theme` | 写主题(style/theme/background 子集) | ✅(非法→422) |
| GET | `/api/files?path=` | 目录列表(白名单) | ✅(无卡→503) |
| GET | `/api/files/read?path=` | 读文本(<1MB;二进制→415) | ✅ |
| GET | `/api/files/download?path=` | 下载流 | ✅ |
| POST | `/api/files/upload` | multipart 上传 | ✅ |
| POST | `/api/files/mkdir` | 新建目录(已存在→409) | ✅ |
| POST | `/api/files/rename` | 重命名 | ✅ |
| DELETE | `/api/files?path=` | 删除(可 recursive=1;非空未 recursive→409) | ✅ |
| GET | `/api/fonts` | 字体库列表(扫描 `MeowOS/fonts/<family>/<weight>/` 三件套齐) | ✅(无卡→503) |
| POST | `/api/fonts/import` | 上传字体库 `.tar.gz`(multipart `file`,可带 `?force=1` 覆盖) | ✅(缺件/坏json/穿越→400,冲突→409,超限→413) |
| GET | `/api/fonts/export` | 导出 `<family>/<weight>/` 三件套为 `.tar.gz`(`?family=&weight=`) | ✅(缺件→404) |
| GET | `/api/audio` | 音频状态快照:meow_audiod IPC `MSG_AUDIO_QUERY→STATE` 透传(当前流/路由/音量/underrun + **`rms/peak/lvlseq` 电平 + `vol_max`**,见 §5.12) | ✅(audiod 不在→`available:false`,不报错) |
| POST | `/api/audio/route` | 手动切输出路由(**写级**,PIN 已护):`MSG_AUDIO_ROUTE`,仅接受 `{port: 0\|2\|3}`,**绝不接受 1/4/5**(4=SPDIF、5=audiod 内部强制重烧值);前端切换前确认弹窗(选错口会无声) | ✅(非法端口→422;audiod 不在→503) |
| POST | `/api/audio/volume` | 写音量(**写级**,PIN 已护):`MSG_AUDIO_VOLUME`,**clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,缺省 100;设备当前 50);audiod 仅 clamp 0-100 无 vol_max 防护,面板是把守方(防炸耳红线) | ✅(clamp 后写;audiod 不在→503) |
| GET | `/api/power` | 功耗管理:meow_powermgr IPC `MSG_PM_QUERY→RESULT`(battery/estimate/breakdown,confidence 原样透传) | ✅(powermgr 不在→503 `POWER_UNAVAILABLE`) |
| GET | `/api/ams` | App 运行表:`am list`(pkg/name/state/pid/launches/crashes/last_kind/last_code) | ✅(amsd 不在→`available:false`) |
| GET | `/api/svcd` | 服务表:`meow_svcd status all`(name/state/pid/restart/backoff_ms/trigger/src/exit/exec) | ✅(svcd 不在→`available:false`) |
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power/audio/net/ams/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
| GET | `/api/apps/:pkg/export` | `nyai export` 下载流 | ✅ |
| DELETE | `/api/apps/:pkg` | `nyai remove [--purge]` | ✅ |
| POST | `/api/key/:code` | 按键注入(tap/long) | ✅(物理键红线内) |
| POST | `/api/key` | 批量/顺序注入 | ✅ |
| GET | `/api/logs` | `MeowOS/logs` 文件列表(每文件含 `group` 来源分组) | ✅(无卡→503) |
| GET | `/api/logs/crash` | **崩溃汇总**:解析最新 `crash_<bootts>.log`(meow_crash 落盘格式)→ 每实体崩溃数 + 最近崩溃时间/信号 + dmesg 计数 | ✅(无卡→503;无 crash 日志→空汇总) |
| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ |
| GET | `/api/notifications` | 通知列表(**优先 launcher IPC 内存活跃集** `source:"ipc"`;IPC 不可达回退 TF 转储日志 `source:"tf"`;见 §5.8) | ✅(无源→空态 `source:none`,不 503) |
| DELETE | `/api/notifications/:id` | 单条删除(**优先 IPC 撤下 launcher 内存**;IPC 不可达回退重写日志移除该 id) | ✅(不存在→404) |
| POST | `/api/notifications/dismiss_all`(别名 `/api/notifications/clear`) | 一键清空:**优先 IPC 联动 launcher 内存**,保留活动中的媒体/进度通知;IPC 不可达回退重写日志 | ✅ |
| GET | `/api/files/read?path=` | 读文本:预览上限 1MB,超限 `truncated:true`(不再 404);二进制→415 | ✅ |
### 返回 501 `NOT_IMPLEMENTED` 的端点(诚实标注)
| 端点 | 原因 |
|---|---|
| `/api/key/:code` 的 `press:"hold"` | `hold` 需常驻持续注入通道(阶段 D),后端未实现;`tap`/`long` 已实现。 |
| OTA 升级模式(`/api/system/ota` 带 `url`) | 升级为草案(MEOWPANEL_PROTOCOL §6 待定案);当前 `POST /api/system/ota` 空 body 触发 `meow_svc_power` 的 `POWER_UPGRADE`(写 mtd5 进升级模式),带 `url` 的下载+升级未实现。 |
> 通知端点已从 501 落地,且现为**实时**:优先经 launcher IPC `MSG_NOTIFY_QUERY(0x25)→QUERY_RESULT(0x27)` 取
> **内存活跃集**(`source:"ipc"`);IPC 不可达/超时回退读 TF 卡通知转储日志(`source:"tf"`,会话级归档);两者皆无
> `source:"none"`。撤/清(`DELETE` / `dismiss_all`)同样**优先走 IPC**联动 launcher 内存(launcher 撤下时自动归档留档)。
> 详见 §5.8。
> 前端对 `501` 显示「该功能后端尚未实现」(`ok:false,error.code=NOT_IMPLEMENTED`)。
---
## 5. 关键实现说明
### 5.1 HTTP 路由 / JSON 序列化
- **单线程 + poll 串行**(面板访问量极小)。`accept` → `read_request`(先读头到 `\r\n\r\n`,**在缓冲区内搜索**头边界,
处理「头+body 同段到达」,再按 `Content-Length` 精确读 body)→ `handle` → 写响应 → 关闭连接(`Connection: close`)。
- 读/写均有 `poll` 超时(读头部 5s、body 分段 5s,IPC 2s)。`SIGPIPE` 忽略。
- JSON 响应信封:成功 `{"ok":true,"data":<payload>}`;失败 `{"ok":false,"error":{"code","message"}}`
(MEOWPANEL_PROTOCOL §1.1/§1.2,HTTP 状态码映射照抄)。
- **无 JSON 库**:手写最小递归下降解析器(对象/数组/字符串转义/数字/bool/null),仅覆盖实际用到的收发。
- 下载/上传用流式(`multipart/form-data` 极简解析:按 boundary 切,取 `path`+`file` 字段)。
### 5.2 IPC 对接(meow_svc_perf / meow_svc_power)
- **帧**:复用 `meow_ipc.h`——16B 头(magic `0x4D4F5701`/version 1/type/req_id/len,全小端)+ 变长负载,
`MEOW_IPC_MAX=4096`。帧读写 `x_read_full`/`x_write_full` + `send_frame`/`recv_frame`(与 meow_svc_perf.c 对称)。
- **握手**:连接后先 `MSG_HELLO`(`app_id="os.meow.panel"`,系统面板身份,§5 二期拟名)→ 读 `MSG_ACK(code=0)`。
- **读侧**:`MSG_PERF_QUERY`(`m_perf_query_t.subset`)→ `MSG_PERF_RESULT`(`m_perf_result_t`,`code`/`json_len`/`json`),
**直接透传**服务端聚合 JSON(`/api/status`=全部、`/api/perf`=perf 子集、`/api/system`=全量取 uptime/storage/tfcard/system)。
读取防溢出:`json_len` 以 `sizeof(m_perf_result_t.json)-1` 与 `cap` 双钳位。
- **动作侧**:`MSG_POWER_ACTION`(`m_power_action_t.cmd=POWER_REBOOT/SHUTDOWN/UPGRADE`)→ `MSG_POWER_STATUS`(`code`/`msg`)。
收到 `MEOW_OK` → `{"accepted":true}`;否则按 `code`(PERM→403 / NOENT/IO→503 / 其余→500)返回错误信封。
- **失败语义**:socket 不存在/连接拒/超时/应答非 OK → 返回 500 `INTERNAL`(或 503 `NO_CARD` 当缺升级文件)
并带中文 message。**绝不静默失败、绝不降级直连 /proc 或 poweroff**(单一权威源)。
- 连接超时:非阻塞 connect + `poll(POLLOUT)` 2s,`SO_RCVTIMEO`/`SO_SNDTIMEO` 兜底。
### 5.3 设置镜像(读写板载 `settings.conf`)
- **不旁路**:只经 `settings_get_value`(`=` 切分 + key 去首尾空白精确匹配,防 `audio.volume` 误配 `audio.volume_max`,
照 `meow_ui_vol_read`/`meow_svc_power settings_get_int_trim` 同款解析)与 `settings_write_one`(`"key = value\n"`,
保留 `# meow-settings 1` 头、逐行重写、缺键追加;目录缺失自动 `mkdir`)。**不绕过此函数直接改文件**。
- **键表/默认值来源**:`patch_v2/meow_launcherd/meow_settings.c` 的 `g_defs[]`(**权威来源**,逐键一致),
含 type/def/min/max/enum_names/enum_labels;`GET /api/settings` 返回 `items/count/groups`(分组按键前缀,
如 `display.*`→显示、`font.*|ui.*`→字体主题…)。
- 类型:`int`→number(带 min/max/def 字符串)、`bool`→原生布尔、`enum`→`options(name/label)+default_name+def`、
`str`→string。**越界/非法枚举 → 422 `UNSUPPORTED`**。
- 语义 clamp:`display.brightness` 读/写都 clamp 到 `[display.brightness_min(默认1), 100]`(防 101 闪屏,
与 `meow_settings_brightness_clamped`/`clamp_brightness` 同口径);`boot.default` 写后同步 `/data/meow_boot_default`
(meow→0/hiby→1,同 `meow_settings_save`)。
- 注:后端为独立进程,无法调用 launcher 进程内的 `meow_settings_*`(那是其内存表),故**直接镜像同一份
`settings.conf`**,保持与实体设置页/CLI 同一存储契约(协议 §3.2 说「经 meow_settings_*」,此处以
「镜像同一文件 + 同款 parse/save 契约」落地,见 §8 出入记录)。
### 5.4 文件白名单
- 根 `/usr/data/mnt/sd_0`;白名单子目录 `MeowOS/Music/MeowData/MeowAPPs/book`(`allowed` 数组)。
- 路径经 `normalize_path` 规范化(去 `.`/`..`,拒绝逃逸出根)后 `path_in_whitelist` 校验(== 根或属于某一白名单
一级子目录)。**`..`/绝对路径逃逸 → 403 `FORBIDDEN`**;无卡 → 503 `NO_CARD`。
- 读取拒绝敏感名(`settings.conf`/`setting.bak`/`.env`/`passwd`/`shadow`/`meow.conf`/`user.ini`/`config.json`)。
- 列表/上传/下载/读文本(1MB+二进制→415)/mkdir(已存在→409)/rename(new_name 不含 `/`)/delete(递归,非空未 recursive→409)。
### 5.5 nyai 对接
- 包管理器 CLI 绝对路径 `/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai`(M2.12+ 固件经 `meow_card_path.sh` 前插 PATH,可直接调;
本后端用绝对路径,兼容更早固件)。包名 = `app.json` 的 `package` = `.mpk` 文件名茎。
- `install`:multipart 上传 `.mpk` 落临时文件(`MeowOS/tmp/`)或 JSON `{path}` → `nyai install <file>`;用 `popen` 捕获输出+退出码,
非 0 → 422。`export`:`nyai export <pkg> <tmp>` → 以 `Content-Disposition: attachment` 流式下发。`remove`:`nyai remove <pkg> [--purge]`。
- `GET /api/apps` / `:pkg` 详情:直接扫描 `MeowAPPs/<dir>/app.json`(手写 JSON 解析提取 name/version/exec/framework/immersive/fullscreen),
不依赖 nyai(nyai 未部署时列表/详情仍可用)。解析失败项 `invalid:true`。
- `nyai` 未部署 → install/export/remove 返回 500(带 nyai 输出)。
### 5.6 按键注入通道
- **复用现有先例**:`patch_v2/uinput_tap/uinput_tap.c` 的 `write`-到-evdev 注入机制。launcher 实际轮询
`meow_launcher_internal.h`:`/dev/input/event0`(md-gpio-keys: POWER/PREVIOUS)、`/dev/input/event2`
(jz adc keyboard: VOL/PLAY/NEXT)。后端 open `O_WRONLY` 写 `EV_KEY press/release + SYN_REPORT` 即可到达 launcher 按键处理。
- **物理键红线**(APPS.md / 任务约束):只注入音量(114/115)/电源(116)/播放(164)/上下曲(163/165);
**禁止注入 返回(158)/主页(172)/确认(28)/退出(96)** → 403 `FORBIDDEN`。
- `tap`(默认,抬起间隔 80ms)、`long`(按住 ms,缺省 3000);`hold` → 501。
- 局限:若 `/dev/input/event0/event2` 在设备上不可写(权限/路径差异),注入失败 → 500;注入为**即时性注入**,
不在 launcher 内维护长按状态机(与实体按键的 `longpressed` 语义近似,见 README 已知边界)。
### 5.7 日志
- `GET /api/logs`:`MeowOS/logs` 下常规文件列表(跳过敏感名)。
- `GET /api/logs/read?file=&lines=`:`file` 必须相对 `MeowOS/logs/`,**禁 `..`/绝对路径 → 403**,敏感名 → 403;
缺省 `lines=200`,上限 `1000`;返回 `{file,lines[],total,truncated}`。
### 5.8 通知(数据源 = 优先 launcher 内存活跃集,回退 TF 归档)
- **数据源优先级**:① launcher IPC 内存活跃集(实时,`source:"ipc"`)→② TF 转储文件(`source:"tf"`)→③ 空态(`source:"none"`)。
- ① **launcher IPC**:后端连 `/usr/data/meow_ipc.sock`,`MSG_HELLO` 握手(`app_id=os.meow.panel`)→ `MSG_NOTIFY_QUERY(0x25)`
(空 payload=全部 app)→ 期望 `MSG_NOTIFY_QUERY_RESULT(0x27)`(`m_notify_query_result_t`,JSON 数组文本,字段同 TF 归档,
`ts` 为 epoch **毫秒**)。launcher 在**主循环单写者**序列化(绝不把通知存储读到 IPC 线程)。后端按 `ms` 升序重排
(oldest..newest,与 TF 一致),emit 时倒序= newest first;`ts` 除以 1000 出秒(与 TF 同口径)。
- ② **TF 转储文件**(回退):`<卡根>/MeowOS/notifications/notifications.log`(launcher `meow_notif_dump_tf` 一行一条 CSV,
字段 `timestamp,id,app_id,app_icon,category,urgency,progress,title,body,target_activity,ongoing,media,dismissed`;
`meow_notif_clear_tf` 开机清空 → 会话级归档)。后端逐行流式读取(环形保留最新 `NOTIF_PREVIEW_MAX=200` 条,不全量进内存)。
注意 TF 是**归档快照**(归档时 `dismissed` 尚未置 1,故该列恒 ≈0 不可靠,且非活跃快照)。
- **`GET /api/notifications`**:返回 `{count, items:[...], notifications:[...], source}`;`items`= `notifications` 别名
(兼容前端读 `.notifications` 与任务约定空态 `items:[]`)。字段含 `id/app_id/app_name/app_icon/category/summary/body/
urgency/ts/progress/ongoing/media/target_activity/dismissed`。`progress<0`→`null`;`app_name` 对 `system`→`系统`,其余用 `app_id` 兜底。
无源(无卡/文件不存在)→ `{count:0,items:[],notifications:[],source:"none"}`(**不 503、不伪造数据**)。
- **`DELETE /api/notifications/:id`**:**优先 IPC** `MSG_NOTIFY_DISMISS(0x22)`(4B id → `MSG_ACK`;launcher 撤下时自动归档留档);
IPC 不可达/未受理 → 回退从日志移除该行(写临时 + rename)。IPC 未受理且日志也无该 id → 404。
- **`POST /api/notifications/dismiss_all`**:**优先 IPC** `MSG_NOTIFY_DISMISS_ALL(0x26)`(→ `MSG_ACK.status`=清除数;
launcher 保留 ongoing/media/progress 红线项);IPC 不可达 → 回退重写日志(`ongoing||media||progress>=0||category=="media"` 保留)。
- **实时性**:IPC 路径直接反映 launcher **内存活跃集**——新通知(含未到期/未归档)**立即**出现在面板(实测:post 一条
通知后面板同时刻即见,`source:"ipc"`);撤/清经 IPC 联动 launcher 内存(实测:面板 DELETE 后 launcher 活跃集即无该 id)。
TF 路径仅为 launcher 不可达时的诚实降级(会显示自动过期的历史通知,直到 clear)。
- **并发**:IPC 可用时面板不写 TF(撤清由 launcher 自行归档),避免与 launcher `O_APPEND` 竞写;仅 IPC 回退到「写临时 + rename」。
### 5.9 访问级 PIN 鉴权(panel.pin;用户定案 2026-09-01)
- **模型(覆盖 282 报告「默认无鉴权」前提)**:**访问面板即需登录**。未认证请求一律 401(静态登录页自身 + css/js 放行——
`serve_static` 先于 `require_auth`);未设 PIN 前无法建立会话,故**读端点也全拦**(登录/设置流程除外),写端点自然一律拒绝。
- **设置键**:`panel.pin`(str,存 `salt:hex` = sha256(salt||pin) 带盐哈希,绝不存明文),写板载 `settings.conf`(与实体设置页/CLI
同一存储契约,面板独立进程直接镜像文件);**与未来 MeowOS 锁屏 PIN 同源**:设置键 `system.pin` 预留(本后端只读面板键,见 `g_sets` 注记)。
`panel.pin` 的 `GET /api/settings` 恒回 `"value":"" + "set":<bool>`,**绝不回传哈希/明文**。
- **PIN**:4 位数字(`pin_validate`);校验时重算 sha256(salt||pin) 并恒定比较(`memcmp`)。
- **门禁**:`require_auth` 在路由分发前执行——除 `/api/auth/{login,setup,logout,status}` 外全部 `/api/` 需有效会话;
缺/错/过期 → 401 `UNAUTHORIZED`。危险端点(reboot/shutdown/ota、key 注入、settings 写等)一律纳入会话校验(不再有「未开启」旁路)。
- **会话 token**:`/dev/urandom` 16B → **32 hex(128bit)**,**只存面板进程内存**(`g_sess[]`,`AUTH_MAX_SESS`=8,满淘汰最早过期),
**无卡上持久化**——设备重启自然清(会话即失效)。有效期 **3 天**(`AUTH_TOKEN_TTL`,**固定不滑动续期**,逐 token 校验到期戳);
多会话并存。`logout` 失效当前 token。
- **锁定(防爆破)**:连续 `PIN_MAX_FAILS=5` 次错 PIN → 锁 `PIN_LOCK_SEC=300`s(`429 LOCKED`,响应附 `lock_remaining_ms`;
`status` 透出 `locked/lock_remaining_ms`);内存态,重启清。
- **旧密码机制兼容迁移**(`panel.auth_enabled`/`panel.auth_pass` 保留):旧密码存在且未设 PIN 时,视为「已设访问屏障」的同等物——
登录可用旧密码(`{"password":…}`),`status` 透出 `legacy:true` 并提示改设 4 位 PIN;**一旦设置 `panel.pin`,新代码以 PIN 为准**
(登录只认 PIN,旧密码不再作为凭据)。`PUT /api/settings/panel.auth_pass` 仍可写(引导迁移),`panel.auth_enabled` 已不作门禁(保留兼容)。
- **前端**:api 客户端统一带 `X-MeowToken`(token 存 `localStorage`);收到 401 → 自动切登录页;未设 PIN → 首次设置流程
(`#login` overlay 双态:设置=输入+确认,登录=单输入);429 `LOCKED` → 显示锁定提示。设置页「面板鉴权」卡片沿用旧密码项。
- **静态屏蔽**:`serve_static` 额外拒绝后端可执行文件(403),防可执行被 HTTP 读取。
### 5.10 实时画面(`/api/screen.jpeg`,含增量帧 `?delta=1`)+ 触摸注入(`/api/input/tap`)— 阶段 C
- **实时画面(整帧,默认)**:`GET /api/screen.jpeg` 读 `/dev/fb0` **当前显示帧**(`FBIOGET_VSCREENINFO` 取 `yoffset`/`xres`/`yres`/`bpp`,
`FBIOGET_FSCREENINFO` 取 `line_length`,`pread` 活动缓冲,**只读,不影响渲染**;16bpp RGB565)→
`mp_jpeg_encode_rgb565()` 编码为内存 JPEG(自研最小 baseline 编码器,见 §1),返回 `Content-Type: image/jpeg`。
`?q=1-100` 控制质量(默认 70)。恢复 `line_length != xres*2`(行尾填充)时先压缩为紧凑 RGB565。
- **增量帧(`?delta=1`,阶段 C 增强)**:服务端**缓存上一帧紧凑 RGB565**(单线程 accept 循环跨请求持有),按 **8×8 块**逐行
`memcmp` 比对(1 块=8×8 像素×2B),只编码**变化块的外接矩形子图**(8 对齐、钳到屏内)为独立 JPEG,并以**响应头**回传偏移/几何
(`X-Meow-Delta:1`/`X-Meow-Full:0|1`/`X-Meow-W/H`(整屏)/`X-Meow-Sub-X/Y/W/H`(子图原点与尺寸)/`X-Meow-Changed`/`X-Meow-Blocks`)。
- 冷启动/几何变化/变化块覆盖全屏 → 发**整帧**(`X-Meow-Full:1`);帧无变化 → `Content-Length:0` + `X-Meow-Changed:0`(前端跳过叠加)。
- 体积:近静态帧(只刷新状态栏时钟等)增量子图可低至数百 B(实测 6 变化块=**804 B**,对比整帧 ~27 KB @q70)。
- **不破坏默认**:未带 `?delta=1` 仍为整帧 `image/jpeg`(零回归)。
- 前端「屏幕」页新增「增量传输(省流量)」开关:开启后走 `?delta=1`,用 `<canvas>` 叠加(整帧播种/替换,子图 `drawImage(x,y)`);
关闭走原 `<img>`+objectURL 整帧。默认关。
- **触摸注入**:`POST /api/input/tap`,body `{x,y}`(0..479 × 0..719 原生像素坐标,前端按 480×720 映射,后端再 clamp)。
**机制 = write 进 goodix evdev(`/dev/input/event1`)**——**本机实测 write 注入可投递**(见 §7 修正历史结论),
事件序列与真实 goodix 一致:先 `BTN_TOUCH=1`(按下边沿)**后** `ABS_MT_*`/`ABS_X/Y`(坐标),`SYN` → 按住 100ms →
`BTN_TOUCH=0` + `ABS_MT_TRACKING_ID=-1` + `SYN`。**关键**:launcher/`meow_touch` 在按下边沿会**重置坐标 -1**,
只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)——故坐标必须**在 BTN_TOUCH=1 之后**,否则原点永不
锁定 → 抬手被判无效 tap(只高亮不启动)。注入前不写 uinput(本设备厂商内核 write 进 goodix 即可投递,逐 UB 少)。
- **仲裁(本机优先打断)**:注入前 `touch_last_local_ms()` 排空 event1,取「最近一次本机真实触摸」的单调毫秒时间
(剔除本进程自注入事件:事件时间 ≤ 上次注入时刻+200ms),若 `now-last < 5000` → **忽略注入**,返回
`{"dispatched":false,"reason":"local_touch_active","cooldown_ms":N}`;否则注入并返回 `{"dispatched":true,"x","y"}`。
MIPS 的 `struct input_event` 无 `time` 成员(ABI),按 launcher 同款读裸 16 字节取偏移 0/4 的 `tv_sec/tv_usec`(单调时钟)。
- **前端「屏幕」页**(`web/#/screen`,导航「屏幕」):`<img>` 轮询 `GET /api/screen.jpeg?t=…&q=70`(`fetch`→blob→objectURL,
防重叠 + 旧 URL 回收;后端不可达提示「后端未连接」);默认 **1fps**,滑条 **0.2–2fps**(存 `screen.fps`,页面隐藏/切走即停刷,
`visibilitychange` 回前台续刷);点击画面按 480×720 比例换算坐标 → `POST /api/input/tap`,`dispatched:false` 时 toast「本机触摸中」。
### 5.11 字体库 tar.gz 导入 / 导出(`/api/fonts*`,新增)
- **定位**(MEOWOS_SPEC_REF §5.7.6 / FONT_SYSTEM.md,**2026-08-31 用户定案:放弃 zip,与 .mpk 同构走 tar.gz**):
系统字库按 `MeowOS/fonts/<family>/<weight>/` 三件套存放(schema 见 SPEC_REF §5.2:family/weight/name/
glyph_count/charset/tier/size_list/bit_depth/sha256_png/sha256_idx…)。**设备 busybox tar/gzip 必定存在**,
后端全部走 `gzip -dc | tar` 管道(nyai .mpk 同款),**零新增依赖、不引入 zlib/miniz/自研 zip**。
- **导入 `POST /api/fonts/import`**(multipart `file`=<.tar.gz>,上限 **`MAX_FONT_UPLOAD`=32MB**,可带 `?force=1`):
1. multipart `file` 落临时 `<TF_CARD>/tmp/mp_font_...tar.gz`(`multipart_extract_file`,二进制安全 `mem_find`)。
2. **成员预校验**:`gzip -dc '<tmp>' | tar -t`,每成员经 `sanitize_member`(去前导 `/`/`./`,拒 `..` 组件与
shell 危险字符)且**恰为三件套平铺**(多出条目/子目录/绝对/`..` → 400)。
3. **解压到临时目录**:`gzip -dc | tar -x -C stg` → opendir/lstat 锁:**恰为 3 个普通文件**
(拒软链/目录/设备;总大小 ≤ `FONT_EXTRACT_MAX`=32MB 防 gzip 炸弹)→ 缺件/坏 json 400。
4. `font.json` 手写 JSON 解析 → `family`(缺省回退 `name`)/`weight`(缺省 `regular`),`safe_dir_segment`
消毒(拒 `/`、`\`、控制符、`.`/`..`)→ 非法 422。
5. **覆盖冲突**:目标目录任一文件已存在且非 `force=1` → **409 `CONFLICT`**(前端 confirm 后带 `force=1` 重试)。
6. **sha256 仅 WARN**(对齐 font.json 约定):流式重算 `sha256_png`/`sha256_idx` 与声明比对,不符写 `warning`
字段、仍导入成功(**不硬失败**);响应含 `family`/`weight`/`imported`/`hint`(提示进设备「设置→字体」页
列表自动重扫生效——**后端不触发 launcher 重扫**,扫描时机=启动/进设置页,勿加热路径)。
7. `mkdir`(系统调用,**不用 `system()`**,防注入)→ `rename` 三件套到目标目录(原子替换)。
- **导出 `GET /api/fonts/export?family=&weight=`**:校验 family/weight 段 → 目标目录三件套须齐(缺件 404)→
`tar -cf - -C <font_dir> font.png font.idx font.json | gzip -c > <tmp>` 落临时文件(求 `Content-Length`)→
`Content-Disposition: attachment; filename="<family>-<weight>.font.tar.gz"` + `application/gzip` → 流式下发。
- **列表 `GET /api/fonts`**:扫描 `MeowOS/fonts/<family>/<weight>/`,三件套齐才列出,读 `font.json` 取
name/tier/glyph_count(容错)。无卡 → 503。
- **往返验证**(宿主):Python tarfile 生成的平铺三件套 `.tar.gz` 导入→导出→逐字节比对源文件一致;本后端导出
再导入→逐字节一致;GNU tar/`tarfile` 均能读;目录穿越 `../`/绝对路径/软链成员/缺件/坏 font.json/多出条目/
子目录包裹/非 gzip——全部拒绝;打包文件名 `<family>-<weight>.font.tar.gz`。见 §7 宿主测试备注。
### 5.12 音频状态(`/api/audio`,新增,AUDIOD_DESIGN §8 面板对接)
- **数据源**:`meow_audiod` 独占音频守护(板载进固件,svcd 托管;HibyOS 模式按门控不启动)。
后端连 `/usr/data/meow_audio.sock`(可被 `--sock-audio` / env `MEOW_AUDIO_SOCK` 覆盖,宿主/qemu 测试用,
与 audiod CLI 同 env),发 `MSG_AUDIO_QUERY`(0x00C7) 空负载,收 `MSG_AUDIO_STATE`(0x00C8) 文本帧
(`active=.. app=.. sid=.. rate=.. ch=.. fmt=.. written=.. played=.. underrun=.. paused=.. route_mode=.. port=.. vol=..`,
空格分隔 k=v、NUL 结尾)。**audiod 无 MSG_HELLO 握手**(见 meow_audio.c CLI),与 perf/power/launcher 路径不同。
- **响应**:解析 k=v 为 JSON——`{available:true, active, app, sid, rate, ch, fmt, written_frames, played_frames,
underrun, paused, route_mode("auto"/"manual"), port, vol, **rms, peak, lvlseq**}`;`port` 为 amixer numid=9 路由口
(0=空闲 1=lineout 2=3.5 3=4.4 4=Type-C SPDIF;宿主无 amixer 时为 -1)。
- **电平(追加键,ABI/协议兼容)**:`rms`/`peak` = deci-dB(`-990`=‑inf;如 `-139`=‑13.9dBFS),`lvlseq` = 快照代际
(每次结算/开流复位递增,前端可用其判「新刻度」驱动 sparkline)。仅追加键,不破坏既有字段。
- **audiod 不在**(socket 缺失/连接拒/超时/非预期应答)→ HTTP 200 `{"available":false}`,**不报错**(面板音频页显示
「audiod 不可用」)。只读端点;不放测试音(真机乱响风险,tone 留给 CLI `meow_audio tone`)。
- **前端**:「音频」页(`web/#/audio`)5s 轮询展示——当前流(app/rate/ch/fmt/已写帧/已播放帧/underrun/暂停态)+
路由口 + 音量 + audiod 可用性;切页即停轮询。
### 5.13 字体管理页(前端 `web/#/fonts`,新增)
- 后端 `/api/fonts*` 三端点(§5.11,agent-243 落地)的专属页面:表格列出 family/weight/name/tier/glyph_count,
行内「导出」(busy 防连点「打包中…」)+ 顶部「上传字体库 .tar.gz」(导入中禁用按钮=进度态)+「刷新」。
- 上传遇 **409 CONFLICT**(同 family/weight 已存在)→ 弹确认框,确认后带 `?force=1` 重试;成功 toast 带
`family/weight` 与 `warning`(sha256 不符仅提示)/`hint`。
- 设置页原「字体库」卡保留(同一后端,互不影响)。
### 5.14 批1-A 新增只读端点(供批1-B 前端消费;JSON 形状)
> 本批只做**后端端点 + 最小前端支撑(登录)**,首页/电源/进程页归批1-B。前端调用统一带 PIN 会话 token(见 §5.9)。
- **`GET /api/overview`**(首页生命线,单线程后端只暴露聚合):
```json
{ "ts": 1788268891,
"perf": { "...": "meow_svc_perf 聚合 JSON(cpu/memory/storage/tfcard/temp/system/network,透传)" },
"power": { "mode":"meow","battery":{"state","pct","mv","health","technology","temp_c10","charger_temp_c10","cycle_count","nominal_mah_note"},
"estimate":{"est_mw","confidence":"estimated|derived|measured|none","calibrated","degraded"},
"breakdown":{"baseline_mw","cpu_mw","cpu_util_pct","backlight_mw","panel_mw","brightness_pct","wifi_mw","wifi_state","bt_mw","bt_state","audio_mw","audio_state","degraded"},
"policy": null },
"audio": { "available": true, "active","app","sid","rate","ch","fmt","written_frames","played_frames","underrun","paused",
"route_mode","port","vol","rms","peak","lvlseq" },
"net": { "...": "netmgrd 概览 JSON(mode/ifaces[]/wifi/bt/rfkill/hostname_local,透传)" },
"ams": { "available": true, "count": 3, "apps":[ {"pkg","name","state":"fg|bg|stop","pid","launches","crashes","last_kind":"clean|exit|sig","last_code"} ] },
"svcd": { "available": true, "count": 3, "services":[ {"name","state":"running|stopped|stopping|disabled|failed","pid","restart","restart_str","backoff_ms","trigger","src":"board|tf","exit","exit_time","exec"} ] }
}
```
- **任一源失败 → 该子块 `{"available":false}`**(`perf`/`power`/`net` 失败也是 `{"available":false}`,绝不假数据);
`audio`/`ams`/`svcd` 成功时自带 `available:true`。`perf`/`power`/`net` 成功时为对应源**原始 JSON**(无 `available` 字段)。
- **`GET /api/power`**:即 overview 的 `power` 子块(可 503 `POWER_UNAVAILABLE`,非 `available:false`)。
- **`GET /api/ams`** / **`GET /api/svcd`**:即 overview 的 `ams`/`svcd` 子块;源不在 → `{"available":false}`。
- **`GET /api/audio`**:overview 的 `audio` 子块 + rms/peak/lvlseq 消费说明见 §5.12。
### 5.15 批2:崩溃/日志中心 + 音频写 + 后端补缺口(PANEL_ENHANCE_PLAN 批2)
- **崩溃/日志中心(升级 `#/logs`)**:
- **`GET /api/logs/crash`**:读最新 `crash_<bootts>.log`(meow_crash 落盘格式:`<epoch> <uptime> <svc|dmesg> <detail>`;
svc 的 `detail=<name> exit=<code>`/`<name> signal=<sig>`)。聚合出 `{file,count,dmesg_count,dmesg_tail,entities:[
{name,source:"svc",count,last_epoch,last_kind,last_code,last_time,last_detail}]}`;只读、无副作用。
读取有界 `CRASH_TAIL_MAX`=256KB、实体 `CRASH_ENT_MAX`=64、dmesg 保留最近 `CRASH_DMESG_TAIL`=5 行(行数上限保护)。
- **`/api/logs` 文件列表**每项带 `group`(`meow_*→system`/`crash_*→crash`/`ams_*→ams`/`audio_monitor_*→audio_monitor`/
`net_*→net`/`power_*→power`/其余→`app`),供前端按来源分组 + 前缀过滤;`group` 为追加键,向后兼容。
- **崩溃尾巴前置**:崩溃汇总实体行「stdout 尾部」按钮复用批1-B 读取路径(`/api/files?path=/MeowOS/logs/<name>/`
找最新 `stdout_*.log` → `/api/logs/read?file=<name>/<file>&lines=200` 读尾部)。
- **音频写(写级,PIN 已护;前端切换/调高前确认弹窗)**:
- **`POST /api/audio/route`**:`body {port}`。**仅 0=auto/2=3.5/3=4.4**,其余(含 1=lineout/4=SPDIF、5=audiod 内部
强制重烧 FPGA 值)→ 422。audiod 对非法 port 也回 MEOW_OK(内部仅记录不应用),故面板在**发出前**自行校验。
应答 `{accepted,port}`;audiod 不在 → 503 `AUDIO_UNAVAILABLE`。
- **`POST /api/audio/volume`**:`body {value}`。**clamp 到系统 vol_max**(`settings.audio.volume_max`,缺省
`PANEL_VOL_MAX_DEFAULT`=100;设备当前 50)。audiod 内部仅 clamp 0-100、无 vol_max 防护——面板是唯一把守方
(防炸耳红线:平方锥度 `dac=(100-pct)²×192/10000`,接近 100 极响)。应答 `{accepted,value(clamped),applied,vol_max}`。
- **`/api/audio` 补 `vol_max`**(读 settings `audio.volume_max`),前端滑条上限同步 + 「平方锥度非线性」标注 +
「当前值只读显示锥度后百分比」。
- **后端补缺口**:
- **`/api/system` 补 `reboot_reason`/`upgrade_status`**:svc_power 读级查询 `POWER_REBOOT_REASON(8)`/
`POWER_UPGRADE_STATUS(9)`(`st.msg` 分别为 `coldboot=reboot|shutdown|unknown`、`upgrade=yes|no`);查询失败回空串不谎报。
前端批1-B 已留位(读 `d.reboot_reason`/`d.upgrade_status`),此处点亮。
- **`/api/overview` net 块并入 DNS 统计**:netmgrd 状态 JSON 剥外层括号拼入 `{"net":{…,"dns":{…}}}`(复用 `/api/net/dns`
同款统计);DNS 不可用则省略 `dns` 键仅保留网络状态,**绝不假数据**。首页 netCard 改读 `net.dns`,去掉单独 `/api/net/dns`
(首页严格单请求)。
---
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)
1. **`ui.style` 值域**:`meow_settings.c g_defs` 源码为 `"md3,miui"`(label "Material You,MIUI"),但
SYSTEM_SETTINGS.md 定案 + MEOWPANEL_PROTOCOL §5 一致行为 **`miuix`**(对应 `MEOW_STYLE_MIUIX`),前端 `data-style` 也用 `miuix`。
→ 本表对齐为 `"md3,miuix"` / "Material You,MIUIx"。**建议**同步改回 `meow_settings.c g_defs`(否则实体设置页与面板值域漂移)。
2. **设置项映射**:协议 §3.2 称后端经 `meow_settings_*` 读写;因后端为独立进程,无法链接 launcher 内 API,
**改为直接镜像同一份 `settings.conf`**(同款 parse/save 契约)。改哪边都会落同一文件,实体/网页/CLI 仍同步(listener/落盘)。
3. **`display.brightness` 元数据**:`g_defs` 为 `min=0 max=255 def=101`;但语义 clamp 到 `[brightness_min(1),100]`
(防 101 闪屏)。后端给前端 `min=1 max=100`(协议 §3.2 期望),读值经 clamp 后返回(默认→100)。若回改协议可注明「原始表 0-255,面板展示语义 1-100」。
4. **`/api/system` 的 `network`/`ota`**:**M2.73** 起 perf 服务已聚合 `network` 段(真实 WiFi `wpa_cli`/`ip` + 蓝牙 `hci0`/sysfs + `hostname_local`,SYSTEM 子集),后端直接透传;只补 `boot.default`(读 `/data/meow_boot_default`)与 `ota{current=version,available:false,url:null}`。**网络栏以「蓝牙」为准**(设备无网口,无 `以太网` 一行,`bluetooth.available=false` 显示「无蓝牙」)。若回改协议可注明「network 已并入 perf 服务,ota 为面板占位」。
5. **`/api/files` 上传/删除/重命名等 path**:前端对这些操作传**绝对路径**(`d.path/子项`),列表传**相对 root**;
后端 `normalize_path` 对两者均容错(相对则拼 root,绝对则直用),符合协议「白名单根 + 规范化」。
6. **`GET /api/files` 返回值**:`files_list` 返回 `500` 语义时以 500 封装(协议未定义 `root` 之外错误枚举,用通用错误信封)。
7. **通知清单数据源**:协议 §3.8 称后端「经通知后端读取」。launcher 现已有**客户端查询/撤/清 IPC**(`MSG_NOTIFY_QUERY(0x25)`/
`MSG_NOTIFY_DISMISS(0x22)`/`MSG_NOTIFY_DISMISS_ALL(0x26)`,agent-199 落地),后端改为**优先经这些 IPC** 取 launcher **内存活跃集**
(实时,`source:"ipc"`);IPC 不可达/超时回退读 TF 卡通知转储日志(`source:"tf"`,会话级归档)。响应额外带 `source` 与 `items` 别名
(与 `notifications` 同内容);`items` 为任务约定空态形状,`notifications` 为前端实际读取字段,二者并存。撤/清同样**优先走 IPC**联动
launcher 内存;`clear` 语义为「保留活动中的媒体/进度通知」(`ongoing||media||progress>=0`),对应通知中心既定语义。
8. **`/api/files/read` 文本截断**:协议 §3.4 称「仅文本(<1MB)→ 二进制 415」;现实现为「预览上限 1MB,超限 `truncated:true`,
不再整体 404」,二进制仍 415(比协议更友好,返回文件真实 `size` + `truncated`)。
9. **`/api/files/upload`/`rename` 修正**:上传文件名经 `safe_filename` 取 basename、拒绝 `..`/分隔符(防 `../../etc/passwd` 逃逸),
落盘前对 dir 与拼接后路径双重 `normalize_path`+白名单校验;rename 的新路径 = **原路径父目录** + `new_name`(原实现误拼到文件尾部,
恒 ENOTDIR 失败)。上传体上限 `MAX_UPLOAD_BODY`=8MB,超限 → 413 `PAYLOAD_TOO_LARGE`。
10. **TF 卡根可覆盖(宿主测试)**:`env MEOW_TF_ROOT` 覆盖卡根(设备上不设,恒用 `/usr/data/mnt/sd_0`);`env MEOW_NOTIF_LOG`
覆盖通知转储日志路径。设 `MEOW_TF_ROOT` 时 `tf_present()` 恒真(测试用)。
11. **实时画面/触摸注入(阶段 C)**:协议 §6 待定案项称「实时画面(`/api/frame` 等,本文件暂未定义)」。本面板落地为
**`GET /api/screen.jpeg` + `POST /api/input/tap`**(命名自定:`.jpeg` 表达二进制帧 + `input/tap` 表达触摸注入),
见 §5.10。回改协议时可补这两端点(以及 `frames` 语义、`/api/input/tap` 的 `dispatched:false/reason` 软失败约定)。
12. **multipart 二进制查找修正(strstr→memmem,本次修复,属 bug 修复非出入变更)**:`parse_multipart`/`multipart_extract_file`
均用 `strstr(data_start, boundary)` 定位 part 数据边界——**strstr 遇到数据中的 NUL 字节即提前停止**(含 NUL 的二进制
.tar.gz/.mpk/图会误把后续 boundary 与拖尾算进数据 → 解压出错)。已改**二进制安全**的 `mem_find`(memmem 语义,手写字节扫描,
不依赖 glibc/musl 扩展)。这修复了字体库 tar.gz 导入,也顺带修复了二进制 `.mpk`/文件上传的潜在同病。**实现优先**,供回改协议/审计。
13. **`/api/fonts*`(新增,§5.11)**:协议 §2 端点总览无此组;按 SPEC_REF §5.7.6「MeowPanel 预留 .zip 导入/导出」落地——**但用户
2026-08-31 定案:改 tar.gz、放弃 zip**(与 .mpk 同构)。落地为 `/api/fonts`(列表)/`/api/fonts/import`(导入,POST multipart,
上限 32MB,`?force=1` 覆盖)/`/api/fonts/export`(导出,GET,`<family>-<weight>.font.tar.gz`)。前两者由前端设置页「字体库」卡接入。
还修正了 `/api/files` 排序:**目录优先 + 名称自然排序**(数字感知 track2<track10,中文按无符号字节序,见 §5.4)。
**导入不触发 launcher 重扫**(扫描时机=启动/进设置页),响应用 `hint` 提示。
14. **`/api/audio`(新增,§5.12)**:协议 §2 无此端点。按 AUDIOD_DESIGN §8「MSG_AUDIO_STATE 供 MeowPanel 系统/音频页展示」落地——
连 audiod sock 发 `MSG_AUDIO_QUERY`(0x00C7)、收 `MSG_AUDIO_STATE`(0x00C8) k=v 文本解析为 JSON 透传;**audiod 无握手**、
不在时回 `{"available":false}`(200,非错误)。前端新增「音频」页(5s 轮询只读)。同批新增前端「字体」页(§5.13,
`/api/fonts*` 的专属页;设置页字体库卡保留)。
---
## 7. 已知边界 / 风险
- **单线程串行**:面板访问量极小,但并发请求会被排队(长上传/下载期间其它请求等待)。对面板用途可接受。
- **字体导入内存/体积上限**:`POST /api/fonts/import` 上传体上限 **32MB**(`MAX_FONT_UPLOAD`,仅此端点放宽;其余
multipart 仍 8MB);tar 解压到临时目录的总大小 ≤ `FONT_EXTRACT_MAX`=32MB(防 gzip 炸弹)。**全程不做整读进内存**
(写临时文件 + `gzip -dc | tar` 管道 + rename),峰值内存很小,不依赖 dma/font.png 大件缓冲——相比内存解压方案的 14MB 约束更稳健。
⚠️ 依赖设备 busybox `gzip`/`tar`(必带);若某内核裁剪过二者,导入/导出会失败(400/500),列表不受影响。
- **无鉴权(默认)**:设备内网信任模型(协议 §1.3)。**高危端点**(`reboot/shutdown/ota/apps/删除/上传)在默认关鉴权时无鉴权,
请勿把 8080 暴露到不可信网络。`meow_svc_power` 服务端做 `SO_PEERCRED uid==0` 基础校验(root 才受理动作),后端以 root 运行可过;launcher 电源菜单/后端同链路。
**可选鉴权**(§5.9)开启后可对内网作加固(登录密码 + 会话),但密码经**局域网明文 HTTP** 传输,不提供 TLS。
- **IPC 服务未起**:`meow_svc_perf`/`meow_svc_power` 为板载 `trigger=boot` 服务,正常恒在;若异常退出,读侧返回 500 `INTERNAL`,
动作侧返回 500/503,绝不静默。请勿拔除这两服务。
- **升级保护**:`POWER_UPGRADE` 由 `meow_svc_power` 在写 mtd5 前校验卡根 `r3proii.upt`(存在/可读/非空/size>0),失败不写 mtd5;期望 md5/hash 校验留二期。
- **文件/日志/应用均依赖 TF 卡**:无卡统一 503 `NO_CARD`(不阻塞系统核心)。`settings`/`system`/`perf`/`key` 无卡也可用(板载/rootfs)。
- **`/api/files/upload` 上传路径**:multipart 需要 `path` 字段(目标目录);缺失时上传文件会被忽略(前端总是带着 `path`)。
- **数据一致性**:设置直接写 `settings.conf`;`display.brightness`/`boot.default` 的 clamp/同步与 launcher `meow_settings_*` 同口径。
若 launcher 未来改 clamp 逻辑,需同步本表。
- **通知 IPC 路径为 launcher 内存活跃集(实时)**:优先 `MSG_NOTIFY_QUERY` 取活跃集;TF 归档日志仅在 launcher 不可达时作为
回退(此时才出现「会话级归档」语义——非 activity 通知 10s TTL 自动归档后仍留日志,会显示已过期历史直到 clear;媒体/进度/ongoing 常驻)。
IPC 响应为 launcher 的 `m_notify_query_result_t` 单帧文本(≤4KB),活跃通知超限时 launcher 侧 `meow_notif_serialize_json` 保前缀截断
(理论上最多 ~30 条,实际通知数远小,可接受)。
- **通知撤/清联动 launcher 内存**:面板 delete/dismiss_all **优先走 IPC**(launcher 撤下时自动归档留档);仅 IPC 不可达时回退重写 TF 日志。
- **增量帧依赖单线程基线**:`/api/screen.jpeg?delta=1` 用上一帧缓存比对,**面板为单线程 accept 循环**(跨请求持久);对**单面板用户**直接有效。
多客户端混用(一全帧+一增量、跨浏览器)时增量基线可能被另一客户端发的全帧刷新,前端「canvas 未播种则整帧取图」兜底自愈;极端并发下增量图
可能基于过期基线(可容忍,仅多发一帧整帧即校准)。子图为独立 JPEG(4:2:0 在子图内自采样),与整帧同源同质量,无拼接色差。
- **上传为「限长缓冲」非全流式**:multipart 请求体整体读入(上限 8MB,超限 413),下载为流式(8KB 分块)。严格「socket 逐字节流式上传
解析」未实现(需增量 multipart 解析器),列为后续;14MB 内存约束下 8MB 上限 + 流式下载已覆盖大文件读侧。
- **存储为 `meowpanel` 编译时刻写死**的 socket/路径会因 `--sock-perf/--sock-power`(qemu sim)可覆盖,但默认走 `/usr/data/meow_*.sock`。
- **触摸注入在设备息屏时不生效**:launcher 主界面在 `meow_screen_on==0` 时**不处理触摸**(main_menu.c,息屏防误触);设备自动息屏
(30s 空闲)后注入的 tap 会被 launcher 忽略——面板无法用触摸唤醒(需先电源键点亮)。前端「屏幕」页已提示「设备息屏请先点亮」;
对「正在看」设备的正常使用场景(屏幕亮着)注入即有效。
- **实时画面读取 fb0 当前帧(只读)**:双层缓冲下 `pread` 活动缓冲,存在翻页瞬间撕裂一行的可能(1fps 预览可接受);
`/dev/fb0` 不可读/非 16bpp → 500 `INTERNAL`(绝不静默)。
- **JPEG 编码器为 baseline 4:2:0**:质量 70 下 480×720 ≈ 24KB;质量越高体积越大(`q=100` 约 60-80KB)。编码为单线程
串行(面板本就单线程),一帧编码约几十 ms(MIPS 单核),1fps 轮询无压力。
- **触摸注入为即时性注入**:不维护长按/多指手势状态机(与按键注入一致);`dispatched:false`(本机触摸仲裁)属软失败,
前端可按 `reason` 提示,不视为错误。
---
## 8. 文件清单(本目录)
```
patch_v2/meowpanel/
├── meowpanel.c # 后端源码(约 5590 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序/audiod 状态快照 + 批2 崩溃/日志中心 + 音频写 + overview DNS 聚合)
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning)
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;meowpanel.c + mp_jpeg.c)
├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 015f9cf1…)
├── meowpanel_host # 宿主编译(本机 ld,调试用)
├── services.d/
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
├── web/ # 前端(已存在;部署时拷到后端同目录)
│ ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航
│ ├── meowpanel.css # 含登录页样式
│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片、音频页(5s轮询)/字体页
└── README.md # 本文件
```