feat: M6.40批 — 阅读器UI改版(书架list·grid+空态骨架+界面设置+App设置页+header·footer+背景黑默认) + 系统级音量翻页映射 + P1 MD3(select面板28/对话框按钮胶囊) + MeowPanel写端点护栏 + SDK公开仓·文档同步
- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page - launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧) - sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样 - meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0) - 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表 0warning;宿主测试全过;二进制留批量批次未入库
This commit is contained in:
1 parent
b364b6371a
commit
2826c1e905
27 files changed
+1704
-129
No files matched your search
@@ -25,6 +25,10 @@ cd /home/Aska/Project/R3ProII/patch_v2/meowpanel
|
||||
./build_meowpanel.sh host # 宿主编译 → meowpanel_host(本机 ld,仅供调试)
|
||||
```
|
||||
|
||||
- **后端源码已按模块拆分**(`src/*.c` + 共享内部头 `src/mp.h`):`mp_server`(HTTP/路由/main) + `mp_util`/`mp_json` + `mp_store`(设置/主题) + `mp_ipc`(IPC 帧/单一收发) + 各 api_* 域 `mp_perf`/`mp_power`/`mp_audio`/`mp_net`/`mp_ams`/`mp_svcd`/`mp_files`/`mp_fonts`/`mp_apps`/`mp_logs`/`mp_notif`/`mp_auth`/`mp_key`/`mp_screen`。
|
||||
`mp.h` 承载全部共享类型/常量/外部全局 + 跨模块函数原型;每个 `.c` 以 `#include "mp.h"` 起步。
|
||||
拆分保持后端接口/行为**完全不变**(宿主 `-Wall -Wextra` 0 warning;重编产物与拆分前**逐字节一致**——同一 mock 环境全端点 diff 无差异)。
|
||||
原单体 `meowpanel.c` 保留在根目录作参考,**不在构建源列表**(构建以 `src/*.c` 为准)。
|
||||
- 两分支均以 `-Os -Wall -Wextra` 复核,**0 warning**(`-Werror` 亦通过)。
|
||||
- 产出(本次构建实测):
|
||||
- `meowpanel`:mipsel 静态,`ELF 32-bit LSB executable, MIPS, MIPS32 rel2, statically linked, stripped`,
|
||||
@@ -120,8 +124,19 @@ http://<host>.local:8080/
|
||||
| POST | `/api/audio/volume` | 写音量(**写级**,PIN 已护):`MSG_AUDIO_VOLUME`,**clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,缺省 100;设备当前 50);audiod 仅 clamp 0-100 无 vol_max 防护,面板是把守方(防炸耳红线) | ✅(clamp 后写;audiod 不在→503) |
|
||||
| GET | `/api/power` | 功耗管理:meow_powermgr IPC `MSG_PM_QUERY→RESULT`(battery/estimate/breakdown,confidence 原样透传) | ✅(powermgr 不在→503 `POWER_UNAVAILABLE`) |
|
||||
| GET | `/api/ams` | App 运行表:`am list`(pkg/name/state/pid/launches/crashes/last_kind/last_code) | ✅(amsd 不在→`available:false`) |
|
||||
| GET | `/api/ams/bg` | **后台活动列表**(M6.38,批④):`am list` 仅取 `state=="bg"`,逐进程 `/proc` 采样 `VmRSS`/`cpu_pct`(首采样/不可得→`null` 前端显 `—`,绝不编造);同源于 amsd FG/BG 元数据,杀/清走既有 `/api/ams/:pkg/kill` | ✅(只读,需 PIN 会话;amsd 不在→`available:false`) |
|
||||
| GET | `/api/svcd` | 服务表:`meow_svcd status all`(name/state/pid/restart/backoff_ms/trigger/src/exit/exec) | ✅(svcd 不在→`available:false`) |
|
||||
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power/audio/net/ams/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
|
||||
| GET | `/api/overview` | **首页聚合**(单线程后端只暴露聚合):后端顺序 fan-out perf/power+events/audio/net+union dns/ams+fg/svcd 六源合成一份 JSON;任一源失败→该子块 `available:false`,绝不假数据 | ✅(**读级,需 PIN 会话**) |
|
||||
| GET | `/api/net/ifaces` | 接口列表详情(kind/ip/gw/rx/tx/rssi/rfkill/dns;netmgrd 0xE2+0xE0 合成) | ✅(只读,需 PIN 会话) |
|
||||
| GET | `/api/net/policy` | 管控策略只读(0xEA SHOW;`enforce=dns_only` 如实)+ **`policy_write_enabled`**(guard③ 门禁状态)| ✅(只读,需 PIN 会话;上游坏字节→`available:false`) |
|
||||
| POST | `/api/net/policy` | 写管控策略(`set_net`/`set_domain`/`del`):**guard① 防自锁**(`SELF_LOCK` 422)+ **guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;自锁→422,门禁→403,IPC 权限→403 FORBIDDEN) |
|
||||
| GET | `/api/power/policy` | 电源策略快照(profile 四档/阈值/`enacted`)+ **`policy_write_enabled`**(guard③;注入后 jv_parse 自校验) | ✅(只读,需 PIN 会话;坏字节→503) |
|
||||
| POST | `/api/power/policy` | 写电源策略(**只写 settings.conf `power.*` 键**):**guard③ 默认关**(`POLICY_DISABLED` 403)+ 二次确认 | ✅(**写级**,PIN 会话;非法值→422,权限→403) |
|
||||
| GET | `/api/power/events` | 电源异常事件(充放电/低电/亮灭屏/背光跳变;overview 摘要 top2 的来源) | ✅(只读,需 PIN 会话;powermgr 不在→503) |
|
||||
| GET | `/api/power/history` | 功耗采样历史(30s 环形 4h,不落盘) | ✅(只读,需 PIN 会话;powermgr 不在→503) |
|
||||
| GET | `/api/ams/:pkg` | 单 App incident 历史(`am history`,崩溃根因 detail;坏 detail→降级 `available:false`) | ✅(只读,需 PIN 会话) |
|
||||
| POST | `/api/ams/:pkg/start` | 启动 App(`am start <pkg>`;**写级**,PIN 会话) | ✅(包名非法→400,launcher 离线→503) |
|
||||
| POST | `/api/ams/:pkg/kill` | 结束 App(`am kill <pkg>`;**写级**,PIN 会话,前台前端二次确认) | ✅(包名非法→400,not registered→404,busy→409) |
|
||||
| GET | `/api/apps` | 扫描 `MeowAPPs/` 读 `app.json` | ✅ |
|
||||
| GET | `/api/apps/:pkg` | 应用详情 | ✅ |
|
||||
| POST | `/api/apps/install` | 经 `nyai install`(multipart 或 path) | ✅ |
|
||||
@@ -396,6 +411,33 @@ http://<host>.local:8080/
|
||||
同款统计);DNS 不可用则省略 `dns` 键仅保留网络状态,**绝不假数据**。首页 netCard 改读 `net.dns`,去掉单独 `/api/net/dns`
|
||||
(首页严格单请求)。
|
||||
|
||||
### 5.16 批 P1 写端点(power/net/ams 写)+ 三条护栏(MEOWPANEL_ENHANCE_PLAN P1)
|
||||
> 三个写端点全部落在 `POST` 分支 + `require_auth` 之后(无一漏到无鉴权路径);响应**如实回错误码**,绝不掩盖。
|
||||
|
||||
- **三条护栏**:
|
||||
1. **guard① 防自锁**(仅 `/api/net/policy`):对系统关键网络/服务组件禁设「阻断」(`set_net`+`arg=block`、
|
||||
或任何 `set_domain` 域名限制)。命中即 `422 {"code":"SELF_LOCK"}`。自锁实体(取 `svc:|app:|proc:` 后裸名),
|
||||
`net_locked_entity()` 精确比对 `dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init`(netmgrd 加载时也剔除,面板侧抢先硬拒)。
|
||||
「放行 / 删除规则」不阻断(恢复连通)仍放行。
|
||||
2. **guard② PIN 会话鉴权**:复用全局 `require_auth`(§5.9);无会话 → `401 UNAUTHORIZED`。
|
||||
3. **guard③ 策略写默认关**:`panel.policy_enabled`(新设置项,`settings.conf`「面板」组,默认 `0`)。
|
||||
`0`(缺省)时 `POST /api/net/policy`、`POST /api/power/policy` 在解析 body **前**即拒 `403 POLICY_DISABLED`
|
||||
(只读 GET 不受此限);置 `1`(设置页 / 电源·网络策略卡的「开启策略写」)放行。与 powermgr `enacted` **独立**。
|
||||
|
||||
- **`POST /api/net/policy`**:`{op:"set_net|set_domain|del", entity:"app:…|svc:…|proc:…", arg:"…"}`(`del` 免 `arg`)。
|
||||
成功 `{"accepted":true,"message":…}`;权限→`403 FORBIDDEN`、非法→`422 UNSUPPORTED`、自锁→`422 SELF_LOCK`、门禁→`403 POLICY_DISABLED`、
|
||||
IPC 不可达→`503 NET_UNAVAILABLE`。`enforce=dns_only` 如实显示「仅 DNS 层拒绝」,不谎称断连。
|
||||
- **`GET /api/net/policy`**:`net_policy_parse` 输出 `{"available":true,"policy_write_enabled":<bool>,"enforce","log_level","rules":[…]}`;
|
||||
坏字节(实体名/域名)降级 `{"available":false}`。前端据此渲染「默认关」横幅 + 「开启策略写」(`enablePolicyWrite`)。
|
||||
- **`POST /api/power/policy`**:`{item:"profile|low-pct|crit-pct|bri-cap|scr-cap", value:"…"}`,**只写 settings.conf `power.*` 键**;
|
||||
成功后透传 powermgr 应答。非法值→`422`(服务端 `ok:false` 按 `why` 映射)、权限→`403`、门禁→`403 POLICY_DISABLED`、
|
||||
IPC 不可达→`503 POWER_UNAVAILABLE`。
|
||||
- **`GET /api/power/policy`**:透传 powermgr 策略快照(含 `enacted`)后经 `power_policy_append_flag()` **追加顶层 `policy_write_enabled`**
|
||||
(剥尾拼键 + jv_parse 自校验,绝不发非法 JSON);非对象答原样透传。
|
||||
- **`POST /api/ams/:pkg/{start,kill}`**:`am start/kill <pkg>`;包名经 `ams_pkg_ok`(仅 ASCII 可见、禁 `/ : 空白`)。成功
|
||||
`{"accepted":true,"op","pkg","message"}`;`not registered`→404、`busy`/`not running`→409、`launcher offline`→503、其余→500。
|
||||
**kill 前台 App 前端二次确认**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 与协议的出入(「实现为准 + 原因」,供回改 MEOWPANEL_PROTOCOL.md)
|
||||
@@ -486,17 +528,46 @@ http://<host>.local:8080/
|
||||
|
||||
```
|
||||
patch_v2/meowpanel/
|
||||
├── meowpanel.c # 后端源码(约 5590 行,-Wall -Wextra 0 warning;含可选鉴权/内联 SHA-256/实时画面+增量帧/触摸注入/通知 IPC 实时化/字体库 tar.gz 导入导出/自然排序/audiod 状态快照 + 批2 崩溃/日志中心 + 音频写 + overview DNS 聚合)
|
||||
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm,-Wall -Wextra 0 warning)
|
||||
├── meowpanel.c # 拆分前单体源码(约 6372 行,保留作参考,**不在构建源列表**;构建以 src/*.c 为准)
|
||||
├── src/ # 后端模块化源码(模块化拆分,接口/行为不变,-Wall -Wextra 0 warning)
|
||||
│ ├── mp.h # 共享内部头:全部共享类型/常量/外部全局 + 跨模块函数原型(含鉴权/实时画面等)
|
||||
│ ├── mp_server.c # HTTP 服务器/路由分发 + 静态托管 + main(单线程 + poll 串行)
|
||||
│ ├── mp_util.c # sbuf/UTF-8 消毒/内存搜索/命令管道/路径与 token 工具
|
||||
│ ├── mp_json.c # 最简 JSON 解析/序列化(jv_t 树)
|
||||
│ ├── mp_store.c # 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme* + g_sets 表
|
||||
│ ├── mp_ipc.c # IPC 帧读写 + meow_svc_perf/power/netmgr 客户端(单点收发)+ socket 路径
|
||||
│ ├── mp_perf.c # /api/perf,/api/system,action,pin-unlock(perf 聚合)
|
||||
│ ├── mp_power.c # meow_powermgr IPC(/api/power*,policy,window,events)+ ipc_pm_send
|
||||
│ ├── mp_audio.c # meow_audiod IPC(状态快照 + 写命令 route/volume)
|
||||
│ ├── mp_net.c # meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)
|
||||
│ ├── mp_ams.c # App 运行表 + amd 历史 + start/kill 控制
|
||||
│ ├── mp_svcd.c # svcd 服务表 + 首页聚合 /api/overview
|
||||
│ ├── mp_files.c # 路径安全/白名单 + /api/files* + multipart 上传 + 卡根路径
|
||||
│ ├── mp_fonts.c # 字体库 tar.gz 导入/导出 + 列表(busybox gzip|tar 管道)
|
||||
│ ├── mp_apps.c # MeowAPPs 扫描 + nyai 安装/导出/移除
|
||||
│ ├── mp_logs.c # /api/logs 列表 + 读 + 崩溃汇总
|
||||
│ ├── mp_notif.c # 通知(优先 launcher IPC 活跃集,回退 TF 归档)
|
||||
│ ├── mp_auth.c # 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话
|
||||
│ ├── mp_key.c # 按键注入(写 /dev/input/event* EV_KEY,物理键红线内)
|
||||
│ └── mp_screen.c # 实时画面(fb0→JPEG,增量帧)+ 触摸注入 + delta 缓存
|
||||
├── mp_jpeg.c # 最小 baseline JPEG 编码器(顺序 DCT + 4:2:0 + 标准 Huffman,整数定点,无 libm)
|
||||
├── mp_jpeg.h # mp_jpeg.c 接口(RGB565 → 内存 JPEG)
|
||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;meowpanel.c + mp_jpeg.c)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,约 218 KB,md5 015f9cf1…)
|
||||
├── build_meowpanel.sh # 交叉编译 + host 分支(-Wall -Wextra 0 warning;src/*.c + mp_jpeg.c + -I.)
|
||||
├── meowpanel # 交叉产物(mipsel 静态,本次 md5 fead4852…)
|
||||
├── meowpanel_host # 宿主编译(本机 ld,调试用)
|
||||
├── services.d/
|
||||
│ └── meowpanel.conf # 部署到 TF 卡 MeowOS/services.d/meowpanel.conf
|
||||
├── web/ # 前端(已存在;部署时拷到后端同目录)
|
||||
│ ├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航
|
||||
│ ├── meowpanel.css # 含登录页样式
|
||||
│ └── meowpanel.js # API 客户端带 token/401 登录、设置页鉴权卡片、音频页(5s轮询)/字体页
|
||||
└── README.md # 本文件
|
||||
└── web/ # 前端(模块化拆分;部署时拷到后端同目录)
|
||||
├── index.html # 含登录页 overlay + 设置密码对话框 + 音频/字体导航;脚本按序加载
|
||||
├── meowpanel.css # 含登录页样式 + 「屏幕·遥控」合并页样式(screen-remote/remote-btn)
|
||||
├── mp_shared.js # 共享框架:全局状态 + API 客户端(token/401) + 公共工具 + 主题/弹窗/登录/滑条(window.MP)
|
||||
├── mp_pages.js # 各页渲染/绑定:home/perf/power/audio/net/files/apps/fonts/settings/system/logs/notif/proc + 合并的屏幕·遥控
|
||||
└── meowpanel.js # 前端入口:ROUTES/路由/启动(读取 window.MP 装配)
|
||||
```
|
||||
**前端拆分说明**:`meowpanel.js`(单体 IIFE)按「公共 util+api(client) / 各页渲染 / 应用入口」拆为
|
||||
`mp_shared.js` + `mp_pages.js` + `meowpanel.js` 三文件(经典脚本按序加载,函数体保持原样,仅顶部从
|
||||
`window.MP` 解构共享框架,**行为不变**)。
|
||||
「屏幕」+「控制」两页**合并为一页**(route `screen`,导航「屏幕·遥控」):内容区左侧=屏幕实时画面
|
||||
(复用 /api/screen.jpeg + 触摸注入 + fps 滑条 + 增量省流开关),右侧=MD3 矢量图标遥控板(音量± / 亮度± /
|
||||
指定亮度滑条 / 播放·暂停 / 下一曲 / 上一曲 / 电源单击 / 电源长按),**全矢量内联 SVG,无 emoji**。
|
||||
`返回`/`主页` 属物理键红线(后端 /api/key 禁注入 158/172),未提供注入按钮(遥控板附说明)。
|
||||
@@ -270,6 +270,7 @@ int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen, uint16_t *rtyp
|
||||
int api_power_policy_get(int sock);
|
||||
int api_power_policy_set(int sock, const char *body, size_t body_len);
|
||||
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail);
|
||||
int power_policy_append_flag(sbuf_t *out, const char *obj); /* 拼 guard③ policy_write_enabled 顶层键(自校验) */
|
||||
int api_power_history(int sock);
|
||||
int api_power_events(int sock);
|
||||
const char *ams_bin(void);
|
||||
@@ -287,6 +288,8 @@ int api_net_ifaces(int sock);
|
||||
void net_policy_parse(const char *txt, sbuf_t *s);
|
||||
int api_net_policy_get(int sock);
|
||||
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
||||
int net_locked_entity(const char *entity); /* 防自锁:系统关键网络/服务组件(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init) */
|
||||
int panel_policy_write_enabled(void); /* policy 默认关 gate:settings.conf `panel.policy_enabled`,缺省 0=关(只读透传) */
|
||||
void panel_strip_ansi(char *s);
|
||||
int txt_tokens(char *buf, char **toks, int maxtoks);
|
||||
void join_tokens(char **toks, int from, int to, char *out, size_t cap);
|
||||
|
||||
@@ -285,11 +285,30 @@ int api_net_ifaces(int sock)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 防自锁实体:对本机关键网络/服务组件禁设「阻断」(net=block / set_domain 域名限制),
|
||||
* 否则会把能连上设备/面板/系统核心的网络能力一并锁死(SSH / WiFi / DHCP / DNS / 面板 / init)。
|
||||
* netmgrd 加载时也会剔除自锁项,面板侧抢先硬拒(guard①),保证 0 warning + 如实回错误码。
|
||||
* 匹配口径:取 `svc:|app:|proc:` 之后的裸名精确比对(`svc:netmgrd`/`proc:sysui`/裸 `udhcpc` 均命中)。 */
|
||||
int net_locked_entity(const char *entity)
|
||||
{
|
||||
if (!entity || !entity[0]) return 0;
|
||||
const char *base = entity;
|
||||
const char *colon = strrchr(entity, ':');
|
||||
if (colon) base = colon + 1;
|
||||
static const char *locked[] = {
|
||||
"dropbear", "wpa_supplicant", "udhcpc", "netmgrd", "svcd", "sysui", "init", NULL
|
||||
};
|
||||
for (int i = 0; locked[i]; i++)
|
||||
if (!strcmp(base, locked[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void net_policy_parse(const char *txt, sbuf_t *s)
|
||||
{
|
||||
char lvl[24] = "", enf[24] = "dns_only";
|
||||
const char *p = txt;
|
||||
sA(s, "{\"available\":true,\"enforce\":");
|
||||
sA(s, "{\"available\":true,\"policy_write_enabled\":%s,\"enforce\":",
|
||||
panel_policy_write_enabled() ? "true" : "false");
|
||||
/* 首行找 enforce=... */
|
||||
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
||||
const char *en = strstr(p, "enforce=");
|
||||
@@ -399,6 +418,9 @@ int api_net_policy_get(int sock)
|
||||
int api_net_policy_set(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
/* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。 */
|
||||
if (!panel_policy_write_enabled())
|
||||
return send_err(sock, 403, "POLICY_DISABLED", "网络策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
|
||||
@@ -415,6 +437,14 @@ int api_net_policy_set(int sock, const char *body, size_t body_len)
|
||||
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
|
||||
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
|
||||
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
|
||||
/* guard① 防自锁:对系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)禁设阻断。
|
||||
* 「放行 / 删除规则」不阻断(那是恢复连通),仍放行;仅 set_net=block 与 set_domain(域名限制)命中即拒。 */
|
||||
{
|
||||
int blocking = ((nop == NET_POLICY_SET_NET) && !strcmp(arg, "block")) ||
|
||||
(nop == NET_POLICY_SET_DOMAIN);
|
||||
if (blocking && net_locked_entity(entity))
|
||||
return send_err(sock, 422, "SELF_LOCK", "防自锁:禁止阻断系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)的网络");
|
||||
}
|
||||
|
||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||
q.op = (uint32_t)nop;
|
||||
|
||||
@@ -71,6 +71,18 @@ done:
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 在对象 JSON(已剥尾部 `}`、经 utf8 校验)上拼 guard③ `policy_write_enabled` 顶层键:
|
||||
* 拼完 jv_parse 自校验,绝不发非法 JSON(防 overview 教训);失败返回 -1(调用方回 500,绝不带病下发)。 */
|
||||
int power_policy_append_flag(sbuf_t *out, const char *obj)
|
||||
{
|
||||
sA(out, "%s", obj);
|
||||
sA(out, ",\"policy_write_enabled\":%s}", panel_policy_write_enabled() ? "true" : "false");
|
||||
jv_t *chk = jv_parse(out->b ? out->b : "{}", (int)(out->b ? out->n : 2));
|
||||
if (!chk) return -1;
|
||||
jv_free(chk);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_power_policy_get(int sock)
|
||||
{
|
||||
char js[MEOW_IPC_MAX];
|
||||
@@ -80,12 +92,27 @@ int api_power_policy_get(int sock)
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||
return send_json(sock, 200, js);
|
||||
/* 追加顶层 `policy_write_enabled`(面板侧 gate 状态,独立于 powermgr 的 `enacted`):
|
||||
* 稳妥剥外层括号再拼(同 net_block_fill 手法)。上游非对象(数组/文本)时原样透传、不带该键
|
||||
* (前端缺省视为已开启,兼容旧 powermgr)。 */
|
||||
size_t l = strlen(js);
|
||||
while (l && (js[l - 1] == ' ' || js[l - 1] == '\t' || js[l - 1] == '\n' || js[l - 1] == '\r')) js[--l] = 0;
|
||||
if (l < 2 || js[0] != '{' || js[l - 1] != '}') return send_json(sock, 200, js);
|
||||
js[l - 1] = 0; /* 去尾部 } */
|
||||
sbuf_t s = {0};
|
||||
if (power_policy_append_flag(&s, js) != 0) { sbuf_free(&s); return send_err(sock, 500, "INTERNAL", "电源策略聚合输出未通过 JSON 自校验"); }
|
||||
int out = send_json(sock, 200, s.b ? s.b : "{}");
|
||||
sbuf_free(&s);
|
||||
return out;
|
||||
}
|
||||
|
||||
int api_power_policy_set(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
/* guard③ policy 默认关:写端点默认关闭,须在设置开启 panel.policy_enabled 才放行(只读 GET 不受此限)。
|
||||
* 注意:powermgr 侧另有 `enacted`(策略是否已应用),本 gate 是面板侧写入口门禁,二者独立。 */
|
||||
if (!panel_policy_write_enabled())
|
||||
return send_err(sock, 403, "POLICY_DISABLED", "电源策略写默认关闭:先到面板设置开启 panel.policy_enabled(危险操作)");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
|
||||
|
||||
@@ -517,6 +517,8 @@ int panel_selftest(void)
|
||||
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||
jv_t *enf = jv_obj_get(pr, "enforce");
|
||||
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled"); /* guard③ 状态键必须为布尔,且拼读无括号错位 */
|
||||
if (!pwe || pwe->type != J_BOOL) { printf("SELFTEST FAIL: net_policy_parse 缺/损坏 policy_write_enabled\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||
jv_free(pr);
|
||||
sbuf_free(&p);
|
||||
}
|
||||
@@ -567,7 +569,66 @@ int panel_selftest(void)
|
||||
sbuf_free(&b);
|
||||
}
|
||||
|
||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
|
||||
/* 7) net_locked_entity:防自锁实体精确匹配(svc:/proc:/裸名);非关键实体放行(guard①)。 */
|
||||
{
|
||||
struct { const char *e; int want; } c[] = {
|
||||
{ "svc:dropbear", 1 }, { "proc:netmgrd", 1 }, { "udhcpc", 1 },
|
||||
{ "svc:sysui", 1 }, { "svc:wpa_supplicant", 1 }, { "svcd", 1 }, { "init", 1 },
|
||||
{ "app:com.example", 0 }, { "svc:avahi", 0 }, { "proc:sh", 0 },
|
||||
{ "app:com.netmgrd.helper", 0 }, /* 含子串但不等于裸名,不得误判 */
|
||||
{ "", 0 }, { NULL, 0 }
|
||||
};
|
||||
for (int i = 0; c[i].e; i++) {
|
||||
int got = net_locked_entity(c[i].e);
|
||||
if (got != c[i].want) { printf("SELFTEST FAIL: net_locked_entity(%s)=%d 期望 %d\n", c[i].e, got, c[i].want); return 1; }
|
||||
}
|
||||
}
|
||||
|
||||
/* 8) policy 默认关 gate(guard③):无设置文件 → panel_policy_write_enabled()==0(默认关);
|
||||
* settings_write_one("panel.policy_enabled","1") 后 ==1(写临时 settings 文件验证,测完清理)。 */
|
||||
{
|
||||
const char *saved = g_settings_file;
|
||||
char tmpf[] = "/tmp/meowpanel_policy_gate_XXXXXX";
|
||||
int tfd = mkstemp(tmpf);
|
||||
if (tfd < 0) { printf("SELFTEST FAIL: mkstemp 失败\n"); return 1; }
|
||||
close(tfd);
|
||||
g_settings_file = tmpf;
|
||||
int before = panel_policy_write_enabled();
|
||||
if (settings_write_one("panel.policy_enabled", "1") != 0) {
|
||||
printf("SELFTEST FAIL: 写 panel.policy_enabled 失败\n"); unlink(tmpf); g_settings_file = saved; return 1;
|
||||
}
|
||||
int after = panel_policy_write_enabled();
|
||||
unlink(tmpf);
|
||||
g_settings_file = saved;
|
||||
if (before != 0 || after != 1) { printf("SELFTEST FAIL: policy gate 默认关/开启 %d->%d\n", before, after); return 1; }
|
||||
}
|
||||
|
||||
/* 9) power_policy_append_flag:对象 JSON 剥尾拼入 policy_write_enabled 后仍可解析、键为布尔、原字段保留
|
||||
* ——防括号错位重演(overview 教训)。测试在无 panel.policy_enabled 的临时 settings 下进行(期望 false=默认关)。 */
|
||||
{
|
||||
const char *saved = g_settings_file;
|
||||
char tmpf[] = "/tmp/meowpanel_policy_append_XXXXXX";
|
||||
int tfd = mkstemp(tmpf);
|
||||
if (tfd >= 0) { close(tfd); g_settings_file = tmpf; }
|
||||
sbuf_t p = {0};
|
||||
if (power_policy_append_flag(&p, "{\"profile\":\"balanced\",\"enacted\":true") != 0) {
|
||||
printf("SELFTEST FAIL: power_policy_append_flag 拼接失败\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||||
}
|
||||
if (!utf8_valid_n(p.b, p.n) || !strstr(p.b, "\"policy_write_enabled\":false")) {
|
||||
printf("SELFTEST FAIL: power_policy_append_flag 产物异常\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
|
||||
}
|
||||
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||||
if (!pr) { printf("SELFTEST FAIL: power_policy_append_flag 产物 JSON 无法解析\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled");
|
||||
if (!pwe || pwe->type != J_BOOL || pwe->bval != 0) { printf("SELFTEST FAIL: power_policy_append_flag policy_write_enabled 非 false(默认关)\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||
jv_t *pfl = jv_obj_get(pr, "profile");
|
||||
if (!pfl || pfl->type != J_STR || strcmp(pfl->str, "balanced") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 丢失原字段\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
|
||||
jv_free(pr);
|
||||
sbuf_free(&p);
|
||||
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
|
||||
}
|
||||
|
||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ static const setdef_t g_sets[] = {
|
||||
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||||
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||
/* panel.policy_enabled — 策略写「默认关」门禁(guard③):POST /api/net/policy 与 /api/power/policy
|
||||
* 默认关闭(缺省 0),置 1 才放行。落 settings.conf(「面板」组,前端设置页可开启),仅作面板侧门禁记录,
|
||||
* 不影响 powermgr 的 `enacted`(那是服务端「策略是否已应用」,二者独立)。 */
|
||||
{ "panel.policy_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||||
};
|
||||
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
||||
|
||||
@@ -143,6 +147,11 @@ int settings_get_int(const char *key, int defv)
|
||||
return r;
|
||||
}
|
||||
|
||||
int panel_policy_write_enabled(void)
|
||||
{
|
||||
return settings_get_int("panel.policy_enabled", 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int brightness_clamp(int v)
|
||||
{
|
||||
int mn = settings_get_int("display.brightness_min", 1);
|
||||
|
||||
@@ -609,21 +609,24 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
|
||||
function powerPolicyCard(pol) {
|
||||
if (!pol) return '<div class="card"><h3 class="card-title">电源策略</h3>' + homeUnavailable() + '</div>';
|
||||
const pwe = !(pol.policy_write_enabled === false); // 缺省视为已开启(兼容旧 powermgr 不带该键)
|
||||
const profiles = ['performance', 'balanced', 'powersave', 'ultra'];
|
||||
const cur = pol.profile || 'balanced';
|
||||
let btns = profiles.map(p => '<button class="btn btn-ghost pol-btn' + (p === cur ? ' pol-on' : '') + '" data-pol="' + esc(p) + '">' + powerProfileLabel(p) + '</button>').join(' ');
|
||||
return '<div class="card"><h3 class="card-title">电源策略</h3>' +
|
||||
'<div class="field-row" style="margin-bottom:12px"><span class="muted">省电档位:</span>' + btns + '</div>' +
|
||||
'<table class="table"><tbody>' +
|
||||
row('低电阈值', '<input type="number" id="pol-low" class="num pol-num" value="' + esc(pol.low_pct) + '" min="1" max="99"> %') +
|
||||
row('临界阈值', '<input type="number" id="pol-crit" class="num pol-num" value="' + esc(pol.critical_pct) + '" min="1" max="99"> %') +
|
||||
row('亮度上限', '<input type="number" id="pol-bri" class="num pol-num" value="' + esc(pol.brightness_cap) + '" min="0" max="101"> <span class="muted">0=不生效</span>') +
|
||||
row('息屏上限', '<input type="number" id="pol-scr" class="num pol-num" value="' + esc(pol.screen_timeout_cap) + '" min="0"> <span class="muted">秒 · 0=不生效</span>') +
|
||||
let h = '<div class="card"><h3 class="card-title">电源策略</h3>';
|
||||
if (!pwe) h += '<div class="page-banner" style="margin-bottom:10px"><span class="ico">🔒</span><span>策略写默认关闭(panel.policy_enabled=0):当前只读,切档/阈值需先开启。</span><button class="btn" id="pol-enable">开启策略写</button></div>';
|
||||
const btns = profiles.map(p => '<button class="btn btn-ghost pol-btn' + (p === cur ? ' pol-on' : '') + '" data-pol="' + esc(p) + '"' + (!pwe ? ' disabled' : '') + '>' + powerProfileLabel(p) + '</button>').join(' ');
|
||||
h += '<div class="field-row" style="margin-bottom:12px"><span class="muted">省电档位:</span>' + btns + '</div>';
|
||||
h += '<table class="table"><tbody>' +
|
||||
row('低电阈值', '<input type="number" id="pol-low" class="num pol-num" value="' + esc(pol.low_pct) + '" min="1" max="99"' + (!pwe ? ' disabled' : '') + '> %') +
|
||||
row('临界阈值', '<input type="number" id="pol-crit" class="num pol-num" value="' + esc(pol.critical_pct) + '" min="1" max="99"' + (!pwe ? ' disabled' : '') + '> %') +
|
||||
row('亮度上限', '<input type="number" id="pol-bri" class="num pol-num" value="' + esc(pol.brightness_cap) + '" min="0" max="101"' + (!pwe ? ' disabled' : '') + '> <span class="muted">0=不生效</span>') +
|
||||
row('息屏上限', '<input type="number" id="pol-scr" class="num pol-num" value="' + esc(pol.screen_timeout_cap) + '" min="0"' + (!pwe ? ' disabled' : '') + '> <span class="muted">秒 · 0=不生效</span>') +
|
||||
row('状态', (pol.enacted ? '<span class="home-chip ok">已手动开启</span>' : '<span class="home-chip mut">默认全关</span>')) +
|
||||
'</tbody></table>' +
|
||||
'<div class="field-row"><button class="btn btn-primary" id="pol-apply">保存阈值</button><span class="muted">切档即时生效(需确认);阈值点「保存阈值」。</span></div>' +
|
||||
'<p class="muted">' + esc(pol.note || '') + '</p>' +
|
||||
'</div>';
|
||||
'</tbody></table>';
|
||||
h += '<div class="field-row"><button class="btn btn-primary" id="pol-apply"' + (!pwe ? ' disabled' : '') + '>保存阈值</button><span class="muted">切档即时生效(需确认);阈值点「保存阈值」。</span></div>';
|
||||
h += '<p class="muted">' + esc(pol.note || '') + '</p>';
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
function powerEventsCard(ev) {
|
||||
@@ -690,6 +693,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
|
||||
function bindPower() {
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
if (e.target.closest('#pol-enable')) { enablePolicyWrite(fetchPowerOnce); return; }
|
||||
const pb = e.target.closest('.pol-btn');
|
||||
if (pb) {
|
||||
const p = pb.dataset.pol;
|
||||
@@ -1003,6 +1007,19 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
(i > 0 ? ' <span class="net-entity">' + esc(e.slice(i + 1)) + '</span>' : '');
|
||||
}
|
||||
|
||||
const NET_LOCKEDS = ['dropbear', 'wpa_supplicant', 'udhcpc', 'netmgrd', 'svcd', 'sysui', 'init'];
|
||||
function netLockedEntity(e) { // 与后端 net_locked_entity 同口径(取 `svc:|app:|proc:` 后裸名精确比对)
|
||||
if (!e) return false;
|
||||
const i = e.indexOf(':');
|
||||
return NET_LOCKEDS.indexOf(i > 0 ? e.slice(i + 1) : e) !== -1;
|
||||
}
|
||||
|
||||
async function enablePolicyWrite(fetchFn) { // 开启 panel.policy_enabled(guard③ 手动开启入口)
|
||||
try { await api('PUT', '/api/settings/panel.policy_enabled', { value: 1 }); toast('策略写已开启(panel.policy_enabled=1)'); }
|
||||
catch (err) { toast('开启失败:' + errText(err)); }
|
||||
if (fetchFn) fetchFn();
|
||||
}
|
||||
|
||||
function netStateCls(st) {
|
||||
if (!st) return '';
|
||||
if (st === 'ESTABLISHED') return 'net-state ok';
|
||||
@@ -1109,7 +1126,9 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
if (!pol || pol.available === false) return '<div class="card"><h3 class="card-title">网络策略</h3>' + homeUnavailable() + '</div>';
|
||||
const enforce = pol.enforce || '';
|
||||
const dnsOnly = enforce === 'dns_only';
|
||||
const pwe = !(pol.policy_write_enabled === false); // 缺省视为已开启(兼容旧 powermgr/netmgrd 不带该键)
|
||||
let h = '<div class="card"><h3 class="card-title">网络策略</h3>';
|
||||
if (!pwe) h += '<div class="page-banner" style="margin-bottom:10px"><span class="ico">🔒</span><span>策略写默认关闭(panel.policy_enabled=0):当前只读,阻断/放行/域名规则需先开启。</span><button class="btn" id="pol-enable">开启策略写</button></div>';
|
||||
h += '<div class="page-banner" style="margin-bottom:10px"><span class="ico">' + (dnsOnly ? 'ℹ️' : '⚠️') + '</span><span>' +
|
||||
(dnsOnly ? '执行模式:仅 DNS 层拒绝(enforce=dns_only;本内核 SOCK_DESTROY 为 no-op,不会真正断连)' : '执行模式:' + esc(enforce)) + '</span></div>';
|
||||
const rules = pol.rules || [];
|
||||
@@ -1117,21 +1136,24 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
else {
|
||||
h += '<table class="table"><thead><tr><th>实体</th><th>网络</th><th>白名单域名</th><th>黑名单域名</th><th style="text-align:right">操作</th></tr></thead><tbody>';
|
||||
rules.forEach(r => {
|
||||
const targetNet = r.net === 'block' ? 'allow' : 'block';
|
||||
const locked = targetNet === 'block' && netLockedEntity(r.entity);
|
||||
h += '<tr><td class="mono">' + netBadge(r.entity) + '</td>' +
|
||||
'<td><span class="home-chip ' + (r.net === 'block' ? 'bad' : 'ok') + '">' + (r.net === 'block' ? '阻断' : '放行') + '</span></td>' +
|
||||
'<td class="muted">' + esc((r.allow_hosts || []).join(', ') || '—') + '</td>' +
|
||||
'<td class="muted">' + esc((r.deny_hosts || []).join(', ') || '—') + '</td>' +
|
||||
'<td style="text-align:right"><div class="row-actions">' +
|
||||
'<button class="btn btn-ghost" data-ptoggle="' + esc(r.entity) + '" data-net="' + (r.net === 'block' ? 'allow' : 'block') + '">' + (r.net === 'block' ? '改放行' : '阻断') + '</button>' +
|
||||
'<button class="btn btn-ghost" data-pdel="' + esc(r.entity) + '">删除</button></div></td></tr>';
|
||||
'<button class="btn btn-ghost" data-ptoggle="' + esc(r.entity) + '" data-net="' + targetNet + '"' +
|
||||
' title="' + (locked ? '防自锁:禁止阻断系统关键服务' : '') + '"' + ((!pwe || locked) ? ' disabled' : '') + '>' + (r.net === 'block' ? '改放行' : '阻断') + '</button>' +
|
||||
'<button class="btn btn-ghost" data-pdel="' + esc(r.entity) + '"' + ((!pwe) ? ' disabled' : '') + '>删除</button></div></td></tr>';
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
}
|
||||
h += '<div class="field-row" style="margin-top:12px"><input type="text" id="pol-entity" placeholder="app:os.meow.ncm / svc:x / proc:y" style="max-width:280px">' +
|
||||
'<button class="btn btn-primary" id="pol-block">设为阻断</button>' +
|
||||
'<button class="btn btn-ghost" id="pol-allow">设为放行</button></div>';
|
||||
'<button class="btn btn-primary" id="pol-block"' + (!pwe ? ' disabled' : '') + '>设为阻断</button>' +
|
||||
'<button class="btn btn-ghost" id="pol-allow"' + (!pwe ? ' disabled' : '') + '>设为放行</button></div>';
|
||||
h += '<div class="field-row" style="margin-top:8px"><input type="text" id="pol-domain" placeholder="域名(+白 / -黑,如 +music.example.com)" style="max-width:280px">' +
|
||||
'<button class="btn" id="pol-dom">添加域名规则</button></div>';
|
||||
'<button class="btn" id="pol-dom"' + (!pwe ? ' disabled' : '') + '>添加域名规则</button></div>';
|
||||
h += '<p class="muted">防自锁实体(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)拒 block。</p>';
|
||||
h += '</div>';
|
||||
return h;
|
||||
@@ -1284,6 +1306,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
flush.disabled = false;
|
||||
fetchNetOnce();
|
||||
}
|
||||
if (e.target.closest('#pol-enable')) { enablePolicyWrite(fetchNetOnce); return; }
|
||||
/* ---- 网络策略写(require_auth + 二次确认;enforce=dns_only 如实显示,不谎称断连) ---- */
|
||||
const tgl = e.target.closest('[data-ptoggle]');
|
||||
if (tgl) {
|
||||
@@ -1309,6 +1332,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
if (blk) {
|
||||
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
|
||||
if (!ent) { toast('请输入实体(app:xxx / svc:xxx / proc:xxx)'); return; }
|
||||
if (netLockedEntity(ent)) { toast('防自锁:禁止阻断系统关键服务(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)'); return; }
|
||||
const ok = await confirmDlg('阻断实体', '确认将 ' + ent + ' 设为阻断(DNS 层拒绝,不会真断连)?', true);
|
||||
if (!ok) { fetchNetOnce(); return; }
|
||||
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: 'block' }); toast('已阻断:' + ent); }
|
||||
@@ -1330,6 +1354,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
|
||||
const hst = ($('#pol-domain') ? $('#pol-domain').value.trim() : '');
|
||||
if (!ent) { toast('请输入实体'); return; }
|
||||
if (netLockedEntity(ent)) { toast('防自锁:禁止为系统关键服务配置域名限制(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)'); return; }
|
||||
if (!hst || (hst[0] !== '+' && hst[0] !== '-')) { toast('域名须以 +(白)或 -(黑)开头'); return; }
|
||||
const ok = await confirmDlg('域名规则', '确认对 ' + ent + ' ' + (hst[0] === '+' ? '加入白名单' : '加入黑名单') + ' ' + hst.slice(1) + '?', hst[0] === '-');
|
||||
if (!ok) { fetchNetOnce(); return; }
|
||||
|
||||
Reference in new issue
Block a user