From ae51fe9f70b1c9c183973f48a3b189c2dbed3dee Mon Sep 17 00:00:00 2001 From: MeowOS Date: Mon, 31 Aug 2026 01:29:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(meowpanel):=20MeowPanel=20=E5=90=8E?= =?UTF-8?q?=E7=AB=AF(HTTP/JSON=E5=8D=95=E8=BF=9B=E7=A8=8B,=20=E5=86=85?= =?UTF-8?q?=E5=B5=8C=E9=9D=99=E6=80=81=E6=89=98=E7=AE=A1,=20=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5meow=5Fsvc=5Fperf/power=20IPC,=20=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E9=95=9C=E5=83=8F,=20=E6=96=87=E4=BB=B6/=E5=BA=94=E7=94=A8/?= =?UTF-8?q?=E6=8C=89=E9=94=AE=E6=B3=A8=E5=85=A5)=20+=20=E5=89=8D=E7=AB=AF(?= =?UTF-8?q?=E8=93=9D=E7=89=99=E6=A0=8F)=20+=20=E6=9C=8D=E5=8A=A1=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20+=20README=20(agent-164/165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patch_v2/meowpanel/README.md | 246 ++ patch_v2/meowpanel/build_meowpanel.sh | 29 + patch_v2/meowpanel/meowpanel | Bin 0 -> 166956 bytes patch_v2/meowpanel/meowpanel.c | 2815 ++++++++++++++++++ patch_v2/meowpanel/services.d/meowpanel.conf | 18 + patch_v2/meowpanel/web/index.html | 107 + patch_v2/meowpanel/web/meowpanel.css | 311 ++ patch_v2/meowpanel/web/meowpanel.js | 751 +++++ 8 files changed, 4277 insertions(+) create mode 100644 patch_v2/meowpanel/README.md create mode 100755 patch_v2/meowpanel/build_meowpanel.sh create mode 100755 patch_v2/meowpanel/meowpanel create mode 100644 patch_v2/meowpanel/meowpanel.c create mode 100644 patch_v2/meowpanel/services.d/meowpanel.conf create mode 100644 patch_v2/meowpanel/web/index.html create mode 100644 patch_v2/meowpanel/web/meowpanel.css create mode 100644 patch_v2/meowpanel/web/meowpanel.js diff --git a/patch_v2/meowpanel/README.md b/patch_v2/meowpanel/README.md new file mode 100644 index 0000000..a32bc74 --- /dev/null +++ b/patch_v2/meowpanel/README.md @@ -0,0 +1,246 @@ +# 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**;动作侧(`/api/system/{reboot,shutdown,ota}`)经 + `meow_svc_power` IPC(`MSG_POWER_ACTION→STATUS`),不再直连 `poweroff`/写 mtd5。两专项服务为**板载 svcd 托管** + (`trigger=boot`),拔卡保留,与无卡依赖最小化自洽。 + +--- + +## 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**。 +- 产出(本次构建实测): + - `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`, + 体积约 **166 KB**(固件阈值内)。md5:`ea1d6a58cbc0d312498d3925e2fd2127`。 + - `meowpanel_host`:aarch64 动态链接(宿主编译,仅供调试),体积约 **77 KB**。 +- 依赖:仅 libc + Linux 内核头(`linux/input.h`),零第三方库(设备无 python3/lua)。 +- 头文件引用:`-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。) +- 无鉴权(设备内网信任模型;token 鉴权为 MEOWPANEL_PROTOCOL §1.3 草案项,未实现)。 + +--- + +## 4. 端点实现清单(对照 MEOWPANEL_PROTOCOL §2 端点总览) + +### 已实现(返回 200/2xx 或语义错误码) + +| 方法 | 路径 | 说明 | 实现 | +|---|---|---|---| +| GET | `/`、`/meowpanel.css`、`/meowpanel.js`、`/assets/` | 静态托管 | ✅ | +| GET | `/api/status` | 性能+系统(`meow_svc_perf` IPC subset=all 透传) | ✅ | +| GET | `/api/perf` | 仅性能(subset=perf 透传) | ✅ | +| GET | `/api/system` | perf IPC + boot/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/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` 文件列表 | ✅(无卡→503) | +| GET | `/api/logs/read?file=&lines=` | 读日志(上限 1000 行;`..`/绝对→403;敏感名→403) | ✅ | +| GET | `/api/notifications` | **501** | ⚠️ | +| DELETE | `/api/notifications/:id` | **501** | ⚠️ | +| POST | `/api/notifications/dismiss_all` | **501** | ⚠️ | + +### 返回 501 `NOT_IMPLEMENTED` 的端点(诚实标注) + +| 端点 | 原因 | +|---|---| +| `/api/notifications`、`/api/notifications/:id`、`/api/notifications/dismiss_all` | 通知存储为 **launcher 进程内存**(`meow_notif.c g_slots[]`,NOTIFICATION_SYSTEM §4),后端为独立 svcd 子进程,**跨进程不可达**那批内存槽;框架只在关机前 dump 到 TF `MeowOS/notifications/notifications.log`(CSV 追加,非活跃快照)。**需为通知后端另开 IPC 查询/清撤通道**(如 `MSG_NOTIFY_LIST` 应答)或持久化活跃集,见 MEOWPANEL_PROTOCOL §6 待定案。 | +| `/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` 显示「该功能后端尚未实现」(`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 通知(501 及其依据) +见 §4 501 表:通知存储为 launcher 进程内存,后端跨进程不可读/清撤。已在源码 `api_notifications*` 注释「为何未实现 + +后续如何接」(通知后端加 `MSG_NOTIFY_LIST` 查询/清撤 IPC,或持久化活跃集到 TF `MeowOS/notifications/` 供只读)。 + +--- + +## 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`**:由性能服务聚合(`SYSTEM_FRAMEWORK_SERVICES §4.1`)后,后端只补 + `boot.default`(读 `/data/meow_boot_default`)、`network.hostname_local`(hostname+.local)与 `ota{current=version,available:false,url:null}`; + **wifi/ethernet/ota.available 不编造**(服务未聚合,前端对缺失字段显示占位)。若回改协议可注明「network/ota 待并入 perf 服务」。 +5. **`/api/files` 上传/删除/重命名等 path**:前端对这些操作传**绝对路径**(`d.path/子项`),列表传**相对 root**; + 后端 `normalize_path` 对两者均容错(相对则拼 root,绝对则直用),符合协议「白名单根 + 规范化」。 +6. **`GET /api/files` 返回值**:`files_list` 返回 `500` 语义时以 500 封装(协议未定义 `root` 之外错误枚举,用通用错误信封)。 + +--- + +## 7. 已知边界 / 风险 + +- **单线程串行**:面板访问量极小,但并发请求会被排队(长上传/下载期间其它请求等待)。对面板用途可接受。 +- **无鉴权**:设备内网信任模型(协议 §1.3 token 鉴权待定案)。**高危端点**(`reboot/shutdown/ota/apps/删除/上传)无鉴权, + 请勿把 8080 暴露到不可信网络。`meow_svc_power` 服务端做 `SO_PEERCRED uid==0` 基础校验(root 才受理动作),后端以 root 运行可过;launcher 电源菜单/后端同链路。 +- **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 逻辑,需同步本表。 +- **存储为 `meowpanel` 编译时刻写死**的 socket/路径会因 `--sock-perf/--sock-power`(qemu sim)可覆盖,但默认走 `/usr/data/meow_*.sock`。 + +--- + +## 8. 文件清单(本目录) + +``` +patch_v2/meowpanel/ +├── meowpanel.c # 后端源码(单文件,约 2700 行) +├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning) +├── meowpanel # 交叉产物(mipsel 静态,165 KB,md5 a8224ef9…) +├── meowpanel_host # 宿主编译(本机 ld,调试用) +├── services.d/ +│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf +├── web/ # 前端(已存在;部署时拷到后端同目录) +│ ├── index.html +│ ├── meowpanel.css +│ └── meowpanel.js +└── README.md # 本文件 +``` diff --git a/patch_v2/meowpanel/build_meowpanel.sh b/patch_v2/meowpanel/build_meowpanel.sh new file mode 100755 index 0000000..645c60f --- /dev/null +++ b/patch_v2/meowpanel/build_meowpanel.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# build_meowpanel.sh — MeowPanel 后端交叉编译(mipsel 静态)+ 宿主编译可选。 +# +# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。后端仅 POSIX + Linux 基接口 +# (复用 meow_ipc.h 帧格式,无 launcher 依赖),单文件满足「卡上、静态、零第三方依赖」。 +# +# 用法: +# ./build_meowpanel.sh # 交叉编译 → patch_v2/meowpanel/meowpanel(mipsel 静态) +# ./build_meowpanel.sh host # 宿主编译 → patch_v2/meowpanel/meowpanel_host +set -e +cd "$(dirname "$0")" +ROOT="$HOME/Project/R3ProII" +CC="$ROOT/.tools/zig-mipsel-cc" +I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/m_perf_query_t/m_perf_result_t/POWER_*) + +if [ "$1" = "host" ]; then + HOST_CC="${CC_HOST:-cc}" + "$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meowpanel.c -o meowpanel_host + echo "== host build ==" + ls -la meowpanel_host + file meowpanel_host + exit 0 +fi + +"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meowpanel.c -o meowpanel +echo "== meowpanel (mipsel static) ==" +ls -la meowpanel +md5sum meowpanel +file meowpanel diff --git a/patch_v2/meowpanel/meowpanel b/patch_v2/meowpanel/meowpanel new file mode 100755 index 0000000000000000000000000000000000000000..dfb91431508055d2e293fc1cb41013f598123172 GIT binary patch literal 166956 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 iA9TyGV@}Q#vuz3Id-b); s->b = NULL; s->n = s->cap = 0; } + +/* ---------------- 响应函数前向声明(文件列表等先于定义处使用) ---------------- */ +static int write_full(int sock, const void *b, size_t n); +static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra); +static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen); +static int send_ok(int sock, const char *data); +static int send_err(int sock, int status, const char *code, const char *msg); +static int send_json(int sock, int status, const char *data); +static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename); + +/* ---------------- CSV(enum 名/标签)非改写取值;定义在设置列表区,先声明供 settings_coerce 用 ---------------- */ +static const char *csv_at(const char *s, int idx, char *buf, int buflen); +static int csv_count(const char *s); + +/* ---------------- 最简 JSON 解析器(无第三方库) ---------------- + * 只覆盖 MeowPanel 实际用到的:对象 / 数组 / 字符串(含转义) / 数字 / bool / null。 + * value 树:J_OBJ 的 child 为键值对链(str=key,child=value,next=下个 pair); + * J_ARR 的 child 为元素链(next=下个元素)。 + */ +enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ }; + +typedef struct jv { + int type; + int bval; /* J_BOOL */ + long long inum; /* J_NUM 整数值 */ + double dnum; /* J_NUM 浮点值 */ + char *str; /* J_STR: 值; J_OBJ: key */ + struct jv *child; /* J_ARR: 头; J_OBJ: 头 pair */ + struct jv *next; /* 链 */ +} jv_t; + +typedef struct { const char *p; const char *end; int err; } jvp_t; + +static jv_t *jv_alloc(int type) +{ + jv_t *v = calloc(1, sizeof *v); + if (v) v->type = type; + return v; +} + +static void jv_free(jv_t *v) +{ + if (!v) return; + jv_t *c = v->child; + while (c) { jv_t *nx = c->next; jv_free(c->child); free(c->str); free(c); c = nx; } + free(v->str); + free(v); +} + +static void jvp_skip_ws(jvp_t *t) +{ + while (t->p < t->end && (*t->p == ' ' || *t->p == '\t' || *t->p == '\n' || *t->p == '\r')) t->p++; +} + +static char *jv_parse_string(jvp_t *t) +{ + if (t->p >= t->end || *t->p != '"') { t->err = 1; return NULL; } + t->p++; + sbuf_t s = {0}; + while (t->p < t->end && *t->p != '"') { + unsigned char c = (unsigned char)*t->p++; + if (c == '\\') { + if (t->p >= t->end) { t->err = 1; break; } + char e = *t->p++; + switch (e) { + case 'n': sA(&s, "\n"); break; + case 't': sA(&s, "\t"); break; + case 'r': sA(&s, "\r"); break; + case 'b': sA(&s, "\b"); break; + case 'f': sA(&s, "\f"); break; + case 'u': { + if (t->end - t->p < 4) { t->err = 1; sbuf_free(&s); return NULL; } + unsigned int cp = 0; + for (int i = 0; i < 4; i++) { char h = *t->p++; cp <<= 4; if (h>='0'&&h<='9') cp|=h-'0'; else if (h>='a'&&h<='f') cp|=h-'a'+10; else if (h>='A'&&h<='F') cp|=h-'A'+10; else { t->err=1; sbuf_free(&s); return NULL; } } + /* 只支持 BMP 常见字符;代理对按 UTF-8 重算(简化:cp<0x80 单字节,否则保守转发) */ + if (cp < 0x80) sA(&s, "%c", (int)cp); + else if (cp < 0x800) { sA(&s, "%c", 0xc0|(cp>>6)); sA(&s, "%c", 0x80|(cp&0x3f)); } + else { sA(&s, "%c", 0xe0|(cp>>12)); sA(&s, "%c", 0x80|((cp>>6)&0x3f)); sA(&s, "%c", 0x80|(cp&0x3f)); } + break; } + default: sA(&s, "%c", (int)e); break; + } + } else if (c < 0x20) { t->err = 1; break; } + else sA(&s, "%c", (int)c); + } + if (t->p >= t->end || *t->p != '"') { t->err = 1; sbuf_free(&s); return NULL; } + t->p++; + if (!s.b) { s.b = calloc(1, 1); } + return s.b; +} + +static jv_t *jv_parse_value(jvp_t *t); + +static jv_t *jv_parse_object(jvp_t *t) +{ + t->p++; + jv_t *obj = jv_alloc(J_OBJ); + if (!obj) { t->err = 1; return NULL; } + jv_t **tail = &obj->child; + jvp_skip_ws(t); + if (t->p < t->end && *t->p == '}') { t->p++; return obj; } + for (;;) { + jvp_skip_ws(t); + if (t->p >= t->end) { t->err = 1; jv_free(obj); return NULL; } + char *key = jv_parse_string(t); + if (!key) { jv_free(obj); return NULL; } + jvp_skip_ws(t); + if (t->p >= t->end || *t->p != ':') { t->err = 1; free(key); jv_free(obj); return NULL; } + t->p++; + jv_t *val = jv_parse_value(t); + if (!val) { t->err = 1; free(key); jv_free(obj); return NULL; } + jv_t *pair = calloc(1, sizeof *pair); + if (!pair) { t->err = 1; free(key); jv_free(val); jv_free(obj); return NULL; } + pair->type = J_OBJ; + pair->str = key; + pair->child = val; + *tail = pair; + tail = &pair->next; + jvp_skip_ws(t); + if (t->p < t->end && *t->p == ',') { t->p++; continue; } + if (t->p < t->end && *t->p == '}') { t->p++; return obj; } + t->err = 1; + jv_free(obj); + return NULL; + } +} + +static jv_t *jv_parse_array(jvp_t *t) +{ + t->p++; + jv_t *arr = jv_alloc(J_ARR); + if (!arr) { t->err = 1; return NULL; } + jv_t **tail = &arr->child; + jvp_skip_ws(t); + if (t->p < t->end && *t->p == ']') { t->p++; return arr; } + for (;;) { + jv_t *v = jv_parse_value(t); + if (!v) { jv_free(arr); return NULL; } + *tail = v; + tail = &v->next; + jvp_skip_ws(t); + if (t->p < t->end && *t->p == ',') { t->p++; continue; } + if (t->p < t->end && *t->p == ']') { t->p++; return arr; } + t->err = 1; + jv_free(arr); + return NULL; + } +} + +static jv_t *jv_parse_value(jvp_t *t) +{ + jvp_skip_ws(t); + if (t->p >= t->end) { t->err = 1; return NULL; } + char c = *t->p; + if (c == '{') return jv_parse_object(t); + if (c == '[') return jv_parse_array(t); + if (c == '"') { + char *s = jv_parse_string(t); + if (!s) return NULL; + jv_t *v = jv_alloc(J_STR); + if (!v) { free(s); t->err = 1; return NULL; } + v->str = s; + return v; + } + if (!strncmp(t->p, "true", 4)) { t->p += 4; jv_t *v = jv_alloc(J_BOOL); if (v) v->bval = 1; return v; } + if (!strncmp(t->p, "false", 5)) { t->p += 5; return jv_alloc(J_BOOL); } + if (!strncmp(t->p, "null", 4)) { t->p += 4; return jv_alloc(J_NULL); } + /* number */ + const char *q = t->p; + int isint = 1; + while (q < t->end && (*q == '-' || *q == '+' || *q == '.' || *q == 'e' || *q == 'E' || isdigit((unsigned char)*q))) { + if (*q == '.' || *q == 'e' || *q == 'E') isint = 0; + q++; + } + if (q == t->p) { t->err = 1; return NULL; } + char tmp[64]; + size_t dlen = (size_t)(q - t->p); + if (dlen >= sizeof tmp) dlen = sizeof tmp - 1; + memcpy(tmp, t->p, dlen); + tmp[dlen] = 0; + t->p = q; + jv_t *v = jv_alloc(J_NUM); + if (!v) { t->err = 1; return NULL; } + v->dnum = strtod(tmp, NULL); + v->inum = (long long)strtoll(tmp, NULL, 10); + (void)isint; + return v; +} + +static jv_t *jv_parse(const char *text, int len) +{ + jvp_t t; + t.p = text; + t.end = text + len; + t.err = 0; + jv_t *v = jv_parse_value(&t); + if (!v || t.err) { if (v) jv_free(v); return NULL; } + return v; +} + +static jv_t *jv_obj_get(const jv_t *obj, const char *key) +{ + if (!obj || obj->type != J_OBJ) return NULL; + for (const jv_t *p = obj->child; p; p = p->next) + if (p->str && !strcmp(p->str, key)) return p->child; + return NULL; +} + +/* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ---------------- + * 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录): + * - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5 + * 一致行为 **miuix**(对应 MEOW_STYLE_MIUIX)。前端 data-style 也用 miuix。故本表对齐为 + * "md3,miuix" / "Material You,MIUIx"(实现优先,回改见 README)。 + * - display.brightness 的 g_defs 为 min=0 max=255 def=101,但语义 clamp 到 [brightness_min(默认1),100] + * (防 101 闪屏)。本表对元数据给出 min=1 max=100(协议 §3.2 期望),实际 set 走 brightness clamp。 + */ +enum { ST_INT, ST_STR, ST_ENUM, ST_BOOL }; + +typedef struct setdef { + const char *key; + int type; + const char *def; + int min, max; /* ST_INT 取值域;max<=0 表示无上界 */ + const char *enum_names; /* ST_ENUM 逗号名 */ + const char *enum_labels; /* 对应中文 */ + const char *group; /* 分组标签(协议 groups 用) */ +} setdef_t; + +static const setdef_t g_sets[] = { + /* 显示 */ + { "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" }, + { "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" }, + { "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" }, + { "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" }, + { "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" }, + { "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" }, + { "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" }, + { "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" }, + { "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" }, + { "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" }, + { "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" }, + { "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" }, + /* 电源 */ + { "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" }, + { "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" }, + { "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" }, + /* 开机 */ + { "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" }, + { "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" }, + { "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" }, + /* 输入 */ + { "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" }, + { "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" }, + { "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" }, + { "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" }, + /* 字体/主题 */ + { "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" }, + { "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" }, + { "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" }, + { "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" }, + { "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" }, + { "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" }, + /* 通知 */ + { "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" }, + { "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" }, + { "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" }, + { "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" }, + { "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" }, + { "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" }, + /* 日志 */ + { "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" }, + { "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" }, + /* 音频 */ + { "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" }, + { "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" }, + { "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" }, + { "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" }, + /* 网络 */ + { "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" }, + { "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" }, + /* 系统 */ + { "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" }, +}; +#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0])) + +static const setdef_t *setdef_find(const char *key) { + for (int i = 0; i < SETS_COUNT; i++) + if (!strcmp(g_sets[i].key, key)) return &g_sets[i]; + return NULL; +} + +/* ---------------- settings.conf 解析(照 meow_ui_vol_read / settings_get_int_trim) ---------------- + * '=' 切分,key 去首尾空白精确匹配(防 audio.volume 误配 audio.volume_max)。 + */ +static char *settings_get_value(const char *key) +{ + FILE *f = fopen(SETTINGS_FILE, "r"); + if (!f) return NULL; + size_t kl = strlen(key); + char line[256]; + char *found = NULL; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + char *k = line; + while (*k == ' ' || *k == '\t') k++; + long n = (long)(eq - k); + while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--; + if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue; + found = eq + 1; + break; + } + fclose(f); + if (!found) return NULL; + while (*found == ' ' || *found == '\t') found++; + char *e = found + strlen(found); + while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0; + return strdup(found); +} + +static int settings_get_int(const char *key, int defv) +{ + char *v = settings_get_value(key); + if (!v) return defv; + int r = atoi(v); + free(v); + return r; +} + +/* 亮度 clamp:下限取 display.brightness_min(默认 1),上限 100(防 101 闪屏)。 + * 与 meow_settings.c clamp_brightness 同口径。 */ +static int brightness_clamp(int v) +{ + int mn = settings_get_int("display.brightness_min", 1); + if (mn < 1) mn = 1; + if (mn > 100) mn = 100; + if (v < mn) v = mn; + if (v > 100) v = 100; + return v; +} + +/* 重写 settings.conf:把 key 写为 "key = value"。保留头与其它行;缺 key 则追加。 + * O_TRUNC 重写。成功返回 0。 */ +static int settings_write_one(const char *key, const char *val) +{ + mkdir("/usr/data/meow", 0777); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */ + FILE *f = fopen(SETTINGS_FILE, "r"); + char tmp[2048]; + size_t n = 0; + int c; + if (f) { + while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c; + fclose(f); + } + tmp[n] = 0; + FILE *nf = fopen(SETTINGS_FILE, "w"); + if (!nf) return -1; + size_t klen = strlen(key); + int found = 0; + if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR); + /* 逐行重写:头照抄;命中 key 行替换;其余照抄 */ + char *save = NULL; + for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { + char *p = ln; + while (*p == ' ' || *p == '\t') p++; + if (!strncmp(p, key, klen) && p[klen] == '=') { + fprintf(nf, "%s = %s\n", key, val); + found = 1; + } else { + fprintf(nf, "%s\n", ln); + } + } + if (!found) fprintf(nf, "%s = %s\n", key, val); + fclose(nf); + return 0; +} + +/* 设定值→落盘规范字符串(clamp/校验集中在此)。返回 -1=非法(422),0=成功。 */ +static int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap) +{ + switch (d->type) { + case ST_INT: { + int v = atoi(input); + if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v); + else { + if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; } + else if (v < d->min) v = d->min; + } + snprintf(out, outcap, "%d", v); + return 0; + } + case ST_BOOL: { + char *q = (char *)input; + while (*q == ' ' || *q == '\t') q++; + int b = 0; + if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1; + else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; } + snprintf(out, outcap, "%d", b ? 1 : 0); + return 0; + } + case ST_ENUM: { + const char *names = d->enum_names; + if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; } + int cnt = csv_count(names); + char tok[128]; + /* 接受名字或索引(向后兼容) */ + int idx = -1; + for (int i = 0; i < cnt; i++) { + csv_at(names, i, tok, sizeof tok); + if (!strcmp(tok, input)) { idx = i; break; } + } + if (idx < 0) { + int n = atoi(input); + if (n >= 0 && n < cnt) idx = n; + } + if (idx < 0) return -1; /* 非法枚举 */ + csv_at(names, idx, tok, sizeof tok); + snprintf(out, outcap, "%s", tok); + return 0; + } + case ST_STR: + strncpy(out, input, outcap - 1); + out[outcap - 1] = 0; + return 0; + } + return -1; +} + +/* 恢复全部默认:重写整个文件为 头 + 所有键 def。 */ +static int settings_reset_all(void) +{ + mkdir("/usr/data/meow", 0777); + FILE *nf = fopen(SETTINGS_FILE, "w"); + if (!nf) return -1; + fprintf(nf, "%s\n", SETTINGS_HDR); + for (int i = 0; i < SETS_COUNT; i++) + fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def); + fclose(nf); + return 0; +} + +/* ---------------- IPC 帧读写(同 meow_svc_perf/power 客户端约定,全小端 LE) ---------------- */ +static int x_read_full(int fd, uint8_t *buf, size_t n) +{ + size_t off = 0; + while (off < n) { + ssize_t r = read(fd, buf + off, n - off); + if (r <= 0) return -1; + off += (size_t)r; + } + return 0; +} +static int x_write_full(int fd, const uint8_t *buf, size_t n) +{ + size_t off = 0; + while (off < n) { + ssize_t w = write(fd, buf + off, n - off); + if (w <= 0) return -1; + off += (size_t)w; + } + return 0; +} +static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len) +{ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t magic = MEOW_IPC_MAGIC, rl = len; + uint16_t ver = MEOW_IPC_VERSION, t = type; + memcpy(hdr + 0, &magic, 4); + memcpy(hdr + 4, &ver, 2); + memcpy(hdr + 6, &t, 2); + memcpy(hdr + 8, &req_id, 4); + memcpy(hdr + 12, &rl, 4); + if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1; + if (len && payload && x_write_full(fd, payload, len) < 0) return -1; + return 0; +} + +/* 读一帧:返回 payload 长度(<=cap 时复制到 buf),-1 失败;type/req_id 回传。 */ +static int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap) +{ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1; + uint32_t magic, rlen, rid; + uint16_t ver, t; + memcpy(&magic, hdr, 4); + memcpy(&ver, hdr + 4, 2); + memcpy(&t, hdr + 6, 2); + memcpy(&rid, hdr + 8, 4); + memcpy(&rlen, hdr + 12, 4); + if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1; + if (rlen > MEOW_IPC_MAX) return -1; + if (type) *type = t; + if (req_id) *req_id = rid; + uint32_t n = (rlen < cap) ? rlen : cap; + if (n && x_read_full(fd, buf, n) < 0) return -1; + /* 若实际更长,丢弃剩余 */ + for (uint32_t i = n; i < rlen; i++) { uint8_t c; if (x_read_full(fd, &c, 1) < 0) return -1; } + return (int)n; +} + +/* 带超时连 AF_UNIX 流 socket:非阻塞 connect + poll(POLLOUT)。 */ +static int ipc_connect_tmo(const char *sock, int tmo_ms) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return -1; + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + strncpy(a.sun_path, sock, sizeof a.sun_path - 1); + int fl = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, fl | O_NONBLOCK); + int rc = connect(fd, (struct sockaddr *)&a, sizeof a); + if (rc < 0 && errno == EINPROGRESS) { + struct pollfd pf = { fd, POLLOUT, 0 }; + int pr = poll(&pf, 1, tmo_ms); + if (pr <= 0) { close(fd); return -1; } + int soerr = 0; + socklen_t sl = sizeof soerr; + getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl); + if (soerr != 0) { close(fd); return -1; } + } else if (rc < 0) { close(fd); return -1; } + fcntl(fd, F_SETFL, fl); + /* 读/写超时兜底(正常服务应即时响应) */ + struct timeval tv = { tmo_ms / 1000, (tmo_ms % 1000) * 1000 }; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + return fd; +} + +/* 服务端握手(MSG_HELLO → MSG_ACK),app_id 用系统面板身份 os.meow.panel。 */ +static int ipc_hello(int fd) +{ + m_hello_t h; + memset(&h, 0, sizeof h); + strncpy(h.app_id, "os.meow.panel", sizeof h.app_id - 1); + h.version = 1; + if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) < 0) return -1; + uint8_t buf[64]; + uint16_t type; uint32_t rid; + int n = recv_frame(fd, &type, &rid, buf, sizeof buf); + if (n < (int)sizeof(m_ack_t)) return -1; + if (type != MSG_ACK) return -1; + m_ack_t a; + memcpy(&a, buf, sizeof a); + return (a.code == MEOW_OK) ? 0 : -1; +} + +/* ---------------- 读侧:meow_svc_perf IPC ---------------- + * 返回 0=成功(json 已填入 out/cap),否则负错误码,errmsg 填中文说明。 + */ +static const char *perf_code_str(int code) +{ + switch (code) { + case MEOW_OK: return ""; /* 不应使用 */ + case MEOW_ERR_INVAL: return "参数非法"; + case MEOW_ERR_IO: return "读取失败"; + case MEOW_ERR_NODEV: return "设备不存在"; + default: return "服务端错误"; + } +} + +static int perf_subset_for_path(const char *path) +{ + if (!strcmp(path, "/api/perf")) return PERF_SUBSET_PERF; + if (!strcmp(path, "/api/system")) return 0; /* system 需 uptime/storage,取全量 */ + return 0; /* /api/status */ +} + +static int ipc_perf_query(int subset, char *out, int cap) +{ + int fd = ipc_connect_tmo(g_sock_perf, IPC_TIMEOUT_MS); + if (fd < 0) return MEOW_ERR_NODEV; + int rc = MEOW_ERR_INTERNAL; + char msg[128] = ""; + if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; snprintf(msg, sizeof msg, "握手中断:性能服务未就绪"); goto done; } + m_perf_query_t q; + memset(&q, 0, sizeof q); + q.subset = (uint32_t)subset; + if (send_frame(fd, MSG_PERF_QUERY, 2, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "发送查询失败"); goto done; } + uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN]; + uint16_t type; uint32_t rid; + int n = recv_frame(fd, &type, &rid, buf, sizeof buf); + if (n < (int)sizeof(m_perf_result_t)) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "应答帧过短"); goto done; } + if (type != MSG_PERF_RESULT) { rc = MEOW_ERR_INVAL; snprintf(msg, sizeof msg, "非预期应答类型 0x%04x", type); goto done; } + m_perf_result_t r; + memcpy(&r, buf, sizeof r); + if (r.code != MEOW_OK) { rc = (int)r.code; snprintf(msg, sizeof msg, "性能服务错误: %s", perf_code_str((int)r.code)); goto done; } + uint16_t jl = r.json_len; + if (jl > sizeof r.json - 1) jl = (uint16_t)(sizeof r.json - 1); + if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1); + memcpy(out, r.json, jl); + out[jl] = 0; + rc = MEOW_OK; +done: + close(fd); + /* 服务不可达/非 OK 由调用方据此返回错误;绝不静默失败、绝不降级直连 /proc。 */ + return rc; +} + +/* ---------------- 动作侧:meow_svc_power IPC ---------------- + * 返回 0=受理;否则负错误码(PERM/INVAL/NODEV/…),msg 填服务端说明。 + */ +static const char *power_code_str(int code) +{ + switch (code) { + case MEOW_OK: return ""; + case MEOW_ERR_PERM: return "无权限(需 root)"; + case MEOW_ERR_INVAL: return "未知命令"; + case MEOW_ERR_NODEV: return "设备/能力不支持"; + case MEOW_ERR_NOENT: return "缺少升级文件"; + case MEOW_ERR_IO: return "读写失败"; + default: return "电源服务错误"; + } +} + +static int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st) +{ + int fd = ipc_connect_tmo(g_sock_power, IPC_TIMEOUT_MS); + if (fd < 0) return MEOW_ERR_NODEV; + int rc = MEOW_ERR_INTERNAL; + if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; } + m_power_action_t a; + memset(&a, 0, sizeof a); + a.cmd = cmd; + a.flag = flag; + if (arg) snprintf(a.arg, sizeof a.arg, "%s", arg); + if (send_frame(fd, MSG_POWER_ACTION, 3, &a, sizeof a) < 0) { rc = MEOW_ERR_IO; goto done; } + uint8_t buf[256]; + uint16_t type; uint32_t rid; + int n = recv_frame(fd, &type, &rid, buf, sizeof buf); + if (n < (int)sizeof(m_power_status_t)) { rc = MEOW_ERR_IO; goto done; } + if (type != MSG_POWER_STATUS) { rc = MEOW_ERR_INVAL; goto done; } + m_power_status_t st; + memcpy(&st, buf, sizeof st); + if (out_st) *out_st = st; + rc = (int)st.code; +done: + close(fd); + return rc; +} + +/* ---------------- 路径安全 ---------------- */ +static int tf_present(void) +{ + FILE *f = fopen("/proc/mounts", "r"); + if (!f) return 0; + char line[512]; + int present = 0; + while (fgets(line, sizeof line, f)) { + if (strstr(line, TF_ROOT)) { present = 1; break; } + } + fclose(f); + return present; +} + +/* 判断某个规范化路径是否在卡根白名单内(==TF_ROOT 或属于某个白名单一级子目录)。 */ +static int path_in_whitelist(const char *abs) +{ + size_t rl = strlen(TF_ROOT); + if (strncmp(abs, TF_ROOT, rl) != 0) return 0; + if (abs[rl] == 0) return 1; /* 根自身 */ + if (abs[rl] != '/') return 0; + const char *rest = abs + rl + 1; + for (int i = 0; i < ALLOWED_COUNT; i++) { + size_t dl = strlen(g_allowed_dirs[i]); + if (strncmp(rest, g_allowed_dirs[i], dl) == 0) { + if (rest[dl] == 0 || rest[dl] == '/') return 1; + } + } + return 0; +} + +static int is_sensitive_name(const char *base) +{ + for (int i = 0; i < SENSITIVE_COUNT; i++) + if (!strcmp(base, g_sensitive_names[i])) return 1; + return 0; +} + +/* 规范化输入路径(去除 . / ..;绝对/相对容错)。 + * 输入可能是绝对(`/usr/data/mnt/sd_0/MeowOS/x`,前端下载/删除等用)或相对 root(`/MeowOS/x`)。 + * 输出(buf)为规范化后的绝对路径;若 escape 出卡根,返回 -1。 */ +static int normalize_path(const char *in, char *buf, size_t buflen) +{ + size_t rl = strlen(TF_ROOT); + char joined[1024]; + if (in[0] == '/') { + if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) { + snprintf(joined, sizeof joined, "%s", in); + } else { + /* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */ + if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1); + else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in); + else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in); + } + } else { + snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in); + } + /* 逐分量规范化:/a/b/../c → /a/c;拒绝逃逸出 root */ + char out[1024]; + size_t n = 0; + out[0] = 0; + char *p = joined; + while (*p) { + while (*p == '/') p++; + if (!*p) break; + char seg[256]; + int k = 0; + while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; } + seg[k] = 0; + if (!strcmp(seg, ".") || !strcmp(seg, "")) continue; + if (!strcmp(seg, "..")) { + /* 弹出一级;若已在 root 再 .. 则逃逸 */ + if (n == 0) return -1; + while (n > 0 && out[n - 1] != '/') n--; + if (n > 0) n--; /* 去掉结尾 / */ + out[n] = 0; + continue; + } + if (k > 0) { + size_t need = strlen(out) + 1 + k + 1; + if (need >= sizeof out) return -1; + if (out[0]) strncat(out, "/", sizeof out - strlen(out) - 1); + strncat(out, seg, sizeof out - strlen(out) - 1); + } + } + /* 空 → root */ + if (!out[0]) snprintf(out, sizeof out, "%s", TF_ROOT); + if (strlen(out) >= buflen) return -1; + snprintf(buf, buflen, "%s", out); + return 0; +} + +/* 展示 rel(相对 root):如 /MeowOS/x。 */ +static void make_rel(const char *abs, char *buf, size_t buflen) +{ + size_t rl = strlen(TF_ROOT); + if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; } + const char *rest = abs + rl; + if (*rest == 0) snprintf(buf, buflen, "/"); + else snprintf(buf, buflen, "%s", rest); +} + +/* 安全地打开一个可读文件(白名单内 + 非敏感名)。失败返回 -1。 */ +static int open_safe_for_read(const char *abs, int follow_rel) +{ + (void)follow_rel; + if (!path_in_whitelist(abs)) return -1; + const char *base = strrchr(abs, '/'); + base = base ? base + 1 : abs; + if (is_sensitive_name(base)) return -1; + struct stat st; + if (stat(abs, &st) < 0) return -1; + if (!S_ISREG(st.st_mode)) return -1; + return open(abs, O_RDONLY); +} + +/* ---------------- 文件浏览 ---------------- */ +static void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime) +{ + sA(s, "{\"name\":"); + sA_str(s, name); + sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}", + isdir ? "dir" : "file", size, mtime); +} + +static int files_list(int sock, const char *relpath) +{ + char abs[1024], rel[1024]; + if (normalize_path(relpath, abs, sizeof abs) < 0) + return 403; + if (!path_in_whitelist(abs)) return 403; + struct stat st; + if (stat(abs, &st) < 0) return 404; + if (!S_ISDIR(st.st_mode)) return 404; + DIR *d = opendir(abs); + if (!d) return 500; + make_rel(abs, rel, sizeof rel); + sbuf_t s = {0}; + sA(&s, "{\"root\":\"" TF_ROOT "\",\"path\":"); + sA_str(&s, abs); + sA(&s, ",\"rel\":"); + sA_str(&s, rel); + sA(&s, ",\"allowed\":["); + for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); } + sA(&s, "],\"items\":["); + int first = 1; + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char full[1400]; + snprintf(full, sizeof full, "%s/%s", abs, e->d_name); + struct stat fs; + if (stat(full, &fs) < 0) continue; + int isdir = S_ISDIR(fs.st_mode); + long long size = (long long)fs.st_size; + long long mtime = (long long)fs.st_mtime; + if (isdir) size = 0; + if (!first) sA(&s, ","); + first = 0; + fmt_json_item(&s, e->d_name, isdir, size, mtime); + } + closedir(d); + sA(&s, "]}"); + return send_json(sock, 200, s.b ? s.b : ""); +} + +/* ---------------- HTTP 响应(JSON 信封) ---------------- */ +/* send_json 前置声明 */ +static int send_json(int sock, int status, const char *data); + +/* 读文本文件(<1MB),返回 malloc 内容;二进制检测 -2;太大 -3;打不开 -1。 */ +static char *read_text_file_limited(const char *abs, long long *size, int *truncated) +{ + int fd = open_safe_for_read(abs, 0); + if (fd < 0) return NULL; + struct stat st; + if (fstat(fd, &st) < 0 || st.st_size > 1024 * 1024) { close(fd); return NULL; } + if (st.st_size < 0) { close(fd); return NULL; } + char *buf = malloc((size_t)st.st_size + 1); + if (!buf) { close(fd); return NULL; } + ssize_t n = read(fd, buf, (size_t)st.st_size); + close(fd); + if (n < 0) { free(buf); return NULL; } + buf[n] = 0; + /* 二进制检测:含 NUL 或大量控制字符 */ + int ctrl = 0; + for (ssize_t i = 0; i < n; i++) { + unsigned char c = (unsigned char)buf[i]; + if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */ + if (c < 0x09) ctrl++; + } + if (ctrl > n / 10) { free(buf); return (char *)-2; } + if (size) *size = (long long)n; + if (truncated) *truncated = 0; + return buf; +} + +/* ---------------- 按键注入(经 /dev/input/event* 写 EV_KEY;uinput_tap inject 同机制) ---------------- + * 设备映射(launcher meow_launcher_internal.h): + * /dev/input/event0 = md-gpio-keys: KEY_POWER(116), KEY_PREVIOUSSONG(165) + * /dev/input/event2 = jz adc keyboard: KEY_VOLUMEUP(115), KEY_VOLUMEDOWN(114), KEY_NEXTSONG(163), KEY_PLAYPAUSE(164) + * 注入 = open(O_WRONLY) 写 EV_KEY press/release + SYN_REPORT(evdev 支持 write 注入,uinput_tap.c inject 先例)。 + * 红线(APPS.md 物理键 / 任务约束):只注入音量/电源/播放/上下曲,**禁止映射 返回(158)/主页(172)/确认/exit**。 + */ +#define PWR_KEYDEV "/dev/input/event0" +#define ADC_KEYDEV "/dev/input/event2" + +static int keycode_to_device(int code, const char **dev) +{ + switch (code) { + case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER; + case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG; + case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP; + case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN; + case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG; + case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE; + default: return -1; + } +} + +static int inject_key_event(const char *dev, int code, int down) +{ + int fd = open(dev, O_WRONLY | O_NONBLOCK); + if (fd < 0) return -1; + struct input_event ev; + memset(&ev, 0, sizeof ev); + ev.type = EV_KEY; + ev.code = code; + ev.value = down ? 1 : 0; + int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); + if (ok) { + memset(&ev, 0, sizeof ev); + ev.type = EV_SYN; + ev.code = SYN_REPORT; + ev.value = 0; + ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev); + } + close(fd); + return ok ? 0 : -1; +} + +/* 注入 tap(按下+抬起)或 long(按住 ms)。返回 0=成功。 */ +static int inject_key_release(int code, int press_ms) +{ + const char *dev = NULL; + int kc = keycode_to_device(code, &dev); + if (kc < 0) return -1; + int release_ms = press_ms > 0 ? press_ms : 0; + if (inject_key_event(dev, code, 1) < 0) return -1; + if (release_ms > 0) usleep((useconds_t)release_ms * 1000); + else usleep(80 * 1000); + if (inject_key_event(dev, code, 0) < 0) return -1; + return 0; +} + +/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */ +static int read_small_file(const char *path, char *buf, int cap) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) return -1; + int n = 0; + while (n < cap - 1) { + ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n)); + if (r <= 0) break; + n += (int)r; + } + close(fd); + buf[n] = 0; + return n; +} + +/* 读某应用目录的 app.json(用 jv 解析提取字段,供扫描/详情)。 */ +static int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail) +{ + char pj[1024]; + snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name); + char buf[8192]; + if (read_small_file(pj, buf, sizeof buf) <= 0) { + /* 解析失败项:invalid */ + if (!want_detail) { + sA(out, "{\"package\":"); + sA_str(out, name); + sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); + } + return -1; + } + jv_t *root = jv_parse(buf, (int)strlen(buf)); + if (!root) { + if (!want_detail) { + sA(out, "{\"package\":"); + sA_str(out, name); + sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}"); + } + return -1; + } + jv_t *jpkg = jv_obj_get(root, "package"); + jv_t *jname = jv_obj_get(root, "name"); + jv_t *jver = jv_obj_get(root, "version"); + jv_t *jexec = jv_obj_get(root, "exec"); + jv_t *jfw = jv_obj_get(root, "framework"); + jv_t *jim = jv_obj_get(root, "immersive"); + jv_t *jfull = jv_obj_get(root, "fullscreen"); + const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name; + const char *nm = (jname && jname->type == J_STR) ? jname->str : ""; + const char *ver = (jver && jver->type == J_STR) ? jver->str : ""; + const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : ""; + /* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */ + long long dsize = 0; + snprintf(pj, sizeof pj, "%s/%s", appdir, name); + DIR *d = opendir(pj); + if (d) { + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char full[1024]; + snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name); + struct stat st; + if (stat(full, &st) == 0) dsize += (long long)st.st_size; + } + closedir(d); + } + if (!want_detail) { + sA(out, "{\"package\":"); + sA_str(out, pkg); + sA(out, ",\"name\":"); + sA_str(out, nm[0] ? nm : NULL); + sA(out, ",\"version\":"); + sA_str(out, ver[0] ? ver : NULL); + sA(out, ",\"exec\":"); + sA_str(out, exec); + sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}", + jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL)); + } else { + sA(out, "{\"package\":"); + sA_str(out, pkg); + sA(out, ",\"name\":"); + sA_str(out, nm[0] ? nm : NULL); + sA(out, ",\"version\":"); + sA_str(out, ver[0] ? ver : NULL); + sA(out, ",\"exec\":"); + sA_str(out, exec); + sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":", + jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize); + sA_str(out, pj); + sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}"); + } + jv_free(root); + return 0; +} + +/* ---------------- HTTP 请求读取 ---------------- */ +typedef struct { + char method[8]; + char path[512]; /* 解码后的路径(不含 query) */ + char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */ + char *body; + size_t body_len; + char content_type[128]; /* 请求 Content-Type(若非空) */ +} req_t; + +static void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; } + +static int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } +static int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; } + +/* URL 解码到 buf。 */ +static void url_decode(const char *in, char *buf, int buflen) +{ + int o = 0; + for (int i = 0; in[i] && o < buflen - 1; i++) { + if (in[i] == '%' && is_hex(in[i + 1]) && is_hex(in[i + 2])) { + buf[o++] = (char)((hexval(in[i + 1]) << 4) | hexval(in[i + 2])); + i += 2; + } else if (in[i] == '+') buf[o++] = ' '; + else buf[o++] = in[i]; + } + buf[o] = 0; +} + +/* 从 query(key=value&…)取第一个值(url 解码后写入 out),未找到返回 0。 */ +static int qparam(const char *query, const char *key, char *out, int outcap) +{ + size_t kl = strlen(key); + const char *p = query; + while (p && *p) { + const char *amp = strchr(p, '&'); + size_t seg = amp ? (size_t)(amp - p) : strlen(p); + if (seg >= kl + 1 && strncmp(p, key, kl) == 0 && p[kl] == '=') { + char tmp[1024]; + size_t vlen = seg - kl - 1; + if (vlen >= sizeof tmp) vlen = sizeof tmp - 1; + memcpy(tmp, p + kl + 1, vlen); + tmp[vlen] = 0; + url_decode(tmp, out, outcap); + return 1; + } + if (!amp) break; + p = amp + 1; + } + return 0; +} + +/* 从请求 path 中取动态段(如 /api/settings/:key)。返回是否匹配。 */ +static int path_segment(const char *prefix, const char *full, char *out, int outcap) +{ + size_t pl = strlen(prefix); + if (strncmp(full, prefix, pl) != 0) return 0; + const char *rest = full + pl; + if (*rest == '/') rest++; + if (!*rest) return 0; + size_t l = strlen(rest); + if (l >= (size_t)outcap) l = (size_t)outcap - 1; + memcpy(out, rest, l); + out[l] = 0; + /* 去掉后续 / 段(取第一段) */ + char *slash = strchr(out, '/'); + if (slash) *slash = 0; + return 1; +} + +/* 从 "GET /path?query HTTP/1.1" 的 path 部分切出 path 与 query。 */ +static void split_path_query(const char *src, char *path, int plc, char *query, int qlc) +{ + const char *q = strchr(src, '?'); + if (q) { + size_t pl = (size_t)(q - src); + if (pl >= (size_t)plc) pl = (size_t)plc - 1; + memcpy(path, src, pl); + path[pl] = 0; + snprintf(query, qlc, "%s", q + 1); + } else { + snprintf(path, plc, "%s", src); + query[0] = 0; + } +} + +/* 在 buf[0..n) 中找首个 "\r\n\r\n",返回其起始下标;找不到返回 -1。 */ +static ptrdiff_t find_header_end(const char *buf, size_t n) +{ + if (n < 4) return -1; + for (size_t i = 0; i + 3 < n; i++) { + if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n') + return (ptrdiff_t)i; + } + return -1; +} + +/* 读请求:先读头(到 \r\n\r\n),解析 Content-Length/Content-Type,再精确读 body。 + * 处理「头+body 同一 TCP 段到达」(小 POST 常见):单次 read 可能把 body 一并读入, + * 故在缓冲区内**搜索** \r\n\r\n 定位头边界,body 从边界后搬移。成功 0;超时/过大/失败 -1。 */ +static int read_request(int sock, req_t *r) +{ + char *buf = malloc(MAX_HTTP_HEADER); + if (!buf) return -1; + size_t hn = 0; + int deadline_ms = 5000; + ptrdiff_t hend = -1; + for (;;) { + struct pollfd pf = { sock, POLLIN, 0 }; + int pr = poll(&pf, 1, deadline_ms); + if (pr <= 0) { free(buf); return -1; } + ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn); + if (rd <= 0) { free(buf); return -1; } + hn += (size_t)rd; + buf[hn] = 0; + hend = find_header_end(buf, hn); + if (hend >= 0) break; + if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; } + } + /* 头结束在 hend,body 起点在 hend+4 */ + char *hdr = buf; + char *line_end = strstr(hdr, "\r\n"); + if (!line_end) { free(buf); return -1; } + *line_end = 0; + char method[8], pathraw[512], proto[16]; + if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; } + snprintf(r->method, sizeof r->method, "%s", method); + split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query); + size_t clen = 0; + char *hp = line_end + 2; + while (hp < buf + hend && strncmp(hp, "\r\n", 2)) { + char *he = strstr(hp, "\r\n"); + if (!he || he > buf + hend) break; + *he = 0; + if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10); + else if (!strncasecmp(hp, "Content-Type:", 13)) { + char *v = hp + 13; + while (*v == ' ') v++; + snprintf(r->content_type, sizeof r->content_type, "%s", v); + } + hp = he + 2; + } + char *body_start = buf + hend + 4; /* body 实际起点 */ + size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0; + if (clen > MAX_HTTP_BODY) { free(buf); return -1; } + if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; } + char *body = malloc(clen + 1); + if (!body) { free(buf); return -1; } + size_t got = 0; + if (buffered > 0) { + size_t take = buffered < clen ? buffered : clen; + memcpy(body, body_start, take); + got = take; + } + free(buf); + while (got < clen) { + struct pollfd pf = { sock, POLLIN, 0 }; + int pr = poll(&pf, 1, 5000); + if (pr <= 0) { free(body); return -1; } + ssize_t rd = read(sock, body + got, clen - got); + if (rd <= 0) { free(body); return -1; } + got += (size_t)rd; + } + body[clen] = 0; + r->body = body; + r->body_len = clen; + return 0; +} + +/* ---------------- HTTP 响应写入 ---------------- */ +static int write_full(int sock, const void *b, size_t n) +{ + const char *p = (const char *)b; + size_t off = 0; + while (off < n) { + ssize_t w = write(sock, p + off, n - off); + if (w <= 0) return -1; + off += (size_t)w; + } + return 0; +} + +static int send_head(int sock, int status, const char *ctype, long long clen, const char *extra) +{ + char h[512]; + const char *sts = "OK"; + switch (status) { + case 200: sts = "OK"; break; + case 201: sts = "Created"; break; + case 202: sts = "Accepted"; break; + case 400: sts = "Bad Request"; break; + case 401: sts = "Unauthorized"; break; + case 403: sts = "Forbidden"; break; + case 404: sts = "Not Found"; break; + case 405: sts = "Method Not Allowed"; break; + case 409: sts = "Conflict"; break; + case 413: sts = "Payload Too Large"; break; + case 415: sts = "Unsupported Media Type"; break; + case 422: sts = "Unprocessable Entity"; break; + case 500: sts = "Internal Server Error"; break; + case 501: sts = "Not Implemented"; break; + case 503: sts = "Service Unavailable"; break; + default: sts = "OK"; break; + } + snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n", + status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : ""); + return write_full(sock, h, strlen(h)); +} + +/* 发送原生 bytes(已含 body)。 */ +static int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen) +{ + if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1; + if (bodylen && write_full(sock, body, bodylen) < 0) return -1; + return 0; +} + +/* 成功 JSON 信封:{"ok":true,"data":}。 + * 前缀 "{"ok":true,"data":" 共 18 字节。用 memcpy 精确构包(sA 的 %s 有 ~511B 单次截断,不能用于大 payload)。 */ +static int send_ok(int sock, const char *data) +{ + static const char prefix[] = "{\"ok\":true,\"data\":"; + size_t pl = sizeof prefix - 1; /* 18 */ + size_t dl = strlen(data); + size_t tot = pl + dl + 1; + char *b = malloc(tot + 1); + if (!b) { + const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}"; + return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e)); + } + memcpy(b, prefix, pl); + memcpy(b + pl, data, dl); + b[pl + dl] = '}'; + b[pl + dl + 1] = 0; + int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot); + free(b); + return rc; +} + +/* 错误 JSON 信封:{"ok":false,"error":{"code":..,"message":..}} */ +static int send_err(int sock, int status, const char *code, const char *msg) +{ + sbuf_t s = {0}; + sA(&s, "{\"ok\":false,\"error\":{\"code\":"); + sA_str(&s, code ? code : "INTERNAL"); + sA(&s, ",\"message\":"); + sA_str(&s, msg ? msg : ""); + sA(&s, "}}"); + int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n); + sbuf_free(&s); + return rc; +} + +static int send_json(int sock, int status, const char *data) +{ + if (status == 200) return send_ok(sock, data); + return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data)); +} + +/* 流式发送磁盘文件(下载)。 */ +static int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename) +{ + int fd = open(abspath, O_RDONLY); + if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在"); + struct stat st; + if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } + char extra[256]; + if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename); + else extra[0] = 0; + if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; } + char buf[8192]; + ssize_t n; + while ((n = read(fd, buf, sizeof buf)) > 0) { + if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; } + } + close(fd); + return 0; +} + +/* ---------------- multipart/form-data 解析(文件上传) ---------------- + * 解析 body,提取 name= 字段与 name=file 字段(可多个)。写到 target dir(白名单校验)。 + * 返回 0=成功(把写入的 item 用 items_json 回填),负错误码语义同协议。 + */ +typedef struct { char path[1400]; char filename[256]; } upfile_t; + +static int parse_multipart(const req_t *r, upfile_t *files, int maxfiles) +{ + /* 取 boundary */ + const char *ct = r->content_type; + const char *b = strstr(ct, "boundary="); + if (!b) return -1; + b += 9; + if (*b == '"') { b++; } + char boundary[128]; + int bi = 0; + while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; } + boundary[bi] = 0; + char dmark[160]; + snprintf(dmark, sizeof dmark, "--%s", boundary); + char emark[160]; + snprintf(emark, sizeof emark, "--%s--", boundary); + /* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */ + const char *p = r->body; + const char *end = r->body + r->body_len; + int nfiles = 0; + char curpath[1024] = ""; + while (p < end) { + const char *fstart = strstr(p, dmark); + if (!fstart) break; + fstart += strlen(dmark); + if (*fstart == '-') break; /* 结束标记 --boundary-- */ + if (*fstart == '\r' || *fstart == '\n') fstart++; + /* 解析该 part 的头部(直至空行) */ + const char *headers_end = strstr(fstart, "\r\n\r\n"); + if (!headers_end) break; + char hdr[512]; + size_t hl = (size_t)(headers_end - fstart); + if (hl >= sizeof hdr) hl = sizeof hdr - 1; + memcpy(hdr, fstart, hl); + hdr[hl] = 0; + const char *data_start = headers_end + 4; + const char *name = NULL, *fname = NULL; + char *save = NULL; + for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { + char *l = ln; + while (*l == '\r' || *l == ' ' || *l == '\t') l++; + if (strncasecmp(l, "Content-Disposition:", 20) == 0) { + char *nm = strstr(l, "name=\""); + if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } } + char *fn = strstr(l, "filename=\""); + if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } } + } + } + /* 数据结束:下一个 "--boundary" 之前 */ + const char *data_end = strstr(data_start, dmark); + if (!data_end) data_end = end; + /* 去掉数据尾部 \r\n */ + while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--; + if (name && !strcmp(name, "path")) { + size_t dl = (size_t)(data_end - data_start); + if (dl >= sizeof curpath) dl = sizeof curpath - 1; + memcpy(curpath, data_start, dl); + curpath[dl] = 0; + } else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) { + size_t dl = (size_t)(data_end - data_start); + snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", fname); + snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", curpath); + /* 写文件 */ + char abs[1024]; + if (normalize_path(files[nfiles].path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { nfiles++; continue; } + snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s/%s", abs, fname); + int fd = open(files[nfiles].path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd >= 0) { + write_full(fd, data_start, dl); + close(fd); + } + nfiles++; + } + p = data_end; + } + return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */ +} + +/* ---------------- 日志(stderr;svcd supervisor 捕获) ---------------- */ +static void panel_log(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + fprintf(stderr, "[MeowPanel] "); + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + va_end(ap); +} + +/* ---------------- 设置项 JSON 序列化 ---------------- */ +static int str_truthy(const char *v) +{ + char q[32]; + snprintf(q, sizeof q, "%s", v); + char *p = q; + while (*p == ' ' || *p == '\t') p++; + if (!strcasecmp(p, "true") || !strcasecmp(p, "yes") || !strcasecmp(p, "on")) return 1; + long n = strtol(p, NULL, 10); + return (p[0] && n != 0); +} + +static int int_meta_max(const setdef_t *d) +{ + if (d->max > 0) return d->max; + int dn = atoi(d->def); + if (dn > 0) return dn * 2 + 100; + return 100; +} + +/* 非改写的 CSV 辅助:取第 idx 个项(抄入 buf,NUL 结尾),未命中返回空串。 + * 用于 enum 名/标签 —— 不再 strtok_r(那会在只读字面量上写 NUL SEGV)。 */ +static const char *csv_at(const char *s, int idx, char *buf, int buflen) +{ + buf[0] = 0; + if (!s || !s[0]) return buf; + const char *p = s; + int i = 0; + for (;;) { + const char *start = p; + while (*p && *p != ',') p++; + if (i == idx) { + size_t k = (size_t)(p - start); + if (k >= (size_t)buflen) k = (size_t)buflen - 1; + memcpy(buf, start, k); + buf[k] = 0; + break; + } + if (!*p) break; + p++; + i++; + } + return buf; +} + +static int csv_count(const char *s) +{ + if (!s || !s[0]) return 0; + int n = 1; + for (const char *p = s; *p; p++) if (*p == ',') n++; + return n; +} + +static void enum_options_json(sbuf_t *s, const char *names, const char *labels) +{ + sA(s, "["); + int cnt = csv_count(names); + for (int i = 0; i < cnt; i++) { + char nb[128], lb[128]; + csv_at(names, i, nb, sizeof nb); + csv_at(labels, i, lb, sizeof lb); + if (i) sA(s, ","); + sA(s, "{\"name\":"); + sA_str(s, nb); + sA(s, ",\"label\":"); + sA_str(s, lb[0] ? lb : nb); + sA(s, "}"); + } + sA(s, "]"); +} + +static void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val) +{ + const char *tn = "str"; + if (d->type == ST_INT) tn = "int"; + else if (d->type == ST_BOOL) tn = "bool"; + else if (d->type == ST_ENUM) tn = "enum"; + sA(s, "{\"key\":"); + sA_str(s, d->key); + sA(s, ",\"type\":\"%s\",\"value\":", tn); + if (d->type == ST_INT) { + int v = atoi(val); + if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v); + else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; } + else if (v < d->min) v = d->min; + sA(s, "%d", v); + sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d)); + } else if (d->type == ST_BOOL) { + sA(s, "%s", str_truthy(val) ? "true" : "false"); + } else { + sA_str(s, val); + } + sA(s, ",\"def\":"); + sA_str(s, d->def); + if (d->type == ST_ENUM) { + sA(s, ",\"options\":"); + enum_options_json(s, d->enum_names, d->enum_labels); + sA(s, ",\"default_name\":"); + sA_str(s, d->def); + } + sA(s, "}"); +} + +/* 读某键当前值(无则用默认;str 用默认)。返回 malloc 字符串。 */ +static char *settings_current_value(const setdef_t *d) +{ + char *v = settings_get_value(d->key); + if (v) return v; + return strdup(d->def); +} + +/* ---------------- 设置端点 ---------------- */ +static int api_settings_get_any(int sock, const char *key) +{ + const setdef_t *d = setdef_find(key); + if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项"); + char *val = settings_current_value(d); + sbuf_t s = {0}; + settings_item_json(&s, d, val); + int rc = send_json(sock, 200, s.b ? s.b : ""); + free(val); + sbuf_free(&s); + return rc; +} + +static int api_settings_get_all(int sock) +{ + sbuf_t s = {0}; + sA(&s, "{\"items\":["); + char groups[16][32]; + int ngroups = 0; + for (int i = 0; i < SETS_COUNT; i++) { + const setdef_t *d = &g_sets[i]; + char *val = settings_current_value(d); + if (i) sA(&s, ","); + settings_item_json(&s, d, val); + free(val); + if (d->group && d->group[0]) { + int found = 0; + for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; } + if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group); + } + } + sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT); + for (int g = 0; g < ngroups; g++) { + if (g) sA(&s, ","); + sA_str(&s, groups[g]); + } + sA(&s, "]}"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_settings_put(int sock, const char *key, const req_t *r) +{ + const setdef_t *d = setdef_find(key); + if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项"); + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + jv_t *val = jv_obj_get(root, "value"); + if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); } + char raw[256]; + switch (val->type) { + case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break; + case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break; + case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break; + default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持"); + } + jv_free(root); + char coerced[256]; + if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { + return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法"); + } + if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败"); + /* boot.default 同步权威文件(同 meow_settings_save) */ + if (!strcmp(key, "boot.default")) { + int v = (!strcmp(coerced, "hiby")) ? 1 : 0; + FILE *bf = fopen(BOOT_DEFAULT_FILE, "w"); + if (bf) { fprintf(bf, "%d\n", v); fclose(bf); } + } + sbuf_t s = {0}; + settings_item_json(&s, d, coerced); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_settings_batch(int sock, const req_t *r) +{ + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); } + sbuf_t ok = {0}, fail = {0}; + int nok = 0, nfail = 0; + for (const jv_t *p = root->child; p; p = p->next) { + const setdef_t *d = setdef_find(p->str); + if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; } + jv_t *val = p->child; + char raw[256]; + switch (val->type) { + case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break; + case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break; + case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break; + default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue; + } + char coerced[256]; + if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { + if (nfail) sA(&fail, ","); + sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}"); + nfail++; + continue; + } + if (settings_write_one(p->str, coerced) == 0) { + if (nok) sA(&ok, ","); + sA_str(&ok, p->str); + nok++; + } else { + if (nfail) sA(&fail, ","); + sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); + nfail++; + } + } + jv_free(root); + sbuf_t s = {0}; + sA(&s, "{\"updated\":["); + if (ok.b) sA(&s, "%s", ok.b); + sA(&s, "],\"failed\":["); + if (fail.b) sA(&s, "%s", fail.b); + sA(&s, "]}"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail); + return rc; +} + +static int api_settings_restore(int sock) +{ + if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败"); + return send_json(sock, 200, "{\"reset\":true}"); +} + +/* ---------------- 主题端点 ---------------- */ +static void theme_json(sbuf_t *s) +{ + const setdef_t *dst = setdef_find("ui.style"); + const setdef_t *dth = setdef_find("display.theme"); + const setdef_t *dbg = setdef_find("display.background"); + char *style = settings_current_value(dst); + char *theme = settings_current_value(dth); + char *bg = settings_current_value(dbg); + /* 当前值 → 中文标签(csv 里同下标查 label) */ + char slabel[128] = "", tlabel[128] = ""; + for (int i = 0; i < csv_count(dst->enum_names); i++) { + char nb[128], lb[128]; + csv_at(dst->enum_names, i, nb, sizeof nb); + csv_at(dst->enum_labels, i, lb, sizeof lb); + if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; } + } + for (int i = 0; i < csv_count(dth->enum_names); i++) { + char nb[128], lb[128]; + csv_at(dth->enum_names, i, nb, sizeof nb); + csv_at(dth->enum_labels, i, lb, sizeof lb); + if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; } + } + sA(s, "{\"style\":"); + sA_str(s, style); + sA(s, ",\"theme\":"); + sA_str(s, theme); + sA(s, ",\"background\":"); + sA_str(s, bg); + sA(s, ",\"style_label\":"); + sA_str(s, slabel); + sA(s, ",\"theme_label\":"); + sA_str(s, tlabel); + sA(s, ",\"style_options\":"); + enum_options_json(s, dst->enum_names, dst->enum_labels); + sA(s, ",\"theme_options\":"); + enum_options_json(s, dth->enum_names, dth->enum_labels); + sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}"); + free(style); free(theme); free(bg); +} + +static int api_theme_get(int sock) +{ + sbuf_t s = {0}; + theme_json(&s); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_theme_put(int sock, const req_t *r) +{ + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + /* 允许 style/theme/background 任意子集 */ + const char *keys[3] = { "style", "theme", "background" }; + const char *setkeys[3] = { "ui.style", "display.theme", "display.background" }; + for (int i = 0; i < 3; i++) { + jv_t *v = jv_obj_get(root, keys[i]); + if (!v) continue; + const setdef_t *d = setdef_find(setkeys[i]); + if (!d) continue; + char raw[256]; + if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str); + else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum); + else continue; + char coerced[256]; + if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); } + settings_write_one(setkeys[i], coerced); + } + jv_free(root); + return api_theme_get(sock); +} + +/* ---------------- 系统信息 / 性能 ---------------- */ +static int api_perf(int sock, const char *path) +{ + char json[4096]; + int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json); + if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); + return send_json(sock, 200, json); +} + +/* 从 meow_svc_perf 聚合 JSON 提取顶层 "network":{...} 段原始文本(含外层 {}), + * 供 /api/system 透传(单一权威源,后端不再各自采集)。找不到顶层 network 返回 -1。 */ +static int perf_extract_network(const char *json, char *out, int cap) +{ + const char *p = json; + for (;;) { + p = strstr(p, "\"network\""); + if (!p) return -1; + const char *q = p + 9; /* strlen("\"network\"") */ + while (*q == ' ' || *q == '\t') q++; + if (*q == ':') { p = q + 1; break; } /* 顶层 network 键 */ + p += 9; /* 嵌套同名键跳过 */ + } + while (*p == ' ' || *p == '\t') p++; + if (*p != '{') return -1; + int depth = 0, in_str = 0, esc = 0; + const char *q; + for (q = p; *q; q++) { + char ch = *q; + if (in_str) { + if (esc) esc = 0; + else if (ch == '\\') esc = 1; + else if (ch == '"') in_str = 0; + continue; + } + if (ch == '"') { in_str = 1; continue; } + if (ch == '{') depth++; + else if (ch == '}') { depth--; if (depth == 0) { q++; break; } } + } + if (depth != 0) return -1; + size_t len = (size_t)(q - p); + if (len == 0) return -1; + if (len >= (size_t)cap) len = (size_t)cap - 1; + memcpy(out, p, len); out[len] = 0; + return 0; +} + +/* /api/system:性能服务 SYSTEM 子集 + boot/ota 局部补全。 + * network 段由 meow_svc_perf 聚合(SYSTEM_FRAMEWORK_SERVICES §4.1 已定案),此处**透传 perf 的 network**, + * 后端不再各自采集 wifi/蓝牙;perf 若缺 network 段才回退 hostname_local(保持不崩)。 */ +static int api_system(int sock) +{ + char json[4096]; + int rc = ipc_perf_query(0, json, sizeof json); + if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)"); + jv_t *root = jv_parse(json, (int)strlen(json)); + if (!root) return send_err(sock, 500, "INTERNAL", "性能服务 JSON 解析失败"); + jv_t *jsys = jv_obj_get(root, "system"); + jv_t *jupt = jv_obj_get(root, "uptime_sec"); + jv_t *jst = jv_obj_get(root, "storage"); + jv_t *jtf = jv_obj_get(root, "tfcard"); + const char *ver = "-", *host = "-", *ker = "-", *arch = "-"; + if (jsys && jsys->type == J_OBJ) { + jv_t *v = jv_obj_get(jsys, "version"); if (v && v->type == J_STR) ver = v->str; + v = jv_obj_get(jsys, "hostname"); if (v && v->type == J_STR) host = v->str; + v = jv_obj_get(jsys, "kernel"); if (v && v->type == J_STR) ker = v->str; + v = jv_obj_get(jsys, "arch"); if (v && v->type == J_STR) arch = v->str; + } + long long upt = jupt ? (long long)jupt->dnum : 0; + /* boot default:读权威 /data/meow_boot_default(0=meow 默认, 1=hiby),失败回退 meow */ + int bdf = -1; + { + char b[16]; + FILE *f = fopen(BOOT_DEFAULT_FILE, "r"); + if (f) { if (fgets(b, sizeof b, f)) bdf = atoi(b); fclose(f); } + } + const char *bootname = bdf < 0 ? "meow" : (bdf ? "hiby" : "meow"); + char host_local[80]; + snprintf(host_local, sizeof host_local, "%s.local", host); + sbuf_t s = {0}; + sA(&s, "{\"version\":"); + sA_str(&s, ver); + sA(&s, ",\"hostname\":"); + sA_str(&s, host); + sA(&s, ",\"kernel\":"); + sA_str(&s, ker); + sA(&s, ",\"arch\":"); + sA_str(&s, arch); + sA(&s, ",\"uptime_sec\":%lld,\"boot\":{\"default\":", upt); + sA_str(&s, bootname); + sA(&s, "},\"network\":"); + char netbuf[1024]; + if (perf_extract_network(json, netbuf, sizeof netbuf) == 0) { + sA(&s, "%s", netbuf); /* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local) */ + } else { + /* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */ + sA(&s, "{\"hostname_local\":"); + sA_str(&s, host_local); + sA(&s, "}"); + } + sA(&s, ",\"storage\":{"); + if (jst && jst->type == J_OBJ) { + jv_t *t = jv_obj_get(jst, "total"); long long tot = t ? (long long)t->dnum : 0; + jv_t *u = jv_obj_get(jst, "used"); long long usd = u ? (long long)u->dnum : 0; + jv_t *a = jv_obj_get(jst, "avail"); long long ava = a ? (long long)a->dnum : 0; + sA(&s, "\"root\":{\"total\":%lld,\"used\":%lld,\"avail\":%lld}", tot, usd, ava); + } else { + sA(&s, "\"root\":{\"total\":0,\"used\":0,\"avail\":0}"); + } + sA(&s, ",\"tfcard\":{"); + if (jtf && jtf->type == J_OBJ) { + jv_t *pr = jv_obj_get(jtf, "present"); int present = pr && pr->bval; + long long tot = 0, usd = 0, ava = 0; + jv_t *t = jv_obj_get(jtf, "total"); if (t) tot = (long long)t->dnum; + jv_t *u = jv_obj_get(jtf, "used"); if (u) usd = (long long)u->dnum; + jv_t *a = jv_obj_get(jtf, "avail"); if (a) ava = (long long)a->dnum; + sA(&s, "\"present\":%s,\"total\":%lld,\"used\":%lld,\"avail\":%lld}", present ? "true" : "false", tot, usd, ava); + } else { + sA(&s, "\"present\":false,\"total\":0,\"used\":0,\"avail\":0}"); + } + sA(&s, "},\"ota\":{\"current\":"); + sA_str(&s, ver); + sA(&s, ",\"latest\":"); + sA_str(&s, ver); + sA(&s, ",\"available\":false,\"url\":null}}"); + jv_free(root); + int ret = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return ret; +} + +/* ---------------- 系统动作(经 meow_svc_power IPC) ---------------- */ +static int api_system_action(int sock, const char *which, const req_t *r) +{ + (void)r; + uint32_t cmd; + if (!strcmp(which, "reboot")) cmd = POWER_REBOOT; + else if (!strcmp(which, "shutdown")) cmd = POWER_SHUTDOWN; + else if (!strcmp(which, "ota")) cmd = POWER_UPGRADE; + else return send_err(sock, 404, "NOT_FOUND", "未知系统动作"); + m_power_status_t st; + memset(&st, 0, sizeof st); + int rc = ipc_power_action(cmd, 0, NULL, &st); + if (rc == MEOW_OK) return send_json(sock, 200, "{\"accepted\":true}"); + /* 系统动作类服务端通常「先应答受理再执行」;此处若返回非 OK 则说明服务未受理 */ + const char *why = st.msg[0] ? st.msg : power_code_str(rc); + if (rc == MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", "电源服务拒绝(非 root 调用)"); + if (rc == MEOW_ERR_NODEV) return send_err(sock, 503, "NO_CARD", "升级文件缺失或服务不可达"); + if (rc == MEOW_ERR_NOENT || rc == MEOW_ERR_IO) return send_err(sock, 503, "NO_CARD", why); + return send_err(sock, 500, "INTERNAL", why); +} + +/* ---------------- 远程控制(按键注入) ---------------- */ +static int api_key_single(int sock, int code, const req_t *r) +{ + int press_ms = 0; + char pressbuf[16] = "tap"; + const char *press = pressbuf; + if (r->body && r->body[0]) { + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (root) { + jv_t *p = jv_obj_get(root, "press"); + if (p && p->type == J_STR) snprintf(pressbuf, sizeof pressbuf, "%s", p->str); + jv_t *m = jv_obj_get(root, "ms"); + if (m && m->type == J_NUM) press_ms = (int)m->inum; + jv_free(root); + } + } + if (!strcmp(press, "hold")) + return send_err(sock, 501, "NOT_IMPLEMENTED", "hold 持续注入需常驻通道(阶段 D),后端未实现"); + if (strcmp(press, "tap") && strcmp(press, "long")) + return send_err(sock, 400, "BAD_REQUEST", "press 非法"); + if (press_ms <= 0) { + if (!strcmp(press, "long")) press_ms = 3000; + } + /* 物理键红线:禁止映射 返回(158)/主页(172)/确认/exit(APPS.md)。 */ + if (code == 158 || code == 172 || code == 32 || code == 28 || code == 96) + return send_err(sock, 403, "FORBIDDEN", "该键码(返回/主页/确认)按物理键红线禁止映射"); + const char *tmpdev = NULL; + int dispatchable = (keycode_to_device(code, &tmpdev) >= 0); + if (!dispatchable) + return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/电源/播放/上下曲)"); + if (inject_key_release(code, press_ms) < 0) + return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)"); + sbuf_t s = {0}; + sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press); + int ret = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return ret; +} + +static int api_key_batch(int sock, const req_t *r) +{ + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + jv_t *keys = jv_obj_get(root, "keys"); + if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); } + sbuf_t s = {0}; + int first = 1; + for (const jv_t *k = keys->child; k; k = k->next) { + if (k->type != J_OBJ) continue; + jv_t *c = jv_obj_get(k, "code"); + jv_t *p = jv_obj_get(k, "press"); + jv_t *m = jv_obj_get(k, "ms"); + if (!c || c->type != J_NUM) continue; + int code = (int)c->inum; + if (code == 158 || code == 172) continue; /* 红线跳过 */ + int press_ms = (m && m->type == J_NUM) ? (int)m->inum : 0; + if (p && p->type == J_STR && !strcmp(p->str, "long") && press_ms <= 0) press_ms = 3000; + const char *tmpdev = NULL; + if (keycode_to_device(code, &tmpdev) < 0) continue; + if (inject_key_release(code, press_ms) < 0) continue; + if (!first) sA(&s, ","); + first = 0; + sA(&s, "%d", code); + } + jv_free(root); + sbuf_t o = {0}; + if (first) sA(&o, "{\"dispatched\":[]}"); + else sA(&o, "{\"dispatched\":[%s]}", s.b); + int ret = send_json(sock, 200, o.b ? o.b : ""); + sbuf_free(&s); sbuf_free(&o); + return ret; +} + +/* ---------------- 日志 ---------------- */ +static int api_logs_list(int sock) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + DIR *d = opendir(LOGS_DIR); + if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在"); + sbuf_t s = {0}; + sA(&s, "{\"dir\":\"" LOGS_DIR "\",\"files\":["); + int first = 1; + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + if (is_sensitive_name(e->d_name)) continue; + char full[1024]; + snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name); + struct stat st; + if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue; + if (!first) sA(&s, ","); + first = 0; + sA(&s, "{\"name\":"); + sA_str(&s, e->d_name); + sA(&s, ",\"size\":%lld,\"mtime\":%lld}", (long long)st.st_size, (long long)st.st_mtime); + } + closedir(d); + sA(&s, "]}"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_logs_read(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + char file[512], lines[16]; + if (!qparam(r->query, "file", file, sizeof file)) + return send_err(sock, 400, "BAD_REQUEST", "缺 file 参数"); + int maxlines = 200; + if (qparam(r->query, "lines", lines, sizeof lines)) { + int n = atoi(lines); + if (n > 0) maxlines = n; + if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES; + } + /* 防逃逸:file 相对 LOGS_DIR,禁止 .. 与绝对路径 */ + if (file[0] == '/' || strstr(file, "..")) return send_err(sock, 403, "FORBIDDEN", "文件路径逃逸"); + if (is_sensitive_name(strrchr(file, '/') ? strrchr(file, '/') + 1 : file)) + return send_err(sock, 403, "FORBIDDEN", "敏感文件"); + char full[1024]; + snprintf(full, sizeof full, "%s/%s", LOGS_DIR, file); + int fd = open(full, O_RDONLY); + if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "日志文件不存在"); + struct stat st; + if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); } + /* 逐行读,保留末尾 maxlines 行 */ + const char *buf = malloc((size_t)st.st_size + 1); + if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); } + ssize_t n = read(fd, (char *)buf, (size_t)st.st_size); + close(fd); + if (n < 0) { free((void *)buf); return send_err(sock, 500, "INTERNAL", "读日志失败"); } + sbuf_t s = {0}; + sA(&s, "{\"file\":"); + sA_str(&s, file); + sA(&s, ",\"lines\":["); + /* 直接扫一遍,保留末尾 maxlines 行文本 */ + const char *p = buf; + const char *end = buf + n; + int total = 0, truncated = 0; + int gotlines = 0; + int linecnt = 0; + for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++; + if (linecnt > 0 && end[-1] != '\n') linecnt++; + int startrow = linecnt - maxlines; + if (startrow < 0) startrow = 0; + if (startrow > 0) truncated = 1; + int rowidx = 0; + p = buf; + while (p < end) { + const char *nl = memchr(p, '\n', (size_t)(end - p)); + const char *line_end = nl ? nl : end; + if (rowidx >= startrow) { + if (gotlines) sA(&s, ","); + sbuf_t ln = {0}; + for (const char *c = p; c < line_end; c++) { + unsigned char cc = (unsigned char)*c; + if (cc == '"' || cc == '\\') sA(&ln, "\\%c", (int)cc); + else if (cc < 0x20 && cc != '\t') sA(&ln, "\\u%04x", cc); + else sA(&ln, "%c", (int)cc); + } + /* 用 sA_str(任意长度安全);不要 sA("%s")——单次超 511B 会截断坏 JSON */ + sA(&s, "\""); + for (const char *c = ln.b ? ln.b : ""; *c; c++) sA(&s, "%c", (int)(unsigned char)*c); + sA(&s, "\""); + sbuf_free(&ln); + gotlines++; + } + rowidx++; + if (!nl) break; + p = nl + 1; + } + total = linecnt; + free((void *)buf); + sA(&s, "],\"total\":%d,\"truncated\":%s}", total, truncated ? "true" : "false"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +/* ---------------- 通知(后端跨进程不可读 → 501) ---------------- */ +/* 注:通知存储为 launcher 进程内存(meow_notif.c g_slots[],NOTIFICATION_SYSTEM §4), + * 后端为独立 svcd 子进程,跨进程无法读取/清撤那批内存槽;框架仅在关机前把通知 dump 到 + * TF 卡 MeowOS/notifications/notifications.log(CSV 追加,非活跃快照)。故本后端无法实现 + * 活跃通知列表/清撤 —— 需为通知后端另开 IPC 查询(如 MSG_NOTIFY_LIST 应答)或持久化活跃集, + * 见 MEOWPANEL_PROTOCOL §6 待定案。当前一律 501。 */ +static int api_notifications(int sock) +{ + return send_err(sock, 501, "NOT_IMPLEMENTED", "通知存储为 launcher 进程内存,后端跨进程不可读(需通知后端暴露 IPC 查询/清撤)"); +} +static int api_notification_delete(int sock) +{ + return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清撤需 launcher 通知后端 IPC,后端未实现"); +} +static int api_notification_dismiss_all(int sock) +{ + return send_err(sock, 501, "NOT_IMPLEMENTED", "通知清空需 launcher 通知后端 IPC,后端未实现"); +} + +/* ---------------- 文件操作 ---------------- */ +static int api_files_list(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + char path[1024] = "/"; + if (r->query[0]) qparam(r->query, "path", path, sizeof path); + return files_list(sock, path); +} + +static int api_files_read(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + char path[1024]; + if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); + char abs[1024]; + if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); + long long size; int trunc; + char *txt = read_text_file_limited(abs, &size, &trunc); + if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取"); + if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大"); + sbuf_t s = {0}; + sA(&s, "{\"path\":"); + sA_str(&s, abs); + sA(&s, ",\"content\":"); + sA_str(&s, txt); + sA(&s, ",\"size\":%lld,\"truncated\":false}", size); + free(txt); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_files_download(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + char path[1024]; + if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); + char abs[1024]; + if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); + const char *base = strrchr(abs, '/'); + base = base ? base + 1 : abs; + if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件"); + return send_file(sock, 200, "application/octet-stream", abs, base); +} + +static int api_files_upload(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) + return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data"); + upfile_t files[16]; + memset(files, 0, sizeof files); + int n = parse_multipart(r, files, 16); + if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败"); + sbuf_t s = {0}; + sA(&s, "{\"item\":["); + int first = 1; + for (int i = 0; i < n && i < 16; i++) { + if (!files[i].filename[0]) continue; + if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */ + struct stat st; + if (stat(files[i].path, &st) < 0) continue; + const char *base = strrchr(files[i].path, '/'); + base = base ? base + 1 : files[i].path; + if (!first) sA(&s, ","); + first = 0; + sA(&s, "{\"name\":"); + sA_str(&s, base); + sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}", + (long long)st.st_size, (long long)st.st_mtime); + } + sA(&s, "]}"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +/* 递归删除目录/文件。 */ +static int rm_recursive(const char *path) +{ + struct stat st; + if (lstat(path, &st) < 0) return -1; + if (S_ISDIR(st.st_mode)) { + DIR *d = opendir(path); + if (!d) return -1; + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + char full[1024]; + snprintf(full, sizeof full, "%s/%s", path, e->d_name); + rm_recursive(full); + } + closedir(d); + return rmdir(path); + } + return unlink(path); +} + +static int api_files_mkdir(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + jv_t *pj = jv_obj_get(root, "path"); + if (!pj || pj->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); } + char abs[1024]; + if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); } + struct stat st; + if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); } + if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); } + const char *base = strrchr(abs, '/'); + const char *b = base ? base + 1 : abs; + sbuf_t s = {0}; + sA(&s, "{\"item\":{\"name\":"); + sA_str(&s, b); + sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL)); + jv_free(root); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_files_rename(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body"); + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + jv_t *pj = jv_obj_get(root, "path"); + jv_t *pnn = jv_obj_get(root, "new_name"); + if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); } + if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); } + char abs[1024], newabs[1400]; + if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); } + snprintf(newabs, sizeof newabs, "%s/%s", abs, pnn->str); + /* newabs 也须在白名单(new_name 不含 /,父目录已校验,无需再 normalize) */ + if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); } + sbuf_t s = {0}; + sA(&s, "{\"item\":{\"name\":"); + sA_str(&s, pnn->str); + sA(&s, ",\"type\":\""); + struct stat st; + if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file"); + sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime); + jv_free(root); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_files_delete(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + char path[1024]; + if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path"); + char recursive[8] = "0"; + qparam(r->query, "recursive", recursive, sizeof recursive); + char abs[1024]; + if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); + if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根"); + struct stat st; + if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在"); + if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) { + DIR *d = opendir(abs); + int nonempty = 0; + if (d) { + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + nonempty = 1; break; + } + closedir(d); + } + if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)"); + } + if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败"); + return send_json(sock, 200, "{\"removed\":true}"); +} + +/* ---------------- 应用(MeowAPPs 扫描 + nyai) ---------------- */ +/* 执行命令(sh -c),捕获 stdout(上限 cap-1),返回退出状态(-1=无法启动)。 */ +static int run_cmd(const char *cmd, char *out, int outcap) +{ + FILE *f = popen(cmd, "r"); + if (!f) return -1; + size_t off = 0; + while (off < (size_t)outcap - 1) { + size_t rd = fread(out + off, 1, (size_t)outcap - 1 - off, f); + if (rd == 0) break; + off += rd; + } + out[off] = 0; + int st = pclose(f); + int code = -1; + if (WIFEXITED(st)) code = WEXITSTATUS(st); + return code; +} + +static int api_apps_list(int sock) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + DIR *d = opendir(MEOWAPPS_DIR); + if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在"); + sbuf_t s = {0}; + sA(&s, "{\"count\":0,\"apps\":["); + int first = 1, count = 0; + struct dirent *e; + while ((e = readdir(d))) { + if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; + if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue; + if (strstr(e->d_name, ".bak")) continue; + char full[1024]; + snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name); + struct stat st; + if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue; + if (!first) sA(&s, ","); + first = 0; + apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0); + count++; + } + closedir(d); + sA(&s, "]}"); + /* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */ + /* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */ + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + (void)count; + return rc; +} + +static int api_apps_info(int sock, const char *pkg) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); + char appdir[1024]; + snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg); + struct stat st; + if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装"); + sbuf_t s = {0}; + if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0) + return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +/* multipart 提取第一个 file part 并写入 outpath。返回 0=成功。 */ +static int multipart_extract_file(const req_t *r, const char *outpath) +{ + const char *b = strstr(r->content_type, "boundary="); + if (!b) return -1; + b += 9; + if (*b == '"') b++; + char boundary[128]; int bi = 0; + while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; } + boundary[bi] = 0; + char dmark[160]; + snprintf(dmark, sizeof dmark, "--%s", boundary); + const char *p = r->body; + while (p < r->body + r->body_len) { + const char *fs = strstr(p, dmark); + if (!fs) break; + fs += strlen(dmark); + if (*fs == '-') break; + if (*fs == '\r' || *fs == '\n') fs++; + const char *he = strstr(fs, "\r\n\r\n"); + if (!he) break; + char hdr[512]; + size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1; + memcpy(hdr, fs, hl); hdr[hl] = 0; + int isfile = 0; + char *save = NULL; + for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) { + char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++; + if (strncasecmp(l, "Content-Disposition:", 20) == 0) { + if (strstr(l, "name=\"file\"")) isfile = 1; + } + } + const char *ds = he + 4; + const char *de = strstr(ds, dmark); + if (!de) de = r->body + r->body_len; + while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--; + if (isfile) { + int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd < 0) return -1; + write_full(fd, ds, (size_t)(de - ds)); + close(fd); + return 0; + } + p = de; + } + return -1; +} + +static int api_apps_install(int sock, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (!access(NYAI_PATH, X_OK)) { /* ok */ } + char mpkpath[1024]; + int ispath = 0; + if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) { + /* JSON {path} */ + if (r->body && r->body[0]) { + jv_t *root = jv_parse(r->body, (int)r->body_len); + if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON"); + jv_t *pj = jv_obj_get(root, "path"); + if (pj && pj->type == J_STR) { + snprintf(mpkpath, sizeof mpkpath, "%s", pj->str); + ispath = 1; + } + jv_free(root); + } + if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file"); + } else { + /* 保存上传 file 到临时文件 */ + snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR); + mkdir(mpkpath, 0777); + char tmp[1024]; + snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR); + if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败"); + snprintf(mpkpath, sizeof mpkpath, "%s", tmp); + } + if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在"); + char cmd[1400]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", NYAI_PATH, mpkpath); + char out[4096]; + int code = run_cmd(cmd, out, sizeof out); + if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败"); + /* 包名 = 文件名茎;版本从 app.json 读 */ + char stem[256]; + const char *base = strrchr(mpkpath, '/'); + base = base ? base + 1 : mpkpath; + snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base); + char *dot = strrchr(stem, '.'); + if (dot) *dot = 0; + char appdir[1024], pj[1100]; + snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem); + jv_t *approot = NULL; + char version[16] = ""; + int upgraded = 0; + snprintf(pj, sizeof pj, "%s/app.json", appdir); + char jbuf[8192]; + if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0) + approot = jv_parse(jbuf, (int)strlen(jbuf)); + if (approot) { + jv_t *v = jv_obj_get(approot, "version"); + if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str); + jv_free(approot); + } + if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1; + sbuf_t s = {0}; + sA(&s, "{\"package\":"); + sA_str(&s, stem); + sA(&s, ",\"version\":"); + sA_str(&s, version[0] ? version : NULL); + sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +static int api_apps_export(int sock, const char *pkg) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); + char tmp[1100]; + mkdir(TF_CARD_DIR "/tmp", 0777); + snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg); + unlink(tmp); + char cmd[1400]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", NYAI_PATH, pkg, tmp); + char out[1024]; + int code = run_cmd(cmd, out, sizeof out); + if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败"); + char dn[256]; + snprintf(dn, sizeof dn, "%s.mpk", pkg); + return send_file(sock, 200, "application/octet-stream", tmp, dn); +} + +static int api_apps_remove(int sock, const char *pkg, const req_t *r) +{ + if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡"); + if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法"); + char purgebuf[8] = "0"; + if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf); + int use_purge = atoi(purgebuf); + char cmd[1400]; + snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", NYAI_PATH, pkg, use_purge ? " --purge" : ""); + char out[1024]; + int code = run_cmd(cmd, out, sizeof out); + if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败"); + sbuf_t s = {0}; + sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false"); + int rc = send_json(sock, 200, s.b ? s.b : ""); + sbuf_free(&s); + return rc; +} + +/* ---------------- 静态文件托管(web_root = 二进制所在目录) ---------------- */ +static char g_webroot[512]; +static const char *web_root(void) +{ + if (g_webroot[0]) return g_webroot; + char exe[512]; + ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1); + if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; } + exe[n] = 0; + char *slash = strrchr(exe, '/'); + if (slash) *slash = 0; + snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : "."); + return g_webroot; +} + +static const char *mime_for(const char *path) +{ + const char *ext = strrchr(path, '.'); + if (!ext) return "application/octet-stream"; + if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8"; + if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8"; + if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8"; + if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8"; + if (!strcasecmp(ext, ".png")) return "image/png"; + if (!strcasecmp(ext, ".ico")) return "image/x-icon"; + if (!strcasecmp(ext, ".svg")) return "image/svg+xml"; + return "application/octet-stream"; +} + +static int serve_static(int sock, const char *path) +{ + const char *root = web_root(); + char rel[512]; + /* / → index.html;/assets/ → ;/ → (meowpanel.css/js 等) */ + if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html"); + else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8); + else snprintf(rel, sizeof rel, "%s", path + 1); + if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸"); + /* 只允许安全文件(防读到二进制/任意文件) */ + char full[1024]; + snprintf(full, sizeof full, "%s/%s", root, rel); + /* 敏感名拒绝 */ + const char *base = strrchr(full, '/'); + base = base ? base + 1 : full; + if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行文件"); + FILE *f = fopen(full, "rb"); + if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在"); + fclose(f); + return send_file(sock, 200, mime_for(full), full, NULL); +} + +/* ---------------- 路由分发 ---------------- */ +static int handle(int sock, req_t *r) +{ + const char *p = r->path; + if (strncmp(p, "/api/", 5) != 0) { + if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0) + return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET"); + return serve_static(sock, p); + } + + /* ---- 性能/状态 ---- */ + if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf"))) + return api_perf(sock, p); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system")) + return api_system(sock); + + /* ---- 系统动作 ---- */ + if (!strcmp(r->method, "POST")) { + if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r); + if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r); + if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r); + if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r); + if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock); + if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r); + if (!strcmp(p, "/api/files/upload")) return api_files_upload(sock, r); + if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r); + if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r); + if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r); + if (!strcmp(p, "/api/notifications/dismiss_all")) return api_notification_dismiss_all(sock); + if (!strcmp(p, "/api/key")) return api_key_batch(sock, r); + char seg[256]; + if (path_segment("/api/key/", p, seg, sizeof seg)) + return api_key_single(sock, atoi(seg), r); + } + + /* ---- 设置 ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock); + if (!strcmp(r->method, "PUT")) { + char seg[256]; + if (path_segment("/api/settings/", p, seg, sizeof seg)) + return api_settings_put(sock, seg, r); + } + if (!strcmp(r->method, "GET")) { + char seg[256]; + if (path_segment("/api/settings/", p, seg, sizeof seg)) + return api_settings_get_any(sock, seg); + } + + /* ---- 文件 ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r); + if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r); + + /* ---- 应用 ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock); + if (!strcmp(r->method, "GET")) { + char seg[256]; + if (path_segment("/api/apps/", p, seg, sizeof seg)) { + /* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */ + size_t pl = strlen(p); + int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export")); + if (is_export) return api_apps_export(sock, seg); + return api_apps_info(sock, seg); + } + } + if (!strcmp(r->method, "DELETE")) { + char seg[256]; + if (path_segment("/api/apps/", p, seg, sizeof seg)) + return api_apps_remove(sock, seg, r); + } + + /* ---- 日志 ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock); + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r); + + /* ---- 通知 ---- */ + if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock); + if (!strcmp(r->method, "DELETE")) { + char seg[256]; + if (path_segment("/api/notifications/", p, seg, sizeof seg)) + return api_notification_delete(sock); + } + + return send_err(sock, 404, "NOT_FOUND", "端点不存在"); +} + +/* ---------------- 主循环 ---------------- */ +static int g_port = DEFAULT_PORT; + +static int read_port_config(void) +{ + /* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */ + char path[1024]; + snprintf(path, sizeof path, "%s/meowpanel.conf", web_root()); + FILE *f = fopen(path, "r"); + if (f) { + char line[160]; + while (fgets(line, sizeof line, f)) { + char *eq = strchr(line, '='); + if (!eq) continue; + char *k = line; while (*k == ' ' || *k == '\t') k++; + if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1); + } + fclose(f); + } + const char *env = getenv("MEOWPORT"); + if (env && env[0]) g_port = atoi(env); + if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; + return g_port; +} + +static void handle_conn(int sock) +{ + req_t r; + memset(&r, 0, sizeof r); + if (read_request(sock, &r) != 0) { close(sock); return; } + handle(sock, &r); + req_free(&r); + close(sock); +} + +int main(int argc, char **argv) +{ + /* 参数:-p PORT(可选)。默认端口 8080。 */ + int port = DEFAULT_PORT; + for (int i = 1; i < argc; i++) { + if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]); + else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i]; + else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i]; + else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; } + else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { + printf("MeowPanel backend %s\n", PANEL_VERSION); + printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P]\n"); + return 0; + } + } + const char *env = getenv("MEOW_IPC_PERF_SOCK"); + if (env && env[0]) g_sock_perf = env; + env = getenv("MEOW_IPC_POWER_SOCK"); + if (env && env[0]) g_sock_power = env; + + (void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */ + /* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */ + if (port != DEFAULT_PORT) g_port = port; + else g_port = read_port_config(); + /* 再按 env MEOWPORT 覆盖 conf(-p 最高) */ + if (port == DEFAULT_PORT) { + const char *ep = getenv("MEOWPORT"); + if (ep && ep[0]) g_port = atoi(ep); + } + if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT; + + signal(SIGPIPE, SIG_IGN); + web_root(); + panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root()); + + int lfd = socket(AF_INET, SOCK_STREAM, 0); + if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; } + int on = 1; + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on); + struct sockaddr_in addr; + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons((uint16_t)g_port); + if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) { + panel_log("bind :%d fail: %s", g_port, strerror(errno)); + close(lfd); + return 1; + } + if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; } + + for (;;) { + int c = accept(lfd, NULL, NULL); + if (c < 0) { if (errno == EINTR) continue; continue; } + handle_conn(c); + } + return 0; +} diff --git a/patch_v2/meowpanel/services.d/meowpanel.conf b/patch_v2/meowpanel/services.d/meowpanel.conf new file mode 100644 index 0000000..8ca1bdf --- /dev/null +++ b/patch_v2/meowpanel/services.d/meowpanel.conf @@ -0,0 +1,18 @@ +# meowpanel.conf — MeowPanel 后端服务配置(外置 TF 卡,meow_svcd 托管)。 +# 部署位置:TF 卡 `/usr/data/mnt/sd_0/MeowOS/services.d/meowpanel.conf` +# (svcd 双源:板载 /usr/data/meow/services.d + TF MeowOS/services.d,同名板载优先;无卡跳过该服务)。 +# 字段对齐 meow_svcd 服务配置样例(SYSTEM_FRAMEWORK_SERVICES §1.3 / meow_svcd.md)。 +# +# 定位:MEOWPANEL_DESIGN §1/§2 —— MeowPanel 是**外置系统组件**,整体走卡(MeowOS/opt/MeowPanel), +# 不进固件;无卡时 svcd 跳过本服务,系统核心(launcher/sdk/设置/通知)绝不对它依赖。 +# +# 注意:本服务在卡上,端口监听 8080(可用 -p / MEOWPORT / 同目录 meowpanel.conf 的 port=N 覆盖)。 + +exec=/usr/data/mnt/sd_0/MeowOS/opt/MeowPanel/meowpanel +args= +enabled=1 +restart=always +backoff_ms=2000 +max_restart=5 +trigger=boot +cap= diff --git a/patch_v2/meowpanel/web/index.html b/patch_v2/meowpanel/web/index.html new file mode 100644 index 0000000..b2c7b73 --- /dev/null +++ b/patch_v2/meowpanel/web/index.html @@ -0,0 +1,107 @@ + + + + + + +MeowPanel · MeowOS 面板 + + + + + + + +
+ + + + + +
+
+ +

实时性能

+
+ + 检测中… + +
+ + +
+
+ + +
+
+
+ +
+

加载中…

+
+
+
+ + + + + + + + + diff --git a/patch_v2/meowpanel/web/meowpanel.css b/patch_v2/meowpanel/web/meowpanel.css new file mode 100644 index 0000000..a5ce810 --- /dev/null +++ b/patch_v2/meowpanel/web/meowpanel.css @@ -0,0 +1,311 @@ +/* MeowPanel — 前端样式 + * 主题模型:风格族(ui.style: md3 / miuix)× 明暗(display.theme: dark / light) + * 双层变量: + * [data-style] 定形状(radius / spacing / outline 宽)+ 主色(--primary)——对齐 SDK §8.1.3 + * [data-theme] 定中性色 / 语义色(bg/surface/on-surface/outline/error/...) + * 默认(:root 无覆盖)= MD3 dark;MD3 中立形状再显式声明便于对照。 + */ + +/* ---------- 结构基线 ---------- */ +:root{ + --space:8px; + --radius-s:8px; /* 小:chip/内嵌 */ + --radius-m:12px; /* 中:按钮/输入 */ + --radius-l:16px; /* 大:卡片 */ + --outline-w:0px; /* MD3 无描边 */ + --nav-w:212px; + --topbar-h:56px; + --font: -apple-system, "Segoe UI", "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; + --mono: ui-monospace, "SFMono-Regular", "Cascadia Mono", Consolas, monospace; + --speed:.16s; +} + +/* 风格族:MIUIx 更大圆角 / 略大间距 / 细描边 / 偏品牌主色(更饱和) */ +:root[data-style="miuix"]{ + --space:10px; + --radius-s:12px; + --radius-m:18px; /* 按钮 ≈18px */ + --radius-l:24px; /* 卡片 ≈24px */ + --outline-w:1px; /* 细描边 */ +} + +/* ---------- 深色(默认主深色) ---------- */ +:root[data-theme="dark"]{ + --bg:#0a0a0a; /* 背景底(黑)*/ + --surface:#141414; + --surface-variant:#1e1e1e; + --surface-dim:#000000; + --on-surface:#ececec; + --on-surface-variant:#a3a3a3; + --outline:#3a3a3a; + --outline-variant:#2a2a2a; + --error:#cf6679; --on-error:#0a0a0a; + --success:#4caf50; --warning:#ffb300; + --shadow:0 2px 10px rgba(0,0,0,.5); + --scrim:rgba(0,0,0,.55); + --hover:rgba(255,255,255,.06); +} +/* 深色 × MIUIx 主色更饱和 */ +:root[data-theme="dark"][data-style="md3"]{ --primary:#4a8bf5; --on-primary:#fff; } +:root[data-theme="dark"][data-style="miuix"]{--primary:#ff7a00; --on-primary:#fff; } + +/* ---------- 浅色 ---------- */ +:root[data-theme="light"]{ + --bg:#f4f5f7; + --surface:#ffffff; + --surface-variant:#ececee; + --surface-dim:#e1e1e4; + --on-surface:#1b1b1d; + --on-surface-variant:#5c5c60; + --outline:#d2d2d6; + --outline-variant:#e4e4e8; + --error:#ba1a1a; --on-error:#ffffff; + --success:#2e7d32; --warning:#ed6c02; + --shadow:0 2px 10px rgba(0,0,0,.12); + --scrim:rgba(0,0,0,.35); + --hover:rgba(0,0,0,.05); +} +:root[data-theme="light"][data-style="md3"]{ --primary:#2f6fe0; --on-primary:#fff; } +:root[data-theme="light"][data-style="miuix"]{--primary:#f57200; --on-primary:#fff; } + +/* ---------- 布局 ---------- */ +*{ box-sizing:border-box; } +html,body{ height:100%; margin:0; } +body{ + background:var(--bg); color:var(--on-surface); + font-family:var(--font); font-size:14px; line-height:1.5; + -webkit-font-smoothing:antialiased; +} +.app{ display:flex; height:100vh; overflow:hidden; } + +/* 侧栏 */ +.sidebar{ + width:var(--nav-w); flex:0 0 var(--nav-w); + background:var(--surface); border-right:1px solid var(--outline-variant); + display:flex; flex-direction:column; transition:margin-left var(--speed) ease; +} +.brand{ display:flex; align-items:center; gap:10px; padding:16px 18px; } +.brand-mark{ + width:30px; height:30px; border-radius:var(--radius-m); + background:var(--primary); color:var(--on-primary); + display:flex; align-items:center; justify-content:center; font-weight:700; +} +.brand-name{ font-weight:600; font-size:15px; } +.nav{ flex:1; overflow-y:auto; padding:8px 10px; display:flex; flex-direction:column; gap:2px; } +.nav-item{ + display:flex; align-items:center; gap:12px; padding:10px 12px; + border:none; background:transparent; color:var(--on-surface-variant); + border-radius:var(--radius-m); cursor:pointer; text-align:left; + font:inherit; font-size:14px; transition:background var(--speed), color var(--speed); +} +.nav-item:hover{ background:var(--hover); color:var(--on-surface); } +.nav-item.active{ background:var(--primary); color:var(--on-primary); } +.nav-ico{ width:22px; height:22px; flex:0 0 22px; } +.nav-ico svg{ width:100%; height:100%; display:block; } +.nav-label{ white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } + +/* 主区 */ +.main{ flex:1; display:flex; flex-direction:column; min-width:0; } +.topbar{ + height:var(--topbar-h); flex:0 0 var(--topbar-h); + display:flex; align-items:center; gap:12px; padding:0 16px; + background:var(--surface); border-bottom:1px solid var(--outline-variant); +} +.topbar-title{ font-size:17px; font-weight:600; margin:0; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } +.icon-btn{ + width:38px; height:38px; border:none; border-radius:var(--radius-m); + background:transparent; color:var(--on-surface-variant); cursor:pointer; + display:flex; align-items:center; justify-content:center; +} +.icon-btn:hover{ background:var(--hover); color:var(--on-surface); } +.icon-btn svg{ width:22px; height:22px; } +.menu-btn{ display:none; } + +/* 顶栏分段控件(风格/明暗) */ +.seg{ display:inline-flex; background:var(--surface-variant); border-radius:var(--radius-m); padding:3px; } +.seg-btn{ + border:none; background:transparent; color:var(--on-surface-variant); + padding:6px 12px; border-radius:calc(var(--radius-m) - 2px); + cursor:pointer; font:inherit; font-size:13px; white-space:nowrap; + transition:background var(--speed), color var(--speed); +} +.seg-btn.active{ background:var(--surface); color:var(--on-surface); box-shadow:var(--shadow); } + +/* 连接状态点 */ +.conn{ display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:var(--radius-m); background:var(--surface-variant); color:var(--on-surface-variant); font-size:12px; } +.conn-dot{ width:9px; height:9px; border-radius:50%; background:var(--on-surface-variant); } +.conn[data-state="ok"] .conn-dot{ background:var(--success); } +.conn[data-state="bad"] .conn-dot{ background:var(--error); } +.conn[data-state="ok"]{ color:var(--success); } +.conn[data-state="bad"]{ color:var(--error); } + +/* 内容区 */ +.content{ flex:1; overflow-y:auto; padding:20px; } +.page{ max-width:1100px; margin:0 auto; } +.placeholder{ color:var(--on-surface-variant); text-align:center; padding:60px 0; } + +/* 卡片 */ +.card{ + background:var(--surface); border-radius:var(--radius-l); + border:var(--outline-w) solid var(--outline); /* MD3 无描边;MIUIx 1px */ + box-shadow:var(--shadow); padding:18px 20px; margin-bottom:16px; + overflow-x:auto; /* 窄屏表格/内容横向滚动,避免整页外溢 */ +} +.card-title{ font-size:15px; font-weight:600; margin:0 0 14px; } + +/* 统计格子(性能页) */ +.stat-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:14px; } +.stat{ + background:var(--surface-variant); border-radius:var(--radius-m); + padding:14px 16px; border:var(--outline-w) solid var(--outline-variant); +} +.stat-label{ color:var(--on-surface-variant); font-size:12px; margin-bottom:6px; } +.stat-value{ font-size:22px; font-weight:700; letter-spacing:.3px; } +.stat-unit{ font-size:13px; font-weight:400; color:var(--on-surface-variant); margin-left:4px; } +.stat-sub{ font-size:12px; color:var(--on-surface-variant); margin-top:6px; } + +/* 进度条 */ +.bar{ height:8px; border-radius:99px; background:var(--surface-dim); overflow:hidden; margin-top:8px; } +.bar > span{ display:block; height:100%; border-radius:99px; background:var(--primary); transition:width .4s ease; } +.bar.warn > span{ background:var(--warning); } +.bar.danger > span{ background:var(--error); } + +/* 按钮 */ +.btn{ + border:none; padding:9px 16px; border-radius:var(--radius-m); + background:var(--surface-variant); color:var(--on-surface); + cursor:pointer; font:inherit; font-size:14px; + display:inline-flex; align-items:center; gap:6px; + transition:filter var(--speed), background var(--speed), transform .05s; +} +.btn:hover{ filter:brightness(1.08); } +.btn:active{ transform:scale(.98); } +.btn:disabled{ opacity:.5; cursor:not-allowed; } +.btn-primary{ background:var(--primary); color:var(--on-primary); } +.btn-danger{ background:var(--error); color:var(--on-error); } +.btn-ghost{ background:transparent; } +.icon-txt{ font-size:16px; } + +/* 表格(文件/日志) */ +.table{ width:100%; border-collapse:collapse; } +.table th, .table td{ text-align:left; padding:10px 12px; border-bottom:1px solid var(--outline-variant); } +.table th{ color:var(--on-surface-variant); font-weight:500; font-size:12px; } +.table tr:hover td{ background:var(--hover); } +.row-actions{ display:flex; gap:6px; justify-content:flex-end; } +.muted{ color:var(--on-surface-variant); font-size:12px; } +.mono{ font-family:var(--mono); font-size:12px; word-break:break-all; } + +/* 面包屑 */ +.crumbs{ display:flex; flex-wrap:wrap; align-items:center; gap:4px; margin-bottom:12px; } +.crumbs a{ color:var(--primary); cursor:pointer; text-decoration:none; } +.crumbs a:hover{ text-decoration:underline; } + +/* 表单行 */ +.field{ margin-bottom:16px; } +.field-label{ display:block; margin-bottom:6px; color:var(--on-surface-variant); font-size:13px; } +.field-row{ display:flex; align-items:center; gap:12px; } +.field-row.wrap{ flex-wrap:wrap; } +input[type=text], input[type=number], select, textarea{ + width:100%; padding:9px 12px; border-radius:var(--radius-m); + border:var(--outline-w) solid var(--outline); background:var(--surface-variant); + color:var(--on-surface); font:inherit; font-size:14px; border:0; +} +textarea{ resize:vertical; font-family:var(--mono); font-size:12px; } +input[type=range]{ width:100%; accent-color:var(--primary); } +.field-row input[type=range]{ flex:1; min-width:0; } + +/* 开关 */ +.switch{ position:relative; width:46px; height:26px; flex:0 0 46px; } +.switch input{ position:absolute; opacity:0; inset:0; margin:0; cursor:pointer; } +.switch .track{ position:absolute; inset:0; border-radius:99px; background:var(--surface-dim); transition:background var(--speed); border:var(--outline-w) solid var(--outline-variant); } +.switch .thumb{ position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:var(--surface); box-shadow:var(--shadow); transition:left var(--speed); } +.switch input:checked + .track{ background:var(--primary); } +.switch input:checked + .track .thumb{ left:23px; background:var(--on-primary); } + +/* 应用卡片 */ +.app-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:14px; } +.app-card{ + background:var(--surface-variant); border-radius:var(--radius-l); + border:var(--outline-w) solid var(--outline-variant); padding:16px; + display:flex; flex-direction:column; gap:8px; +} +.app-card .app-head{ display:flex; align-items:center; gap:12px; } +.app-card .app-ico{ width:40px; height:40px; border-radius:var(--radius-m); background:var(--primary); color:var(--on-primary); display:flex; align-items:center; justify-content:center; font-weight:700; font-size:16px; flex:0 0 40px; } +.app-card .app-name{ font-weight:600; } +.app-card .app-pkg{ font-size:12px; color:var(--on-surface-variant); font-family:var(--mono); } +.app-card .app-meta{ font-size:12px; color:var(--on-surface-variant); } +.app-card .app-actions{ display:flex; gap:6px; margin-top:auto; flex-wrap:wrap; } + +/* 远程按键区 */ +.key-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:12px; } +.key-btn{ + padding:14px; border:none; border-radius:var(--radius-m); + background:var(--surface-variant); color:var(--on-surface); + border:var(--outline-w) solid var(--outline-variant); + cursor:pointer; font:inherit; text-align:center; transition:background var(--speed); +} +.key-btn:hover{ background:var(--hover); } +.key-btn .k-ico{ font-size:20px; display:block; margin-bottom:4px; } + +/* 危险区 */ +.danger-box{ border:1px solid var(--error); border-radius:var(--radius-l); padding:16px 18px; } +.danger-box .card-title{ color:var(--error); } +.warn-note{ color:var(--warning); font-size:12px; margin-top:8px; } + +/* 日志查看 */ +.log-view{ background:var(--surface-dim); border-radius:var(--radius-m); padding:12px; max-height:60vh; overflow:auto; font-family:var(--mono); font-size:12px; white-space:pre-wrap; color:var(--on-surface); } + +/* 通知列表 */ +.notif{ display:flex; gap:12px; padding:12px; border-radius:var(--radius-m); background:var(--surface-variant); margin-bottom:10px; align-items:flex-start; border:var(--outline-w) solid var(--outline-variant); } +.notif .n-ico{ width:36px; height:36px; border-radius:var(--radius-m); background:var(--primary); color:var(--on-primary); display:flex; align-items:center; justify-content:center; flex:0 0 36px; font-weight:700; } +.notif .n-body{ flex:1; min-width:0; } +.notif .n-title{ font-weight:600; } +.notif .n-text{ color:var(--on-surface-variant); font-size:13px; margin-top:2px; } +.notif .n-meta{ display:flex; gap:8px; margin-top:6px; color:var(--on-surface-variant); font-size:11px; } +.urgency-hi{ color:var(--error); } + +/* 横幅 / toast / modal */ +.offline-banner{ + position:sticky; top:0; z-index:30; padding:10px 16px; + background:var(--error); color:var(--on-error); font-size:13px; +} +.offline-banner code{ background:rgba(0,0,0,.2); padding:1px 5px; border-radius:4px; } + +.toast{ + position:fixed; left:50%; bottom:28px; transform:translateX(-50%); + z-index:60; background:var(--surface); color:var(--on-surface); + padding:11px 18px; border-radius:var(--radius-m); box-shadow:var(--shadow); + border:var(--outline-w) solid var(--outline-variant); font-size:13px; + max-width:80vw; +} + +.modal-scrim{ + position:fixed; inset:0; z-index:50; background:var(--scrim); + display:flex; align-items:center; justify-content:center; padding:20px; +} +.modal-scrim[hidden]{ display:none; } /* 覆盖上方 display:flex,否则 hidden 失效 */ +.modal{ background:var(--surface); border-radius:var(--radius-l); width:min(420px,100%); padding:22px; box-shadow:var(--shadow); border:var(--outline-w) solid var(--outline-variant); } +.modal-title{ font-size:16px; margin:0 0 10px; } +.modal-body{ color:var(--on-surface-variant); margin:0 0 18px; word-break:break-all; } +.modal-actions{ display:flex; justify-content:flex-end; gap:10px; } + +/* 空态 / 加载 */ +.empty{ text-align:center; color:var(--on-surface-variant); padding:48px 0; } +.skel{ color:var(--on-surface-variant); } +.spinner{ display:inline-block; width:18px; height:18px; border:2px solid var(--outline-variant); border-top-color:var(--primary); border-radius:50%; animation:spin .7s linear infinite; vertical-align:middle; } +@keyframes spin{ to{ transform:rotate(360deg); } } + +/* 响应式:窄屏收起侧栏 */ +@media (max-width:820px){ + :root{ --nav-w:216px; } + .sidebar{ position:fixed; left:0; top:0; bottom:0; z-index:40; margin-left:calc(-1 * var(--nav-w)); } + .sidebar.open{ margin-left:0; box-shadow:var(--shadow); } + .menu-btn{ display:flex; } + .brand-name{ display:none; } + .conn-text{ display:none; } +} +@media (max-width:520px){ + .content{ padding:14px; } + .topbar{ padding:0 10px; gap:8px; } + .seg-btn{ padding:6px 9px; } +} diff --git a/patch_v2/meowpanel/web/meowpanel.js b/patch_v2/meowpanel/web/meowpanel.js new file mode 100644 index 0000000..c38a9b4 --- /dev/null +++ b/patch_v2/meowpanel/web/meowpanel.js @@ -0,0 +1,751 @@ +/* MeowPanel — 前端逻辑(纯静态,零第三方依赖) + * + * 对接协议:devdocs/plan/MEOWPANEL_PROTOCOL.md(草案,后端未实现)。 + * 所有 fetch 失败都优雅降级为「后端未连接」,不假设后端就绪。 + */ +(function () { + 'use strict'; + + /* ---------------- 小工具 ---------------- */ + const $ = (sel, root) => (root || document).querySelector(sel); + const $$ = (sel, root) => Array.prototype.slice.call((root || document).querySelectorAll(sel)); + + function esc(s) { + return String(s == null ? '' : s).replace(/[&<>"']/g, m => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' + }[m])); + } + function fmtBytes(n) { + if (n == null || isNaN(n)) return '—'; + const u = ['B', 'KB', 'MB', 'GB', 'TB']; let v = Number(n), i = 0; + while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } + return (i === 0 ? v : v.toFixed(1)) + ' ' + u[i]; + } + function fmtUptime(sec) { + if (sec == null || isNaN(sec)) return '—'; + sec = Math.floor(sec); + const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60); + return (d > 0 ? d + ' 天 ' : '') + h + ' 小时 ' + m + ' 分'; + } + function fmtTime(ts) { return ts ? new Date(ts * 1000).toLocaleString() : '—'; } + function debounce(fn, ms) { let t = null; return function () { const a = arguments, c = this; clearTimeout(t); t = setTimeout(() => fn.apply(c, a), ms); }; } + function pct(p) { return Math.max(0, Math.min(100, p)); } + + /* ---------------- 全局状态 ---------------- */ + const state = { + style: 'md3', theme: 'dark', + conn: 'unknown', + filesRel: '/', filesAbs: null, + perfTimer: null, // 性能页轮询 + route: 'perf', + downloadNonce: 0 + }; + + /* ---------------- 后端连接状态 ---------------- */ + function setConn(ok) { + state.conn = ok ? 'ok' : 'bad'; + const dot = $('#conn-dot'); + dot.dataset.state = state.conn; + $('#conn-text').textContent = ok ? '在线' : '离线'; + $('#offline-banner').hidden = !!ok; + } + + /* ---------------- API 客户端 ---------------- */ + // 返回 {code,message,status} 形异常;网络/非 JSON → 视为断连 + async function api(method, path, body) { + let res; + const init = { method: method || 'GET' }; + if (body instanceof FormData) { + init.body = body; + } else if (body !== undefined && body !== null) { + init.headers = { 'Content-Type': 'application/json' }; + init.body = JSON.stringify(body); + } + try { + res = await fetch(path, init); + } catch (e) { + setConn(false); + throw { code: 'NETWORK', message: '后端未连接', status: 0 }; + } + let json = null; + try { json = await res.json(); } catch (e) { json = null; } + if (!json || typeof json.ok !== 'boolean') { + // 静态服务器返回 HTML / 404 等 + setConn(res.ok ? true : false); + throw { code: 'NOT_JSON', message: '后端返回了非 JSON 响应(HTTP ' + res.status + ')', status: res.status }; + } + if (!json.ok) { + setConn(true); + const code = (json.error && json.error.code) || 'ERROR'; + throw { code: code, message: (json.error && json.error.message) || ('后端错误 ' + code), status: res.status }; + } + setConn(true); + return json.data; + } + + // 下载(返回 blob URL) + async function apiDownload(path) { + let res; + try { res = await fetch(path); } + catch (e) { setConn(false); throw { code: 'NETWORK', message: '后端未连接' }; } + if (!res.ok) { setConn(true); throw { code: 'HTTP_' + res.status, message: '下载失败(HTTP ' + res.status + ')' }; } + setConn(true); + return URL.createObjectURL(await res.blob()); + } + + /* ---------------- toast / 确认弹窗 ---------------- */ + let toastTimer = null; + function toast(msg, ms) { + const t = $('#toast'); + t.textContent = msg; t.hidden = false; + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { t.hidden = true; }, ms || 2600); + } + function confirmDlg(title, body, danger) { + return new Promise(res => { + $('#modal-title').textContent = title || '确认'; + $('#modal-body').textContent = body || ''; + const yes = $('#modal-yes'), no = $('#modal-no'); + yes.className = 'btn btn-primary' + (danger ? ' btn-danger' : ''); + const scrim = $('#modal'); scrim.hidden = false; + const done = v => { scrim.hidden = true; yes.onclick = no.onclick = scrim.onclick = null; res(v); }; + yes.onclick = () => done(true); + no.onclick = () => done(false); + scrim.onclick = e => { if (e.target === scrim) done(false); }; + }); + } + + /* ---------------- 主题(MD3/MIUIx × 深/浅) ---------------- */ + function applyTheme(style, theme) { + state.style = style; state.theme = theme; + const root = document.documentElement; + root.dataset.style = style; root.dataset.theme = theme; + $$('#style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style)); + $$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.theme === theme)); + try { localStorage.setItem('meowpanel.style', style); localStorage.setItem('meowpanel.theme', theme); } catch (e) {} + } + async function sendTheme(patch) { + try { await api('PUT', '/api/theme', patch); } + catch (e) { toast('主题已本地切换(后端未同步:' + e.message + ')'); } + } + async function initTheme() { + let style = 'md3', theme = 'dark'; + try { style = localStorage.getItem('meowpanel.style') || 'md3'; theme = localStorage.getItem('meowpanel.theme') || 'dark'; } catch (e) {} + applyTheme(style, theme); + try { + const t = await api('GET', '/api/theme'); + applyTheme(t.style || style, t.theme || theme); + } catch (e) { /* 后端离线,保持本地 */ } + } + // 顶栏「风格 / 明暗」切换器:本地立即切换 + 同步后端 + function bindThemeUi() { + $('#style-seg').addEventListener('click', function (e) { + const b = e.target.closest('.seg-btn'); if (!b) return; + applyTheme(b.dataset.style, state.theme); + sendTheme({ style: b.dataset.style }); + }); + $('#theme-seg').addEventListener('click', function (e) { + const b = e.target.closest('.seg-btn'); if (!b) return; + applyTheme(state.style, b.dataset.theme); + sendTheme({ theme: b.dataset.theme }); + }); + } + + /* ---------------- 页面空态 / 错误提示 ---------------- */ + function emptyState(msg, code) { + const t = code === 'NO_CARD' ? '未识别到 TF 卡' : (code === 'NOT_IMPLEMENTED' ? '该功能后端尚未实现' : msg); + return '
' + esc(t) + '
'; + } + function errText(e) { + if (e && e.code === 'NO_CARD') return '未识别到 TF 卡'; + if (e && e.code === 'NOT_IMPLEMENTED') return '该功能后端尚未实现'; + return (e && e.message) ? e.message : '未知错误'; + } + + /* ---------------- 1. 性能页(2s 轮询 /api/perf) ---------------- */ + function barHtml(p) { + p = pct(p); + const c = p >= 90 ? 'danger' : (p >= 70 ? 'warn' : ''); + return '
'; + } + function stat(label, valueHtml, sub, barPct) { + return '
' + esc(label) + '
' + valueHtml + + (sub ? '
' + sub + '
' : '') + + (barPct != null ? barHtml(barPct) : '') + '
'; + } + function renderPerfData(page, d) { + const cpu = d.cpu || {}; + const mem = d.memory || {}, st = d.storage || {}, tf = d.tfcard || {}, tmp = d.temp; + let h = '

实时性能

'; + h += '
运行时间:' + fmtUptime(d.uptime_sec) + '
'; + h += stat('CPU', '' + (cpu.usage == null ? '—' : +cpu.usage.toFixed(1)) + '%', + cpu.cores != null ? cpu.cores + ' 核 · 负载 ' + (cpu.load ? cpu.load.map(x => (+x).toFixed(2)).join(' / ') : '—') : '', + cpu.usage || 0); + const memP = mem.total ? (mem.used / mem.total) * 100 : 0; + h += stat('内存', '' + fmtBytes((mem.used || 0) * 1024) + '/ ' + fmtBytes((mem.total || 0) * 1024) + '', + '可用 ' + fmtBytes((mem.available || (mem.free || 0)) * 1024), memP); + const stP = st.total ? (st.used / st.total) * 100 : 0; + h += stat('存储 (rootfs)', '' + fmtBytes((st.used || 0) * 1024) + '/ ' + fmtBytes((st.total || 0) * 1024) + '', st.path || '', stP); + if (tf) { + if (tf.present) { + const tfP = tf.total ? (tf.used / tf.total) * 100 : 0; + h += stat('TF 卡', '' + fmtBytes((tf.used || 0) * 1024) + '/ ' + fmtBytes((tf.total || 0) * 1024) + '', + fmtBytes((tf.avail || 0) * 1024) + ' 可用 · ' + (tf.filesystem || ''), tfP); + } else { + h += stat('TF 卡', '未插卡', ''); + } + } + const tmpCpu = tmp && tmp.cpu != null ? +tmp.cpu.toFixed(1) : null; + h += stat('温度', '' + (tmpCpu == null ? '—' : tmpCpu) + '℃', + tmp && tmp.board != null ? '板载 ' + (+tmp.board.toFixed(1)) + ' ℃' : ''); + h += '
'; + page.innerHTML = h; + } + async function fetchPerfOnce() { + const page = $('#perf-page'); + if (!page || state.route !== 'perf') return; + let d; + try { d = await api('GET', '/api/perf'); } + catch (e) { if (page && state.route === 'perf') page.innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route === 'perf') renderPerfData(page, d); + } + function stopPerf() { if (state.perfTimer) { clearInterval(state.perfTimer); state.perfTimer = null; } } + function renderPerf() { + $('#content').innerHTML = '

加载中…

'; + if (state.perfTimer) stopPerf(); + fetchPerfOnce(); + state.perfTimer = setInterval(fetchPerfOnce, 2000); + } + + /* ---------------- 2. 亮度 / 音量页 ---------------- */ + let volTimer = null; + function slur(step) { // step: {key,label,min,max,val} + return '

' + esc(step.label) + '

' + + '
' + step.val + '' + + '' + + '' + step.max + '
'; + } + async function renderBright() { + $('#content').innerHTML = '

加载设置…

'; + let bright, vol; + try { + bright = await api('GET', '/api/settings/display.brightness'); + } catch (e) { $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + try { vol = await api('GET', '/api/settings/audio.volume'); } catch (e) { vol = null; } + if (state.route !== 'bright') return; + let h = '
'; + h += slur({ key: 'display.brightness', label: '亮度', min: bright.min || 1, max: bright.max || 100, val: bright.value }); + if (vol) h += slur({ key: 'audio.volume', label: '音量', min: vol.min || 0, max: vol.max || 100, val: vol.value }); + else h += '

音量设置项不可用(后端未实现 audio.volume)

'; + h += '

滑条改动自动保存到系统(走 meow_settings_*),与实体设置页同步。

'; + $('#content').innerHTML = h; + } + const saveSetting = debounce(async function (key, value, type) { + try { + await api('PUT', '/api/settings/' + key, { value: value }); + toast('已保存 ' + key + ' = ' + value); + } catch (e) { toast('保存失败:' + errText(e)); } + }, 400); + function bindSlider() { + $('#content').addEventListener('input', function (e) { + const r = e.target.closest('[data-slider]'); + if (!r) return; + const row = r.closest('.field-row'); + if (row) row.querySelector('.muted').textContent = r.value; + saveSetting(r.dataset.key, +r.value, 'int'); + }); + } + + /* ---------------- 3. 文件管理页 ---------------- */ + // 返回带索引的 rel 显示面包屑 + function joinRel(a, b) { + if (!a || a === '/') return '/' + b; + return (a.endsWith('/') ? a.slice(0, -1) : a) + '/' + b; + } + function crumbHtml(items, cur) { // items: [{name,rel}] + let h = '
'; + } + async function navFiles(rel) { + const page = $('#files-page'); + if (!page) return; + let relPath = rel || '/'; + page.innerHTML = '

加载中…

'; + let d; + try { d = await api('GET', '/api/files?path=' + encodeURIComponent(relPath)); } + catch (e) { page.innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + state.filesRel = d.rel || relPath; state.filesAbs = d.path; + let h = crumbHtml(segments(d.rel), d.rel); + // 白名单快捷入口(根目录下) + if (d.rel === '/' && d.allowed) { + h += '
' + + d.allowed.map(a => '').join(' ') + '
'; + } + h += '
' + + '' + + '' + + '' + + '
'; + h += '
'; + if (!d.items.length) h += ''; + d.items.forEach(function (it) { + const isDir = it.type === 'dir'; + const icon = isDir ? '📁' : (it.type === 'file' ? '📄' : '❔'); + h += '' + + ''; + }); + h += '
名称大小修改时间操作
空目录
' + (isDir ? '' : '') + icon + ' ' + esc(it.name) + (isDir ? '' : '') + + '' + (isDir ? '—' : fmtBytes(it.size)) + '' + fmtTime(it.mtime) + '
' + + (isDir ? '' : '下载') + + '' + + '' + + '
'; + page.innerHTML = h; + } + function segments(rel) { + const parts = (rel || '').split('/').filter(Boolean); const out = []; let cur = ''; + parts.forEach(p => { cur += '/' + p; out.push({ name: p, rel: cur }); }); + return out; + } + function renderFiles() { $('#content').innerHTML = '
'; navFiles(state.filesRel || '/'); } + function absFor(name) { return state.filesAbs ? (state.filesAbs.endsWith('/') ? state.filesAbs + name : state.filesAbs + '/' + name) : null; } + function bindFiles() { + const c = $('#content'); + c.addEventListener('click', async function (e) { + const t = e.target; + const enter = t.closest('[data-enter]'); const dl = t.closest('[data-dl]'); + const ren = t.closest('[data-ren]'); const del = t.closest('[data-del]'); + const dirlink = t.closest('[data-dir]'); const nav = t.closest('[data-nav]'); + const crumb = t.closest('[data-rel]'); + const btn = t.closest('button[id]'); + if (crumb) { navFiles(crumb.dataset.rel); return; } + if (nav) { navFiles(nav.dataset.nav); return; } + if (dirlink || enter) { const n = (dirlink || enter).dataset.directory || (dirlink || enter).dataset.dir || (dirlink || enter).dataset.enter; navFiles(joinRel(state.filesRel, n)); return; } + if (dl) { + const abs = absFor(dl.dataset.dl); if (!abs) return toast('无当前目录'); + doDownload('/api/files/download?path=' + encodeURIComponent(abs), dl.dataset.dl); + return; + } + if (ren) { renameFile(ren.dataset.ren); return; } + if (del) { removeFile(del.dataset.del); return; } + if (btn && btn.id === 'file-newdir') { mkdirFile(); return; } + if (btn && btn.id === 'file-upload') { const fi = $('#file-input'); if (fi) { fi.value = ''; fi.click(); } return; } + if (btn && btn.id === 'file-refresh') { navFiles(state.filesRel); return; } + }); + c.addEventListener('change', function (e) { + if (e.target.id === 'file-input') uploadFiles(e.target.files); + }); + } + function doDownload(url, name) { + apiDownload(url).then(blobUrl => { + const a = document.createElement('a'); a.href = blobUrl; a.download = name || 'file'; + document.body.appendChild(a); a.click(); a.remove(); + setTimeout(() => URL.revokeObjectURL(blobUrl), 5000); + }).catch(e => toast('下载失败:' + errText(e))); + } + async function removeFile(name) { + const abs = absFor(name); if (!abs) return; + const ok = await confirmDlg('删除 ' + name, '此操作不可撤销,确认删除?\n' + abs, true); + if (!ok) return; + try { + await api('DELETE', '/api/files?path=' + encodeURIComponent(abs)); + toast('已删除 ' + name); navFiles(state.filesRel); + } catch (e) { toast('删除失败:' + errText(e)); } + } + async function renameFile(name) { + const abs = absFor(name); if (!abs) return; + const nn = prompt('将「' + name + '」重命名为:', name); + if (!nn || nn === name) return; + try { + await api('POST', '/api/files/rename', { path: abs, new_name: nn }); + toast('已重命名'); navFiles(state.filesRel); + } catch (e) { toast('重命名失败:' + errText(e)); } + } + async function mkdirFile() { + const abs = state.filesAbs; if (!abs) return; + const nn = prompt('新建目录名称(将创建于 ' + state.filesRel + '):', 'newdir'); + if (!nn) return; + try { + await api('POST', '/api/files/mkdir', { path: joinRel(abs, nn) }); + toast('已创建 ' + nn); navFiles(state.filesRel); + } catch (e) { toast('创建失败:' + errText(e)); } + } + async function uploadFiles(files) { + if (!files || !files.length) return; + const abs = state.filesAbs; if (!abs) return toast('无当前目录'); + const fd = new FormData(); fd.append('path', abs); + Array.prototype.forEach.call(files, f => fd.append('file', f)); + const n = files.length; + toast('正在上传 ' + n + ' 个文件…'); + try { + await api('POST', '/api/files/upload', fd); + toast('上传完成'); navFiles(state.filesRel); + } catch (e) { toast('上传失败:' + errText(e)); } + } + + /* ---------------- 4. 应用管理页 ---------------- */ + function renderApps() { + $('#content').innerHTML = '

加载应用列表…

'; + (async function () { + let d; + try { d = await api('GET', '/api/apps'); } + catch (e) { if (state.route === 'apps') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route !== 'apps') return; + let h = '
'; + h += '
' + + '' + + '' + + '
'; + h += '
'; + if (!d.apps.length) h += '
尚未安装任何 MeowAPP
'; + d.apps.forEach(a => { + const ino = esc((a.name || a.package || '?').charAt(0).toUpperCase()); + h += '
' + + '
' + ino + '
' + + '
' + esc(a.name || a.package || '未命名') + '
' + + '
' + esc(a.package || '') + '
' + + '
版本 ' + esc(a.version || '—') + (a.invalid ? ' · 无效' : '') + + (a.size ? ' · ' + fmtBytes(a.size) : '') + '
' + + '
' + + '' + + '导出 .mpk' + + '' + + '
'; + }); + h += '
'; + // 本地源 .mpk 安装(后端支持 path 形式) + h += '

从卡上路径安装

' + + '
' + + '
'; + $('#content').innerHTML = h; + })(); + } + function bindApps() { + const c = $('#content'); + c.addEventListener('click', async function (e) { + const t = e.target; + const install = t.closest('#app-install'); const refresh = t.closest('#app-refresh'); + const info = t.closest('[data-ainfo]'); const exp = t.closest('[data-aexport]'); + const uninst = t.closest('[data-auninst]'); const goPath = t.closest('#app-install-path-go'); + if (install) { const fi = $('#app-file'); if (fi) { fi.value = ''; fi.click(); } return; } + if (refresh) { renderApps(); return; } + if (info) { appInfo(info.dataset.ainfo); return; } + if (exp) { e.preventDefault(); doDownload('/api/apps/' + exp.dataset.aexport + '/export', exp.dataset.aexport + '.mpk'); return; } + if (uninst) { appUninstall(uninst.dataset.auninst); return; } + if (goPath) { installFromPath($('#app-install-path').value); return; } + }); + c.addEventListener('change', function (e) { + if (e.target.id === 'app-file') installFromFile(e.target.files); + }); + } + async function installFromFile(files) { + if (!files || !files.length) return; + const fd = new FormData(); fd.append('file', files[0]); + toast('正在安装 ' + files[0].name + '…'); + try { + const d = await api('POST', '/api/apps/install', fd); + toast('已安装 ' + (d.package || '应用') + (d.version ? ' v' + d.version : '')); + renderApps(); + } catch (e) { toast('安装失败:' + errText(e)); } + } + async function installFromPath(p) { + if (!p) return toast('请输入 .mpk 路径'); + toast('正在安装 ' + p + '…'); + try { + const d = await api('POST', '/api/apps/install', { path: p }); + toast('已安装 ' + (d.package || '应用')); renderApps(); + } catch (e) { toast('安装失败:' + errText(e)); } + } + async function appUninstall(pkg) { + const ok = await confirmDlg('卸载 ' + pkg, '将移除应用与数据预留(数据默认保留)。确认卸载?', true); + if (!ok) return; + try { await api('DELETE', '/api/apps/' + pkg); toast('已卸载 ' + pkg); renderApps(); } + catch (e) { toast('卸载失败:' + errText(e)); } + } + async function appInfo(pkg) { + try { + const d = await api('GET', '/api/apps/' + pkg); + let txt = '包名: ' + d.package + '\n名称: ' + d.name + '\n版本: ' + d.version + '\nexec: ' + d.exec + + '\nsize: ' + fmtBytes(d.size) + '\n路径: ' + d.path; + if (d.description) txt += '\n描述: ' + d.description; + alert(txt); + } catch (e) { toast('获取详情失败:' + errText(e)); } + } + + /* ---------------- 5. 设置镜像页 ---------------- */ + function settingControl(it) { + const key = it.key; + if (it.type === 'bool') { + return '
' + esc(key) + '
' + + '
'; + } + if (it.type === 'int') { + const mn = (it.min == null ? 0 : it.min), mx = (it.max == null ? 100 : it.max); + return '
' + + '
' + esc(it.value == null ? '' : it.value) + '' + + '
'; + } + if (it.type === 'enum') { + const opts = (it.options || []).map(o => '').join(''); + return '
'; + } + return '
'; + } + async function renderSettings() { + $('#content').innerHTML = '

加载设置…

'; + let d; + try { d = await api('GET', '/api/settings'); } + catch (e) { if (state.route === 'settings') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route !== 'settings') return; + let h = '
'; + h += '
' + + '' + + '
'; + const items = d.items || []; + // 按类型分组展示(bool 独立一卡,int/enum/str 各自卡片),便于查看 + const groups = [['开关/布尔', items.filter(x => x.type === 'bool')], + ['数值/滑条', items.filter(x => x.type === 'int')], + ['枚举/选择', items.filter(x => x.type === 'enum')], + ['字符串/路径', items.filter(x => x.type === 'str')]]; + groups.forEach(function (g) { + if (!g[1].length) return; + h += '

' + g[0] + '

' + g[1].map(settingControl).join('') + '
'; + }); + h += '
'; + $('#content').innerHTML = h; + } + function bindSettings() { + const c = $('#content'); + c.addEventListener('input', function (e) { + const r = e.target.closest('[data-stype="int"]'); if (!r) return; + const row = r.closest('.field-row'); if (row) row.querySelector('.muted').textContent = r.value; + updateSetting(r.dataset.skey, +r.value, 'int'); + }); + c.addEventListener('change', function (e) { + const t = e.target; + const sk = t.dataset.skey; if (!sk) return; + if (t.dataset.stype === 'bool') updateSetting(sk, t.checked, 'bool'); + else if (t.dataset.stype === 'enum') updateSetting(sk, t.value, 'enum'); + else if (t.dataset.stype === 'str') updateSetting(sk, t.value, 'str'); + }); + c.addEventListener('click', async function (e) { + if (e.target.closest('#set-restore')) { + const ok = await confirmDlg('恢复全部默认', '将把全部设置恢复为出厂默认(含主题/亮度/音量)。继续?', true); + if (!ok) return; + try { await api('POST', '/api/settings/restore'); toast('已恢复默认'); renderSettings(); } + catch (err) { toast('恢复失败:' + errText(err)); } + } else if (e.target.closest('#set-refresh')) { renderSettings(); } + }); + } + const updateSetting = debounce(async function (key, value, type) { + try { await api('PUT', '/api/settings/' + key, { value: value }); toast('已保存 ' + key); } + catch (e) { toast('保存失败:' + errText(e)); } + }, 350); + + /* ---------------- 6. 远程控制页 ---------------- */ + function renderRemote() { + const keys = [ + { code: 115, label: '音量 +', ico: '🔊' }, + { code: 114, label: '音量 −', ico: '🔉' }, + { code: 116, label: '电源(单击)', ico: '⏻' }, + { code: 164, label: '播放/暂停', ico: '⏯' }, + { code: 163, label: '下一曲', ico: '⏭' }, + { code: 165, label: '上一曲', ico: '⏮' }, + { code: 158, label: '返回', ico: '↩' }, + { code: 172, label: '主页', ico: '⌂' } + ]; + let h = '
'; + h += '

模拟按键

' + + '

行为与本地按键一致(设备端系统统一接管)。长按电源将触发电源菜单。

' + + '
' + keys.map(k => + '' + ).join('') + '
'; + h += '

电源长按(电源菜单)

' + + '
' + + '发送 code=116 · press=long · ms=3000
'; + h += '
'; + $('#content').innerHTML = h; + } + async function sendKey(code, press, ms) { + try { await api('POST', '/api/key/' + code, { press: press || 'tap', ms: ms }); toast('已发送 键 ' + code); } + catch (e) { toast('发送失败:' + errText(e)); } + } + function bindRemote() { + $('#content').addEventListener('click', function (e) { + const kb = e.target.closest('[data-code]'); if (kb) { sendKey(kb.dataset.code, 'tap'); return; } + const pl = e.target.closest('[data-powerlong]'); if (pl) { sendKey(116, 'long', 3000); return; } + }); + } + + /* ---------------- 7. 系统信息 / OTA 页 ---------------- */ + async function renderSystem() { + $('#content').innerHTML = '

加载系统信息…

'; + let d; + try { d = await api('GET', '/api/system'); } + catch (e) { if (state.route === 'system') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route !== 'system') return; + const net = d.network || {}; const wf = net.wifi || {}; const bt = net.bluetooth || {}; const ota = d.ota || {}; + let h = '
'; + h += '

系统信息

' + + row('MeowOS 版本', esc(d.version || '—')) + row('内核', esc(d.kernel || '—')) + row('架构', esc(d.arch || '—')) + + row('主机名', esc(d.hostname || '—')) + row('运行时间', fmtUptime(d.uptime_sec)) + + row('默认启动', esc((d.boot && d.boot.default) || '—')) + + '
'; + h += '

网络

' + + row('WiFi', wf.connected ? esc(wf.ssid || '已连接') + ' · ' + esc(wf.ip || '—') + (wf.rssi != null ? ' · ' + wf.rssi + 'dBm' : '') : '未连接') + + row('蓝牙', bt.available ? ((bt.powered ? '已开启' : '已关闭') + (bt.connected ? ' · 已连接' : '')) : '无蓝牙') + + row('mDNS', esc(net.hostname_local || '—')) + + '
'; + h += '

升级

' + + '

当前 ' + esc(ota.current || d.version || '—') + + (ota.available ? ' · 发现新版本 ' + esc(ota.latest || '') : ' · 已是最新') + '

' + + '
' + + '
'; + h += '

危险操作

' + + '
' + + '
' + + '

⚠️ 重启/关机/升级均为高风险操作,请先保存好正在进行的任务。

'; + h += '
'; + $('#content').innerHTML = h; + } + function row(k, v) { return '' + k + '' + v + ''; } + function bindSystem() { // init 一次性委托绑定(不随渲染累积) + $('#content').addEventListener('click', async function (e) { + if (e.target.closest('#sys-reboot')) { + if (await confirmDlg('确认重启', '系统将立即重启,请在浏览器中关闭本页。', true)) + try { await api('POST', '/api/system/reboot', {}); toast('已请求重启'); } catch (err) { toast('重启失败:' + errText(err)); } + return; + } + if (e.target.closest('#sys-shutdown')) { + if (await confirmDlg('确认关机', '系统将立即关机,请在浏览器中关闭本页。', true)) + try { await api('POST', '/api/system/shutdown', {}); toast('已请求关机'); } catch (err) { toast('关机失败:' + errText(err)); } + return; + } + if (e.target.closest('#sys-ota')) { + if (await confirmDlg('确认升级', '⚠️ 升级中断可能导致系统无法启动,请确保已连接电源。确认开始升级?', true)) + try { await api('POST', '/api/system/ota', {}); toast('已触发升级任务'); } catch (err) { toast('升级失败:' + errText(err)); } + return; + } + if (e.target.closest('#sys-ota-check')) { + // 检查升级只读,用 GET /api/system 读 ota.available/latest;勿 POST /api/system/ota(那是触发升级) + try { const r = await api('GET', '/api/system'); const o = r.ota || {}; toast(o.available ? '发现新版本 ' + o.latest : '已是最新版本'); } + catch (err) { toast('检查失败:' + errText(err)); } + return; + } + }); + } + + /* ---------------- 8. 日志页 ---------------- */ + async function renderLogs() { + $('#content').innerHTML = '

加载日志列表…

'; + let d; + try { d = await api('GET', '/api/logs'); } + catch (e) { if (state.route === 'logs') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route !== 'logs') return; + let h = '
'; + h += '

日志文件(MeowOS/logs)

'; + h += '
' + esc(d.dir || '') + '
'; + h += ''; + if (!d.files.length) h += ''; + d.files.forEach(f => { + h += '' + + ''; + }); + h += '
文件大小修改时间操作
暂无日志
' + esc(f.name) + '' + fmtBytes(f.size) + '' + fmtTime(f.mtime) + '
'; + $('#content').innerHTML = h; + } + function bindLogs() { + $('#content').addEventListener('click', async function (e) { + const t = e.target.closest('[data-log]'); if (!t) return; + const p = $('#log-view'); + try { + const d = await api('GET', '/api/logs/read?file=' + encodeURIComponent(t.dataset.log) + '&lines=400'); + p.hidden = false; p.textContent = (d.lines || []).join('\n') + (d.truncated ? '\n…(更多行被截断)' : ''); + } catch (err) { toast('查看失败:' + errText(err)); } + }); + } + + /* ---------------- 9. 通知页 ---------------- */ + async function renderNotif() { + $('#content').innerHTML = '

加载通知…

'; + let d; + try { d = await api('GET', '/api/notifications'); } + catch (e) { if (state.route === 'notif') $('#content').innerHTML = emptyState('加载失败:' + errText(e), e.code); return; } + if (state.route !== 'notif') return; + let h = '
'; + h += '
' + + '
'; + const list = d.notifications || []; + if (!list.length) h += '
暂无通知
'; + list.forEach(n => { + const urg = n.urgency >= 2 ? '高' : (n.urgency === 1 ? '中' : '低'); + h += '
' + esc((n.app_name || '?').charAt(0).toUpperCase()) + '
' + + '
' + esc(n.summary || n.app_name || '通知') + '
' + + (n.body ? '
' + esc(n.body) + '
' : '') + + '
' + esc(n.app_name || '') + '' + fmtTime(n.ts) + '' + + '紧急度 ' + urg + '' + + (n.progress != null ? '进度 ' + n.progress + '%' : '') + + (n.ongoing ? '进行中' : '') + '
' + + '
'; + }); + h += '
'; + $('#content').innerHTML = h; + } + function bindNotif() { + $('#content').addEventListener('click', async function (e) { + if (e.target.closest('#notif-refresh')) { renderNotif(); return; } + if (e.target.closest('#notif-clear')) { + if (await confirmDlg('清空通知', '将清空全部通知。确认?', true)) + try { await api('POST', '/api/notifications/dismiss_all'); toast('已清空'); renderNotif(); } + catch (err) { toast('清空失败:' + errText(err)); } + return; + } + const del = e.target.closest('[data-ndel]'); if (del) { + try { await api('DELETE', '/api/notifications/' + del.dataset.ndel); toast('已清除'); renderNotif(); } + catch (err) { toast('清除失败:' + errText(err)); } + } + }); + } + + /* ---------------- 路由 ---------------- */ + const ROUTES = { + perf: { title: '实时性能', render: renderPerf, bind: null }, + bright: { title: '亮度 · 音量', render: renderBright, bind: null }, + files: { title: '文件管理', render: renderFiles, bind: bindFiles }, + apps: { title: 'MeowAPPs 管理', render: renderApps, bind: bindApps }, + settings: { title: '设置镜像', render: renderSettings, bind: bindSettings }, + remote: { title: '远程控制', render: renderRemote, bind: bindRemote }, + system: { title: '系统信息 / 升级', render: renderSystem, bind: bindSystem }, + logs: { title: '系统日志', render: renderLogs, bind: bindLogs }, + notif: { title: '通知中心', render: renderNotif, bind: bindNotif } + }; + // 一次性绑定(各菜单 click / 侧栏);页面内交互用 #content 事件委托(已各自绑定) + function bindNav() { + $$('.nav-item').forEach(b => b.addEventListener('click', () => { location.hash = '#/' + b.dataset.route; })); + $('#menu-btn').addEventListener('click', () => $('#sidebar').classList.toggle('open')); + } + function route() { + const id = (location.hash || '#/perf').replace('#/', '').split('?')[0] || 'perf'; + const r = ROUTES[id] || ROUTES.perf; + state.route = id; + stopPerf(); + $$('.nav-item').forEach(b => b.classList.toggle('active', b.dataset.route === id)); + $('#page-title').textContent = r.title; + r.render(); + } + + /* ---------------- 启动 ---------------- */ + function init() { + bindNav(); + bindThemeUi(); + bindSlider(); + bindFiles(); bindApps(); bindSettings(); bindRemote(); bindSystem(); bindLogs(); bindNotif(); + initTheme(); + window.addEventListener('hashchange', route); + route(); + } + document.addEventListener('DOMContentLoaded', init); +})();
'; + h += '根目录'; + items.forEach(it => { h += '/' + esc(it.name) + ''; }); + return h + '