# 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,仅供调试) ``` - **后端源码已按模块拆分**(`src/*.c` + 共享内部头 `src/mp.h`):`mp_server`(HTTP/路由/main) + `mp_util`/`mp_json` + `mp_store`(设置/主题) + `mp_ipc`(IPC 帧/单一收发) + 各 api_* 域 `mp_perf`/`mp_power`/`mp_audio`/`mp_net`/`mp_ams`/`mp_svcd`/`mp_files`/`mp_fonts`/`mp_apps`/`mp_logs`/`mp_notif`/`mp_auth`/`mp_key`/`mp_screen`。 `mp.h` 承载全部共享类型/常量/外部全局 + 跨模块函数原型;每个 `.c` 以 `#include "mp.h"` 起步。 拆分保持后端接口/行为**完全不变**(宿主 `-Wall -Wextra` 0 warning;重编产物与拆分前**逐字节一致**——同一 mock 环境全端点 diff 无差异)。 原单体 `meowpanel.c` 保留在根目录作参考,**不在构建源列表**(构建以 `src/*.c` 为准)。 - 两分支均以 `-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/`(映射到同目录文件),前端当前用根相对路径。 - 服务启动后整卡由 `meow_svcd` 按 `trigger=boot` 拉起;`meow_sysui` 监管(restart/backoff/max_restart)。 - **无卡**:`MeowOS/services.d/` 不可读 → svcd 跳过本服务;系统核心照常。 ## 3. 访问 浏览器(同局域网): ``` http://.local:8080/ ``` - mDNS 由板载 `meow_mdns` 应答 `.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: `(或 `Authorization: Bearer ` / `Cookie: meow_token=`); 缺/错/过期 → 401 `UNAUTHORIZED`。**HTTP 明文(无 TLS),内网使用;请勿把 8080 暴露公网**。 --- ## 4. 端点实现清单(对照 MEOWPANEL_PROTOCOL §2 端点总览) ### 已实现(返回 200/2xx 或语义错误码) | 方法 | 路径 | 说明 | 实现 | |---|---|---|---| | GET | `/`、`/meowpanel.css`、`/meowpanel.js`、`/assets/` | 静态托管 | ✅ | | 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///` 三件套齐) | ✅(无卡→503) | | POST | `/api/fonts/import` | 上传字体库 `.tar.gz`(multipart `file`,可带 `?force=1` 覆盖) | ✅(缺件/坏json/穿越→400,冲突→409,超限→413) | | GET | `/api/fonts/export` | 导出 `//` 三件套为 `.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/ams/bg` | **后台活动列表**(M6.38,批④):`am list` 仅取 `state=="bg"`,逐进程 `/proc` 采样 `VmRSS`/`cpu_pct`(首采样/不可得→`null` 前端显 `—`,绝不编造);同源于 amsd FG/BG 元数据,杀/清走既有 `/api/ams/:pkg/kill` | ✅(只读,需 PIN 会话;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+events/audio/net+union dns/ams+fg/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) | | GET | `/api/net/ifaces` | 接口列表详情(kind/ip/gw/rx/tx/rssi/rfkill/dns;netmgrd 0xE2+0xE0 合成) | ✅(只读,需 PIN 会话) | | GET | `/api/net/policy` | 管控策略只读(0xEA SHOW;`enforce=dns_only` 如实)+ **`policy_write_enabled`**(guard③ 门禁状态)| ✅(只读,需 PIN 会话;上游坏字节→`available:false`) | | POST | `/api/net/policy` | 写管控策略(`set_net`/`set_domain`/`del`):**guard① 防自锁**(`SELF_LOCK` 422)+ **guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;自锁→422,门禁→403,IPC 权限→403 FORBIDDEN) | | GET | `/api/power/policy` | 电源策略快照(profile 四档/阈值/`enacted`)+ **`policy_write_enabled`**(guard③;注入后 jv_parse 自校验) | ✅(只读,需 PIN 会话;坏字节→503) | | POST | `/api/power/policy` | 写电源策略(**只写 settings.conf `power.*` 键**):**guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;非法值→422,权限→403) | | GET | `/api/power/events` | 电源异常事件(充放电/低电/亮灭屏/背光跳变;overview 摘要 top2 的来源) | ✅(只读,需 PIN 会话;powermgr 不在→503) | | GET | `/api/power/history` | 功耗采样历史(30s 环形 4h,不落盘) | ✅(只读,需 PIN 会话;powermgr 不在→503) | | GET | `/api/ams/:pkg` | 单 App incident 历史(`am history`,崩溃根因 detail;坏 detail→降级 `available:false`) | ✅(只读,需 PIN 会话) | | POST | `/api/ams/:pkg/start` | 启动 App(`am start `;**写级**,PIN 会话) | ✅(包名非法→400,launcher 离线→503) | | POST | `/api/ams/:pkg/kill` | 结束 App(`am kill `;**写级**,PIN 会话,前台前端二次确认) | ✅(包名非法→400,not registered→404,busy→409) | | 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_.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":}`;失败 `{"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 `;用 `popen` 捕获输出+退出码, 非 0 → 422。`export`:`nyai export ` → 以 `Content-Disposition: attachment` 流式下发。`remove`:`nyai remove [--purge]`。 - `GET /api/apps` / `:pkg` 详情:直接扫描 `MeowAPPs//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":`,**绝不回传哈希/明文**。 - **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`,用 `` 叠加(整帧播种/替换,子图 `drawImage(x,y)`); 关闭走原 ``+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`,导航「屏幕」):`` 轮询 `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///` 三件套存放(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` 落临时 `/tmp/mp_font_...tar.gz`(`multipart_extract_file`,二进制安全 `mem_find`)。 2. **成员预校验**:`gzip -dc '' | 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.png font.idx font.json | gzip -c > ` 落临时文件(求 `Content-Length`)→ `Content-Disposition: attachment; filename="-.font.tar.gz"` + `application/gzip` → 流式下发。 - **列表 `GET /api/fonts`**:扫描 `MeowOS/fonts///`,三件套齐才列出,读 `font.json` 取 name/tier/glyph_count(容错)。无卡 → 503。 - **往返验证**(宿主):Python tarfile 生成的平铺三件套 `.tar.gz` 导入→导出→逐字节比对源文件一致;本后端导出 再导入→逐字节一致;GNU tar/`tarfile` 均能读;目录穿越 `../`/绝对路径/软链成员/缺件/坏 font.json/多出条目/ 子目录包裹/非 gzip——全部拒绝;打包文件名 `-.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_.log`(meow_crash 落盘格式:` `; svc 的 `detail= exit=`/` signal=`)。聚合出 `{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//` 找最新 `stdout_*.log` → `/api/logs/read?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` (首页严格单请求)。 ### 5.16 批 P1 写端点(power/net/ams 写)+ 三条护栏(MEOWPANEL_ENHANCE_PLAN P1) > 三个写端点全部落在 `POST` 分支 + `require_auth` 之后(无一漏到无鉴权路径);响应**如实回错误码**,绝不掩盖。 - **三条护栏**: 1. **guard① 防自锁**(仅 `/api/net/policy`):对系统关键网络/服务组件禁设「阻断」(`set_net`+`arg=block`、 或任何 `set_domain` 域名限制)。命中即 `422 {"code":"SELF_LOCK"}`。自锁实体(取 `svc:|app:|proc:` 后裸名), `net_locked_entity()` 精确比对 `dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init`(netmgrd 加载时也剔除,面板侧抢先硬拒)。 「放行 / 删除规则」不阻断(恢复连通)仍放行。 2. **guard② PIN 会话鉴权**:复用全局 `require_auth`(§5.9);无会话 → `401 UNAUTHORIZED`。 3. **guard③ 策略写默认关**:`panel.policy_enabled`(新设置项,`settings.conf`「面板」组,默认 `0`)。 `0`(缺省)时 `POST /api/net/policy`、`POST /api/power/policy` 在解析 body **前**即拒 `403 POLICY_DISABLED` (只读 GET 不受此限);置 `1`(设置页 / 电源·网络策略卡的「开启策略写」)放行。与 powermgr `enacted` **独立**。 - **`POST /api/net/policy`**:`{op:"set_net|set_domain|del", entity:"app:…|svc:…|proc:…", arg:"…"}`(`del` 免 `arg`)。 成功 `{"accepted":true,"message":…}`;权限→`403 FORBIDDEN`、非法→`422 UNSUPPORTED`、自锁→`422 SELF_LOCK`、门禁→`403 POLICY_DISABLED`、 IPC 不可达→`503 NET_UNAVAILABLE`。`enforce=dns_only` 如实显示「仅 DNS 层拒绝」,不谎称断连。 - **`GET /api/net/policy`**:`net_policy_parse` 输出 `{"available":true,"policy_write_enabled":,"enforce","log_level","rules":[…]}`; 坏字节(实体名/域名)降级 `{"available":false}`。前端据此渲染「默认关」横幅 + 「开启策略写」(`enablePolicyWrite`)。 - **`POST /api/power/policy`**:`{item:"profile|low-pct|crit-pct|bri-cap|scr-cap", value:"…"}`,**只写 settings.conf `power.*` 键**; 成功后透传 powermgr 应答。非法值→`422`(服务端 `ok:false` 按 `why` 映射)、权限→`403`、门禁→`403 POLICY_DISABLED`、 IPC 不可达→`503 POWER_UNAVAILABLE`。 - **`GET /api/power/policy`**:透传 powermgr 策略快照(含 `enacted`)后经 `power_policy_append_flag()` **追加顶层 `policy_write_enabled`** (剥尾拼键 + jv_parse 自校验,绝不发非法 JSON);非对象答原样透传。 - **`POST /api/ams/:pkg/{start,kill}`**:`am start/kill `;包名经 `ams_pkg_ok`(仅 ASCII 可见、禁 `/ : 空白`)。成功 `{"accepted":true,"op","pkg","message"}`;`not registered`→404、`busy`/`not running`→409、`launcher offline`→503、其余→500。 **kill 前台 App 前端二次确认**。 --- ## 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,`-.font.tar.gz`)。前两者由前端设置页「字体库」卡接入。 还修正了 `/api/files` 排序:**目录优先 + 名称自然排序**(数字感知 track20),失败不写 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 # 拆分前单体源码(约 6372 行,保留作参考,**不在构建源列表**;构建以 src/*.c 为准) ├── src/ # 后端模块化源码(模块化拆分,接口/行为不变,-Wall -Wextra 0 warning) │ ├── mp.h # 共享内部头:全部共享类型/常量/外部全局 + 跨模块函数原型(含鉴权/实时画面等) │ ├── mp_server.c # HTTP 服务器/路由分发 + 静态托管 + main(单线程 + poll 串行) │ ├── mp_util.c # sbuf/UTF-8 消毒/内存搜索/命令管道/路径与 token 工具 │ ├── mp_json.c # 最简 JSON 解析/序列化(jv_t 树) │ ├── mp_store.c # 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme* + g_sets 表 │ ├── mp_ipc.c # IPC 帧读写 + meow_svc_perf/power/netmgr 客户端(单点收发)+ socket 路径 │ ├── mp_perf.c # /api/perf,/api/system,action,pin-unlock(perf 聚合) │ ├── mp_power.c # meow_powermgr IPC(/api/power*,policy,window,events)+ ipc_pm_send │ ├── mp_audio.c # meow_audiod IPC(状态快照 + 写命令 route/volume) │ ├── mp_net.c # meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3) │ ├── mp_ams.c # App 运行表 + amd 历史 + start/kill 控制 │ ├── mp_svcd.c # svcd 服务表 + 首页聚合 /api/overview │ ├── mp_files.c # 路径安全/白名单 + /api/files* + multipart 上传 + 卡根路径 │ ├── mp_fonts.c # 字体库 tar.gz 导入/导出 + 列表(busybox gzip|tar 管道) │ ├── mp_apps.c # MeowAPPs 扫描 + nyai 安装/导出/移除 │ ├── mp_logs.c # /api/logs 列表 + 读 + 崩溃汇总 │ ├── mp_notif.c # 通知(优先 launcher IPC 活跃集,回退 TF 归档) │ ├── mp_auth.c # 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话 │ ├── mp_key.c # 按键注入(写 /dev/input/event* EV_KEY,物理键红线内) │ └── mp_screen.c # 实时画面(fb0→JPEG,增量帧)+ 触摸注入 + delta 缓存 ├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm) ├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG) ├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;src/*.c + mp_jpeg.c + -I.) ├── meowpanel # 交叉产物(mipsel 静态,本次 md5 fead4852…) ├── meowpanel_host # 宿主编译(本机 ld,调试用) ├── services.d/ │ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf └── web/ # 前端(模块化拆分;部署时拷到后端同目录) ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航;脚本按序加载 ├── meowpanel.css # 含登录页样式 + 「屏幕·遥控」合并页样式(screen-remote/remote-btn) ├── mp_shared.js # 共享框架:全局状态 + API 客户端(token/401) + 公共工具 + 主题/弹窗/登录/滑条(window.MP) ├── mp_pages.js # 各页渲染/绑定:home/perf/power/audio/net/files/apps/fonts/settings/system/logs/notif/proc + 合并的屏幕·遥控 └── meowpanel.js # 前端入口:ROUTES/路由/启动(读取 window.MP 装配) ``` **前端拆分说明**:`meowpanel.js`(单体 IIFE)按「公共 util+api(client) / 各页渲染 / 应用入口」拆为 `mp_shared.js` + `mp_pages.js` + `meowpanel.js` 三文件(经典脚本按序加载,函数体保持原样,仅顶部从 `window.MP` 解构共享框架,**行为不变**)。 「屏幕」+「控制」两页**合并为一页**(route `screen`,导航「屏幕·遥控」):内容区左侧=屏幕实时画面 (复用 /api/screen.jpeg + 触摸注入 + fps 滑条 + 增量省流开关),右侧=MD3 矢量图标遥控板(音量± / 亮度± / 指定亮度滑条 / 播放·暂停 / 下一曲 / 上一曲 / 电源单击 / 电源长按),**全矢量内联 SVG,无 emoji**。 `返回`/`主页` 属物理键红线(后端 /api/key 禁注入 158/172),未提供注入按钮(遥控板附说明)。