feat: M6.10–M6.19 批量收编 + M6.19 推卡验收
- M7 launcher 重写 5 期(虚拟化宫格/meow_theme 单一权威/横幅动画/自定义 PIN 键盘) - OOM 通用化(cachegov 上收 SDK)、自动切歌根治、meowpanel jv_free 泄漏修复、Hello 滚动卡顿修复 - ncm 歌词 UI / 媒体通知封面 / 封面分辨率 640 - 5 bug 修复:ncm 后台化 UI 叠层+点击穿透、再进 ncm 桌面叠层、播放页新旧 UI 叠层抢刷脏区(meow_ui_gfx 恢复边沿整帧重画)、PIN 亮屏亮度跳(keys.c meow_panel_on 按 settings_brightness_clamped 恢复)、通知中心封面显歌单封面(cover.c save_cover_raw 加 is_song + 命中块守卫) - 四 App 统一重链新 meow_ui.a 51caee4a;M6.19=0.8.19 固件 2d2331f2 推卡自刷验收全绿
This commit is contained in:
1 parent
c2ee7626a4
commit
3d70a3dd2c
176 files changed
+15507
-597
No files matched your search
Binary file not shown.
@@ -385,7 +385,12 @@ 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; }
|
||||
while (c) {
|
||||
jv_t *nx = c->next;
|
||||
if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */
|
||||
else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */
|
||||
c = nx;
|
||||
}
|
||||
free(v->str);
|
||||
free(v);
|
||||
}
|
||||
@@ -548,6 +553,37 @@ static jv_t *jv_obj_get(const jv_t *obj, const char *key)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* 将 jv 树序列化回 JSON 文本(追加到 sbuf)。覆盖解析器支持的 6 类型;字符串经 sA_str 转义+UTF-8 消毒。
|
||||
* 注意 J_OBJ 的 child 链是「键值对」节点(str=key、child=value),此处按键值对展开,不递归其自身。 */
|
||||
static void jv_to_json(sbuf_t *s, const jv_t *v)
|
||||
{
|
||||
if (!v) { sA(s, "null"); return; }
|
||||
switch (v->type) {
|
||||
case J_NULL: sA(s, "null"); break;
|
||||
case J_BOOL: sA(s, v->bval ? "true" : "false"); break;
|
||||
case J_NUM:
|
||||
if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum);
|
||||
else sA(s, "%g", v->dnum);
|
||||
break;
|
||||
case J_STR: sA_str(s, v->str ? v->str : ""); break;
|
||||
case J_ARR:
|
||||
sA(s, "[");
|
||||
{ int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } }
|
||||
sA(s, "]");
|
||||
break;
|
||||
case J_OBJ:
|
||||
sA(s, "{");
|
||||
{ int f2 = 1;
|
||||
for (const jv_t *p = v->child; p; p = p->next) {
|
||||
if (!f2) { sA(s, ","); }
|
||||
f2 = 0;
|
||||
sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child);
|
||||
} }
|
||||
sA(s, "}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 设置表(权威来源:meow_settings.c g_defs,逐键一致) ----------------
|
||||
* 特别说明(与 MEOWPANEL_PROTOCOL/实现出入记录):
|
||||
* - ui.style 值域源码为 "md3,miui",但 SYSTEM_SETTINGS §1 定案 + MEOWPANEL_PROTOCOL §5
|
||||
@@ -1236,6 +1272,130 @@ static int api_power(int sock)
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
/* ---------------- powermgr 期2/期3 扩展消息号(POWER_MANAGER_DESIGN §6.1;本文件局部定,同 meow_ipc.h 号段)----------------
|
||||
* meow_ipc.h 只在注释里预留 0x00D2-0x00D7,未定义宏;与 meow_powermgr.c 本地 `#ifndef` 同值对齐。 */
|
||||
#ifndef MSG_PM_POLICY_GET
|
||||
#define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */
|
||||
#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */
|
||||
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */
|
||||
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */
|
||||
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */
|
||||
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */
|
||||
#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */
|
||||
#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */
|
||||
#endif
|
||||
/* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */
|
||||
typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */
|
||||
|
||||
/* 通用 powermgr IPC:发 req_type + 可选 payload,收任意响应帧。返回值:
|
||||
* >0 = json 字节长度(out 已填,*rtype 置响应帧类型;帧为 m_pm_result_t 布局 code+subset+json_len+json);
|
||||
* <=0 = 负 MEOW_ERR_*(含连不上 NODEV / MSG_ERR 的 code),*rtype 置收到的帧类型(MSG_ERR=0x0003)。
|
||||
* 兼容服务端「紧凑负载」(json 从偏移 12 起,json_len 在偏移 8)。 */
|
||||
static int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen,
|
||||
uint16_t *rtype, char *out, int cap)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return MEOW_ERR_NODEV;
|
||||
int rc = MEOW_ERR_INTERNAL;
|
||||
if (rtype) *rtype = 0;
|
||||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||||
if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n <= 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
if (rtype) *rtype = type;
|
||||
if (type == MSG_ERR) {
|
||||
m_err_t e; memcpy(&e, buf, sizeof e);
|
||||
rc = (int)e.code; goto done;
|
||||
}
|
||||
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
|
||||
uint32_t c; memcpy(&c, buf, 4);
|
||||
if (c != MEOW_OK) { rc = (int)c; goto done; }
|
||||
uint16_t jl; memcpy(&jl, buf + 8, 2);
|
||||
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
|
||||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||||
memcpy(out, buf + 12, jl); out[jl] = 0;
|
||||
rc = (int)jl;
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* GET/POST /api/power/policy → powermgr 期3 策略档位(GET 读级;POST 动作级,前端需二次确认)。
|
||||
* GET:MSG_PM_POLICY_GET → policy JSON(profile 四档/低电阈值/亮度息屏上限,默认全关只透传)。 */
|
||||
static int api_power_policy_get(int sock)
|
||||
{
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js);
|
||||
if (rc <= 0)
|
||||
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);
|
||||
}
|
||||
|
||||
/* POST /api/power/policy:body {"item":"profile|low-pct|crit-pct|bri-cap|scr-cap","value":<枚举|数字>}。
|
||||
* 经 MSG_PM_POLICY_SET 写 settings.conf power.* 键(powermgr 只写键不直接动系统)。
|
||||
* 服务端对非法值回 200 + {"kind":"policy","ok":false,"why":..};权限不足回 MSG_ERR(MEOW_ERR_PERM)。 */
|
||||
static 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");
|
||||
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");
|
||||
char item[20] = "", val[40] = "";
|
||||
if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str);
|
||||
if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str);
|
||||
else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum);
|
||||
jv_free(root);
|
||||
if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)");
|
||||
if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value");
|
||||
|
||||
m_pm_policy_set_t s; memset(&s, 0, sizeof s);
|
||||
snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */
|
||||
snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js);
|
||||
if (rc <= 0) {
|
||||
if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */
|
||||
return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝");
|
||||
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", "电源管理服务应答包含非法字节");
|
||||
/* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */
|
||||
if (strstr(js, "\"ok\":false")) {
|
||||
char why[96] = "";
|
||||
jv_t *jr = jv_parse(js, (int)strlen(js));
|
||||
if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); }
|
||||
return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值");
|
||||
}
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
/* GET /api/power/history → MSG_PM_HISTORY 采样环(30s 环形 4h,不落盘)。
|
||||
* GET /api/power/events → MSG_PM_EVENTS 异常耗电/充放电/亮灭屏/背光跳变事件(内存环形)。
|
||||
* 两者语义对齐 powermgr 期 2;count=0(默认)由服务端选合理上限,读级任意调用者。 */
|
||||
static int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js);
|
||||
if (rc <= 0)
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", unavail);
|
||||
if (rt != want_type || !utf8_valid_n(js, strlen(js)))
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
static int api_power_history(int sock)
|
||||
{ return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); }
|
||||
static int api_power_events(int sock)
|
||||
{ return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); }
|
||||
|
||||
/* ---- am/meow_svcd 二进制解析(popen 薄壳,复用有界收割 run_cmd) ----
|
||||
* env MEOW_AMS_BIN / MEOW_SVCD_BIN 可覆盖(宿主/qemu 测试用;设备默认 /usr/bin,回退卡上路径)。 */
|
||||
static const char *ams_bin(void)
|
||||
@@ -1514,6 +1674,211 @@ static int api_net_log(int sock)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- netmgrd 期2/期3:接口列表 + 管控策略(只读展示 / 写级经 require_auth) ----------------
|
||||
* GET /api/net/ifaces → 0xE2(name 空=全部)接口数组 + 0xE0 状态里的 rssi(WiFi 信号)+ rfkill + dns。
|
||||
* 上游任一源坏字节 → available:false(绝不伪造)。netmgrd 不在 → available:false(HTTP 200,不报错)。
|
||||
* GET /api/net/policy → 0xEA SHOW(只读)→ 策略文本 parse 成 JSON(enforce=dns_only 如实透传)。
|
||||
* POST /api/net/policy → 0xEA 写级(SET_NET/SET_DOMAIN/DEL);HTTP 层已过 require_auth,IPC 层由 netmgrd
|
||||
* 依 SO_PEERCRED 判系统身份(面板精确登记 os.meow.panel),拒绝时如实回传错误码,绝不掩盖。 */
|
||||
static int api_net_ifaces(int sock)
|
||||
{
|
||||
m_net_iface_req_t q; memset(&q, 0, sizeof q);
|
||||
uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr);
|
||||
if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0])
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
/* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */
|
||||
if (!utf8_valid_n(arr, strlen(arr)))
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
|
||||
/* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */
|
||||
uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX];
|
||||
int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj);
|
||||
long long rssi = -9999; int got_rssi = 0;
|
||||
sbuf_t rfkill = {0}, dns = {0};
|
||||
if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) {
|
||||
jv_t *jr = jv_parse(sj, (int)strlen(sj));
|
||||
if (jr) {
|
||||
jv_t *w = jv_obj_get(jr, "wifi");
|
||||
jv_t *r = w ? jv_obj_get(w, "rssi") : NULL;
|
||||
if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; }
|
||||
jv_t *rf = jv_obj_get(jr, "rfkill");
|
||||
if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf);
|
||||
jv_t *bt = jv_obj_get(jr, "bt");
|
||||
jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL;
|
||||
if (dn && dn->type == J_ARR) jv_to_json(&dns, dn);
|
||||
jv_free(jr);
|
||||
}
|
||||
}
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"available\":true,\"rssi\":");
|
||||
if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null");
|
||||
sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr);
|
||||
sbuf_free(&rfkill); sbuf_free(&dns);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 策略 SHOW 文本 → JSON。netmgrd 首行:`# 策略(NETMGR_DESIGN §6)log_level=%s enforce=dns_only(...)`;
|
||||
* 其后每实体一行:`<entity> net=block domain=+h1,-h2` 或 `(allow)`(无规则)。 */
|
||||
static 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\":");
|
||||
/* 首行找 enforce=... */
|
||||
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
||||
const char *en = strstr(p, "enforce=");
|
||||
if (en) {
|
||||
en += 8;
|
||||
int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; }
|
||||
enf[i] = 0;
|
||||
}
|
||||
const char *lg = strstr(p, "log_level=");
|
||||
if (lg) {
|
||||
lg += 10;
|
||||
int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; }
|
||||
lvl[i] = 0;
|
||||
}
|
||||
sA_str(s, enf);
|
||||
sA(s, ",\"log_level\":");
|
||||
sA_str(s, lvl);
|
||||
sA(s, ",\"rules\":[");
|
||||
int first = 1;
|
||||
const char *ln = txt;
|
||||
while (ln && *ln) {
|
||||
const char *nl = strchr(ln, '\n');
|
||||
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||
if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; }
|
||||
/* 跳注释行(以 # 开头)与空;数据行为 `<entity> net=... domain=...`(entity 含冒号无空格) */
|
||||
const char *c = ln;
|
||||
while (c < ln + ll && (*c == ' ' || *c == '\t')) c++;
|
||||
int is_comment = (c < ln + ll && *c == '#');
|
||||
int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0);
|
||||
if (!is_comment && !is_none && c < ln + ll) {
|
||||
/* entity = 首个空白符之前 */
|
||||
char entity[64] = ""; size_t ei = 0;
|
||||
const char *e = c;
|
||||
while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; }
|
||||
entity[ei] = 0;
|
||||
/* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */
|
||||
char net[8] = "allow"; int has_net = 0;
|
||||
char allow_h[512] = "", deny_h[512] = "";
|
||||
const char *k = e;
|
||||
if (k < ln + ll) {
|
||||
const char *seg = k;
|
||||
while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++;
|
||||
const char *nh = strstr(seg, "net=");
|
||||
if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; }
|
||||
const char *dh = strstr(seg, "domain=");
|
||||
if (dh) {
|
||||
dh += 7;
|
||||
/* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */
|
||||
size_t ao = 0, do2 = 0;
|
||||
const char *h = dh;
|
||||
while (*h && *h != ' ' && *h != '\n' && *h != '\r') {
|
||||
if (*h == '+') {
|
||||
h++;
|
||||
if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ',';
|
||||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++;
|
||||
} else if (*h == '-') {
|
||||
h++;
|
||||
if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ',';
|
||||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++;
|
||||
} else h++;
|
||||
if (*h == ',') h++;
|
||||
}
|
||||
allow_h[ao] = 0; deny_h[do2] = 0;
|
||||
}
|
||||
if (!first) sA(s, ",");
|
||||
first = 0;
|
||||
sA(s, "{\"entity\":"); sA_str(s, entity);
|
||||
sA(s, ",\"net\":"); sA_str(s, has_net ? net : "");
|
||||
sA(s, ",\"has_net\":%s", has_net ? "true" : "false");
|
||||
sA(s, ",\"allow_hosts\":[");
|
||||
{ const char *hp = allow_h; int hf = 1;
|
||||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||||
if (!hf) { sA(s, ","); }
|
||||
hf = 0; sA_str_n(s, hp, hl);
|
||||
hp = comma ? comma + 1 : hp + hl; } }
|
||||
sA(s, "],\"deny_hosts\":[");
|
||||
{ const char *hp = deny_h; int hf = 1;
|
||||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||||
if (!hf) { sA(s, ","); }
|
||||
hf = 0; sA_str_n(s, hp, hl);
|
||||
hp = comma ? comma + 1 : hp + hl; } }
|
||||
sA(s, "]}");
|
||||
}
|
||||
}
|
||||
ln = nl ? nl + 1 : ln + ll;
|
||||
}
|
||||
sA(s, "]}");
|
||||
}
|
||||
|
||||
/* GET /api/net/policy(只读):0xEA SHOW → JSON(rules + enforce=dns_only 如实)。 */
|
||||
static int api_net_policy_get(int sock)
|
||||
{
|
||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||
q.op = NET_POLICY_SHOW;
|
||||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK)
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
sbuf_t s = {0};
|
||||
net_policy_parse(txt, &s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* POST /api/net/policy:body {"op":"set_net|set_domain|del","entity":"<app:..|svc:..|proc:..|global>","arg":"<allow|block|+host|-host>"}。
|
||||
* op 缺失默认 set_net;op 非法 → 422。arg 校验交给 netmgrd(防自锁硬拒在 netmgrd 服务端)。 */
|
||||
static 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");
|
||||
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");
|
||||
char op[16] = "set_net", entity[64] = "", arg[64] = "";
|
||||
if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str);
|
||||
if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str);
|
||||
if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str);
|
||||
jv_free(root);
|
||||
if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:<pkg>/svc:<svc>/proc:<comm>)");
|
||||
if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法");
|
||||
int nop = 0;
|
||||
if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET;
|
||||
else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN;
|
||||
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");
|
||||
|
||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||
q.op = (uint32_t)nop;
|
||||
snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */
|
||||
snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */
|
||||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_POLICY_RESULT)
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
if (code == (uint32_t)MEOW_ERR_PERM)
|
||||
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?");
|
||||
if (code == (uint32_t)MEOW_ERR_INVAL)
|
||||
return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法");
|
||||
if (code != MEOW_OK)
|
||||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"message\":");
|
||||
sA_str(&s, txt);
|
||||
sA(&s, "}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---------------- amsd / svcd 服务表 + 首页聚合(批1 只读) ---------------- */
|
||||
|
||||
/* 就地去掉 ANSI 转义序列(\033[...final);svcd status all 走管道时 CLI 会去色,此处双保险)。 */
|
||||
@@ -1559,6 +1924,8 @@ static void join_tokens(char **toks, int from, int to, char *out, size_t cap)
|
||||
out[o] = 0;
|
||||
}
|
||||
|
||||
static void ams_fg_fill(sbuf_t *s); /* 定义于下;ams_fill 内引用其前台摘要(前向声明) */
|
||||
|
||||
/* GET /api/ams:`am list`(注册表+状态)parse → App 表。amsd 不在 → available:false(绝无假数据)。 */
|
||||
static void ams_fill(sbuf_t *s)
|
||||
{
|
||||
@@ -1597,8 +1964,57 @@ static void ams_fill(sbuf_t *s)
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]}", count, apps.b ? apps.b : "");
|
||||
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : "");
|
||||
sbuf_free(&apps);
|
||||
ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */
|
||||
sA(s, "}");
|
||||
}
|
||||
|
||||
/* `am status` → `,"fg":{...},"running":[...]`(供 /api/ams 与 overview 的 ams 块复用;仅前台摘要,轻)。
|
||||
* am status 行:`RUN %s %s %ld %llu %s\n` = pkg state pid dur activity(可含空格)。
|
||||
* activity=join(tok[5..n));fg=首个 state=="fg"。amsd 不在 → fg:null,running:[](绝无假数据)。 */
|
||||
static void ams_fg_fill(sbuf_t *s)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin());
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; }
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0;
|
||||
sbuf_t run = {0}; int first = 1;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "RUN ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 6) { /* RUN pkg state pid dur activity... */
|
||||
char act[160];
|
||||
join_tokens(toks, 5, n, act, sizeof act);
|
||||
const char *st = toks[2];
|
||||
if (!first) sA(&run, ",");
|
||||
first = 0;
|
||||
sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]);
|
||||
sA(&run, ",\"state\":"); sA_str(&run, st);
|
||||
sA(&run, ",\"pid\":%d", atoi(toks[3]));
|
||||
sA(&run, ",\"activity\":"); sA_str(&run, act);
|
||||
sA(&run, "}");
|
||||
if (strcmp(st, "fg") == 0 && !fg_pkg[0]) {
|
||||
snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]);
|
||||
snprintf(fg_act, sizeof fg_act, "%s", act);
|
||||
fg_pid = atoi(toks[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, ",\"fg\":");
|
||||
if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); }
|
||||
else sA(s, "null");
|
||||
sA(s, ",\"running\":[%s]", run.b ? run.b : "");
|
||||
sbuf_free(&run);
|
||||
}
|
||||
|
||||
static int api_ams(int sock)
|
||||
@@ -1610,6 +2026,100 @@ static int api_ams(int sock)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 包名合法性校验(ams history/start/kill 共用):反向域名命名字符集,拒 /、: 与空白。 */
|
||||
static int ams_pkg_ok(const char *pkg)
|
||||
{
|
||||
if (!pkg || !pkg[0]) return 0;
|
||||
if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0;
|
||||
for (const char *p = pkg; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0;
|
||||
if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* GET /api/ams/:pkg → `am history <pkg>`(amsd 现成:incident 登记 + 崩溃根因 detail)。
|
||||
* 输出行:`EVT <epoch> <up> <kind> <pkg> <detail...>`(detail 可含空格,为崩溃根因/事件文本)。
|
||||
* 上游 text 含崩溃 detail,须 utf8 校验坏字节降级 available:false(绝不发非法 JSON)。
|
||||
* 解析部分独立成 ams_history_parse(供 selftest 喂截断 detail 断言合法 JSON)。 */
|
||||
static void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */
|
||||
{
|
||||
if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
sbuf_t ev = {0};
|
||||
int first = 1, count = 0;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "EVT ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 5) { /* EVT epoch up kind pkg detail... */
|
||||
char det[256];
|
||||
join_tokens(toks, 5, n, det, sizeof det);
|
||||
if (!first) sA(&ev, ",");
|
||||
first = 0; count++;
|
||||
sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2]));
|
||||
sA_str(&ev, toks[3]);
|
||||
sA(&ev, ",\"detail\":"); sA_str(&ev, det);
|
||||
sA(&ev, "}");
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg);
|
||||
sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : "");
|
||||
sbuf_free(&ev);
|
||||
}
|
||||
static void ams_history_fill(sbuf_t *s, const char *pkg)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg);
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; }
|
||||
ams_history_parse(out, s, pkg);
|
||||
}
|
||||
static int api_ams_history(int sock, const char *pkg)
|
||||
{
|
||||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
sbuf_t s = {0};
|
||||
ams_history_fill(&s, pkg);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* POST /api/ams/:pkg/start → `am start <pkg>`(amsd→launcher MSG_AMS_START 拉起;launcher 离线回 ERR)。
|
||||
* POST /api/ams/:pkg/kill → `am kill <pkg>`(SIGTERM 语义,前台需前端二次确认)。均 require_auth 之后。 */
|
||||
static int api_ams_ctl(int sock, const char *pkg, int kill)
|
||||
{
|
||||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
char out[256];
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg);
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code == 0 && strstr(out, "OK ")) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start");
|
||||
sA_str(&s, pkg);
|
||||
sA(&s, ",\"message\":"); sA_str(&s, out);
|
||||
sA(&s, "}");
|
||||
int rc2 = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc2;
|
||||
}
|
||||
/* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */
|
||||
const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败");
|
||||
if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail);
|
||||
if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail);
|
||||
if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail);
|
||||
if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达");
|
||||
return send_err(sock, 500, "INTERNAL", detail);
|
||||
}
|
||||
|
||||
/* GET /api/svcd:`meow_svcd status all` parse → 服务表(state/pid/restart 计数/exec/来源)。
|
||||
* svcd 未运行(exit!=0)→ available:false(绝无假数据)。 */
|
||||
static void svcd_fill(sbuf_t *s)
|
||||
@@ -1691,6 +2201,17 @@ static int api_overview(int sock)
|
||||
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw)))
|
||||
sA(&s, "\"power\":%s,", pw);
|
||||
else sA(&s, "\"power\":{\"available\":false},");
|
||||
/* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */
|
||||
{
|
||||
uint32_t ecnt = 2;
|
||||
char ej[MEOW_IPC_MAX];
|
||||
uint16_t ert = 0;
|
||||
int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej);
|
||||
if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej)))
|
||||
sA(&s, "\"events\":%s,", ej);
|
||||
else
|
||||
sA(&s, "\"events\":{\"available\":false},");
|
||||
}
|
||||
sA(&s, "\"audio\":");
|
||||
audio_fill(&s);
|
||||
if (net_block_fill(&s) == 0)
|
||||
@@ -5496,12 +6017,22 @@ static int handle(int sock, req_t *r)
|
||||
|
||||
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock);
|
||||
if (!strcmp(r->method, "GET")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0])
|
||||
return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */
|
||||
}
|
||||
|
||||
/* ---- 网络管理(meow_netmgrd 期2 透传:读级任意调用者;flush 写级走 require_auth) ---- */
|
||||
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
|
||||
@@ -5533,6 +6064,8 @@ static int handle(int sock, req_t *r)
|
||||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||||
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||||
if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock);
|
||||
if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||
@@ -5540,6 +6073,19 @@ static int handle(int sock, req_t *r)
|
||||
char seg[256];
|
||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||
return api_key_single(sock, atoi(seg), r);
|
||||
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
|
||||
if (!strncmp(p, "/api/ams/", 9)) {
|
||||
size_t pl = strlen(p);
|
||||
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
return api_ams_ctl(sock, pkg, 0);
|
||||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
return api_ams_ctl(sock, pkg, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 设置 ---- */
|
||||
@@ -5697,7 +6243,43 @@ static int panel_selftest(void)
|
||||
jv_free(ar);
|
||||
sbuf_free(&a);
|
||||
|
||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」→ 合法 JSON + 无非法字节\n");
|
||||
/* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。
|
||||
* 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */
|
||||
{
|
||||
static const char ptext[] =
|
||||
"# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n"
|
||||
"app:com.example.trunc net=block domain=+good.example,-bad\xE7\n"
|
||||
"svc:meowpanel net=allow domain=+svc.local\n";
|
||||
sbuf_t p = {0};
|
||||
net_policy_parse(ptext, &p);
|
||||
if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; }
|
||||
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||||
if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; }
|
||||
jv_t *rules = jv_obj_get(pr, "rules");
|
||||
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_free(pr);
|
||||
sbuf_free(&p);
|
||||
}
|
||||
|
||||
/* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。
|
||||
* detail 合法时 → 合法 JSON 可解析。 */
|
||||
{
|
||||
char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n";
|
||||
sbuf_t h = {0};
|
||||
ams_history_parse(bad, &h, "com.example.trunc");
|
||||
if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; }
|
||||
sbuf_free(&h);
|
||||
|
||||
char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n";
|
||||
sbuf_t h2 = {0};
|
||||
ams_history_parse(good, &h2, "com.example.trunc");
|
||||
if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; }
|
||||
sbuf_free(&h2);
|
||||
}
|
||||
|
||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -752,11 +752,16 @@
|
||||
function healthCard() {
|
||||
const ams = homeData && homeData.ams;
|
||||
const svcd = homeData && homeData.svcd;
|
||||
let appTxt = '—', svcBad = 0, svcHi = 0, svcTxt = '—';
|
||||
let appTxt = '—', svcBad = 0, svcHi = 0, svcTxt = '—', fgHtml = '';
|
||||
if (isAvail(ams)) {
|
||||
const apps = ams.apps || [];
|
||||
const run = apps.filter(a => a.state === 'fg' || a.state === 'bg').length;
|
||||
appTxt = run + ' 在跑 / ' + (ams.count || apps.length) + ' 已装';
|
||||
const fg = ams.fg;
|
||||
if (fg && fg.pkg) {
|
||||
fgHtml = '<span class="home-chip ok">前台</span> ' + esc(fg.pkg) +
|
||||
(fg.activity ? ' · <span class="muted">' + esc(fg.activity) + '</span>' : '');
|
||||
}
|
||||
}
|
||||
let svcHtml;
|
||||
if (isAvail(svcd)) {
|
||||
@@ -773,7 +778,8 @@
|
||||
svcTxt = '—';
|
||||
}
|
||||
return homeCard('在跑 App 与服务', '<div class="home-card-grid">' +
|
||||
homeKv('在跑 App', esc(appTxt)) + homeKv('服务健康', svcHtml + esc(svcTxt)) + '</div>');
|
||||
homeKv('在跑 App', esc(appTxt)) + homeKv('服务健康', svcHtml + esc(svcTxt)) +
|
||||
(fgHtml ? homeKv('前台应用', fgHtml) : '') + '</div>');
|
||||
}
|
||||
function storageCard() {
|
||||
const perf = homeData && homeData.perf;
|
||||
@@ -797,11 +803,37 @@
|
||||
'<div class="stat"><div class="stat-label">温度</div><div class="stat-value">' + (tmp.cpu != null ? (+tmp.cpu).toFixed(1) + '<span class="stat-unit">℃</span>' : '—') + '</div></div>' +
|
||||
'</div>');
|
||||
}
|
||||
/* 首页功耗摘要条(est_mw + breakdown:复用 estimateCardBody/breakdownCardBody 保持一致口径,诚实标注 confidence)。
|
||||
* 上游 power 不可用 → 不渲染该条(绝不假数据);HibyOS 只读 degraded → 显示说明。 */
|
||||
function powerSummaryCard() {
|
||||
const pw = homeData && homeData.power;
|
||||
if (!isAvail(pw)) return '';
|
||||
const est = pw.estimate;
|
||||
if (!est) return '';
|
||||
if (est.est_mw == null) return homeCard('功耗视角', '<div class="home-sub">' + (est.degraded ? '功耗估算不可用(HibyOS 只读模式)' : '—') + '</div>');
|
||||
return homeCard('功耗视角', estimateCardBody(est) + (pw.breakdown ? breakdownCardBody(pw.breakdown) : ''));
|
||||
}
|
||||
/* 最近异常事件条(power.events 摘要 top3 进 overview;完整列表走 /api/power/events)。 */
|
||||
function eventsBar() {
|
||||
const ev = homeData && homeData.events;
|
||||
if (!ev || ev.available === false) return '';
|
||||
const list = (ev && ev.events) || [];
|
||||
if (!list.length) return '';
|
||||
const typeLabel = { charger: '充电', lowbat: '低电', screen: '屏幕', brightness: '背光' };
|
||||
let items = list.slice(0, 3).map(e => {
|
||||
const tn = typeLabel[e[1]] || e[1];
|
||||
const txt = String(e[3] == null ? '' : e[3]);
|
||||
const cls = (e[1] === 'lowbat') ? 'warn' : (e[1] === 'charger' ? 'ok' : 'mut');
|
||||
return '<span class="home-chip ' + cls + '" title="' + esc(txt) + '">' + esc(tn) + (txt ? ' · ' + esc(txt.length > 28 ? txt.slice(0, 28) + '…' : txt) : '') + '</span>';
|
||||
}).join(' ');
|
||||
return '<div class="card"><h3 class="card-title">最近事件</h3><div class="home-sub">' + items + '</div></div>';
|
||||
}
|
||||
function renderHomeCards() {
|
||||
const body = $('#home-body');
|
||||
if (!body || state.route !== 'home') return;
|
||||
if (!homeData) { body.innerHTML = '<p class="placeholder">加载总览…</p>'; return; }
|
||||
let h = '<div class="home-grid">' +
|
||||
let h = powerSummaryCard() + eventsBar(); // 摘要条(功耗/事件)置顶
|
||||
h += '<div class="home-grid">' +
|
||||
deviceCard() + batteryCard() + audioCard() + netCard() + healthCard() + storageCard() + perfCard() +
|
||||
'</div>';
|
||||
body.innerHTML = h;
|
||||
@@ -921,31 +953,126 @@
|
||||
}
|
||||
return '<div class="home-sub">合计 ' + total + ' mW(分解为经验估算)</div>' + stack + legend;
|
||||
}
|
||||
function renderPowerBody(d) {
|
||||
let powerEditing = false; // 用户在编辑策略表单时暂停 5s 重渲染(防输入被轮询清空)
|
||||
/* 电源策略卡:profile 四档 + 低电/临界阈值 + 亮度/息屏上限。前端确认后 POST /api/power/policy。
|
||||
* 默认全关(enacted=0)如实标注;note 透传 powermgr 口径(只写 power.* 键,不直接写系统)。 */
|
||||
function powerProfileLabel(p) {
|
||||
return { performance: '性能', balanced: '均衡', powersave: '省电', ultra: '超省电' }[p] || p;
|
||||
}
|
||||
function powerPolicyCard(pol) {
|
||||
if (!pol) return '<div class="card"><h3 class="card-title">电源策略</h3>' + homeUnavailable() + '</div>';
|
||||
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>') +
|
||||
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>';
|
||||
}
|
||||
function powerEventsCard(ev) {
|
||||
if (!ev) return '';
|
||||
if (ev.available === false) return '<div class="card"><h3 class="card-title">电源事件</h3>' + homeUnavailable() + '</div>';
|
||||
const list = (ev && ev.events) || [];
|
||||
if (!list.length) return '<div class="card"><h3 class="card-title">电源事件</h3><div class="empty">暂无事件</div></div>';
|
||||
const typeLabel = { charger: '充电', lowbat: '低电', screen: '屏幕', brightness: '背光' };
|
||||
let rows = list.map(e => {
|
||||
const tn = typeLabel[e[1]] || e[1];
|
||||
return '<tr><td class="muted">' + esc(tn) + '</td><td>' + (e[2] != null ? esc(e[2]) + '%' : '—') + '</td><td>' + esc(e[3] || '') + '</td></tr>';
|
||||
}).join('');
|
||||
return '<div class="card"><h3 class="card-title">电源事件</h3>' +
|
||||
'<table class="table"><thead><tr><th>类型</th><th>电量</th><th>说明</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
|
||||
}
|
||||
function powerHistoryCard(h) {
|
||||
if (!h) return '';
|
||||
if (h.available === false) return '<div class="card"><h3 class="card-title">功耗历史</h3>' + homeUnavailable() + '</div>';
|
||||
let body = '<table class="table"><tbody>' +
|
||||
row('采样窗口', (h.n_samples || 0) + ' 点 · ' + fmtDur(h.window_sec)) +
|
||||
row('电量范围', (h.min_pct != null ? h.min_pct : '—') + '% ~ ' + (h.max_pct != null ? h.max_pct : '—') + '%') +
|
||||
row('起止电量', (h.start_pct != null ? h.start_pct : '—') + '% → ' + (h.end_pct != null ? h.end_pct : '—') + '%') +
|
||||
row('平均功耗', (h.avg_mw != null ? h.avg_mw + ' mW' : '—')) +
|
||||
'</tbody></table>';
|
||||
return '<div class="card"><h3 class="card-title">功耗历史</h3>' + body + '</div>';
|
||||
}
|
||||
function renderPowerBody(d, ev, hi) {
|
||||
let h = '<div class="page" id="power-page">';
|
||||
if (d && d.mode === 'readonly') h += '<div class="page-banner"><span class="ico">ℹ️</span><span>当前为电源只读观测(HibyOS 模式),仅显示电池采样,无功耗估算/分解。</span></div>';
|
||||
h += '<div class="card"><h3 class="card-title">电池</h3>' + ((d && d.battery) ? batteryCardBody(d.battery) : homeUnavailable()) + '</div>';
|
||||
h += '<div class="card"><h3 class="card-title">功耗估算</h3>' + ((d && d.estimate) ? estimateCardBody(d.estimate) : homeUnavailable()) + '</div>';
|
||||
h += '<div class="card"><h3 class="card-title">子系统分解</h3>' + ((d && d.breakdown) ? breakdownCardBody(d.breakdown) : homeUnavailable()) + '</div>';
|
||||
if (d && d.policy == null) h += '<p class="muted" style="margin-top:6px">电源策略(profile 四档/低电阈值)为 期3 功能,此处以不可用占位。</p>';
|
||||
h += ((d && d.policy) ? powerPolicyCard(d.policy) : '<div class="card"><h3 class="card-title">电源策略</h3>' + homeUnavailable() + '</div>');
|
||||
h += powerEventsCard(ev);
|
||||
h += powerHistoryCard(hi);
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
let powerData = null, powerEvents = null, powerHistory = null;
|
||||
async function fetchPowerOnce() {
|
||||
const el = $('#power-page');
|
||||
if (!el || state.route !== 'power') return;
|
||||
let d;
|
||||
try { d = await api('GET', '/api/power'); }
|
||||
catch (e) { if (el && state.route === 'power') el.innerHTML = '<div class="page" id="power-page">' + emptyState('加载失败:' + errText(e), e.code) + '</div>'; return; }
|
||||
let d, ev, hi;
|
||||
const r = await Promise.allSettled([api('GET', '/api/power'), api('GET', '/api/power/events'), api('GET', '/api/power/history')]);
|
||||
if (r[0].status !== 'fulfilled') {
|
||||
if (el && state.route === 'power') el.innerHTML = '<div class="page" id="power-page">' + emptyState('加载失败:' + errText(r[0].reason), (r[0].reason && r[0].reason.code) || '') + '</div>';
|
||||
return;
|
||||
}
|
||||
if (state.route !== 'power') return;
|
||||
el.innerHTML = renderPowerBody(d);
|
||||
d = r[0].value; ev = (r[1].status === 'fulfilled') ? r[1].value : null; hi = (r[2].status === 'fulfilled') ? r[2].value : null;
|
||||
if (!powerEditing) el.innerHTML = renderPowerBody(d, ev, hi);
|
||||
}
|
||||
function renderPower() {
|
||||
stopPower();
|
||||
powerEditing = false;
|
||||
powerData = null; powerEvents = null; powerHistory = null;
|
||||
$('#content').innerHTML = '<div class="page" id="power-page"><p class="placeholder">加载电源数据…</p></div>';
|
||||
fetchPowerOnce();
|
||||
powerTimer = setInterval(fetchPowerOnce, 5000);
|
||||
}
|
||||
function bindPower() {
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
const pb = e.target.closest('.pol-btn');
|
||||
if (pb) {
|
||||
const p = pb.dataset.pol;
|
||||
const ok = await confirmDlg('切换省电档位', '确认将省电档位切换为「' + powerProfileLabel(p) + '」?(写 settings.conf power.profile,launcher/sysui 期4消费生效)', false);
|
||||
if (!ok) { powerEditing = false; fetchPowerOnce(); return; }
|
||||
powerEditing = false;
|
||||
try { await api('POST', '/api/power/policy', { item: 'profile', value: p }); toast('已切档:' + powerProfileLabel(p)); }
|
||||
catch (err) { toast('切档失败:' + errText(err)); }
|
||||
fetchPowerOnce();
|
||||
return;
|
||||
}
|
||||
const apply = e.target.closest('#pol-apply');
|
||||
if (apply) {
|
||||
const low = parseInt($('#pol-low') ? $('#pol-low').value : '', 10);
|
||||
const crit = parseInt($('#pol-crit') ? $('#pol-crit').value : '', 10);
|
||||
const bri = parseInt($('#pol-bri') ? $('#pol-bri').value : '', 10);
|
||||
const scr = parseInt($('#pol-scr') ? $('#pol-scr').value : '', 10);
|
||||
if (isNaN(low) || isNaN(crit)) { toast('请输入有效的低电/临界阈值'); return; }
|
||||
const ok = await confirmDlg('保存电源阈值', '确认写入 低电=' + low + '% / 临界=' + crit + '% / 亮度上限=' + (isNaN(bri) ? 0 : bri) + ' / 息屏上限=' + (isNaN(scr) ? 0 : scr) + 's?', false);
|
||||
if (!ok) { powerEditing = false; fetchPowerOnce(); return; }
|
||||
powerEditing = false;
|
||||
try {
|
||||
await api('POST', '/api/power/policy', { item: 'low-pct', value: low });
|
||||
await api('POST', '/api/power/policy', { item: 'crit-pct', value: crit });
|
||||
await api('POST', '/api/power/policy', { item: 'bri-cap', value: bri || 0 });
|
||||
await api('POST', '/api/power/policy', { item: 'scr-cap', value: scr || 0 });
|
||||
toast('电源阈值已保存');
|
||||
} catch (err) { toast('保存失败:' + errText(err)); }
|
||||
fetchPowerOnce();
|
||||
return;
|
||||
}
|
||||
});
|
||||
$('#content').addEventListener('focusin', function (e) {
|
||||
if (e.target.closest('.pol-num')) powerEditing = true; // 编辑中暂停 5s 重渲染(防输入被清空)
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 2f. 进程 / 服务页(#/proc;/api/svcd + /api/ams,只读,无 restart/kill) ----------------
|
||||
* svcd 服务表(state/pid/restart 计数≥3 高亮/exec/来源 board|tf)+ ams App 表(state/pid/crashes/launches/last_kind);
|
||||
@@ -1000,7 +1127,11 @@
|
||||
'<td class="muted">' + esc(a.launches || 0) + '</td>' +
|
||||
'<td class="' + ((a.crashes || 0) > 0 ? 'crash-hi' : 'muted') + '">' + (a.crashes || 0) + ((a.crashes || 0) > 0 ? ' ⚠' : '') + '</td>' +
|
||||
'<td class="muted">' + esc(a.last_kind || '—') + (a.last_code != null && a.last_code !== 0 ? ' (' + esc(a.last_code) + ')' : '') + '</td>' +
|
||||
'<td><div class="row-actions"><button class="btn btn-ghost" data-applog="' + esc(a.pkg) + '">' + (openHere ? '收起' : 'stdout 尾部') + '</button></div></td></tr>';
|
||||
'<td><div class="row-actions">' +
|
||||
'<button class="btn btn-ghost" data-applog="' + esc(a.pkg) + '">' + (openHere ? '收起' : 'stdout 尾部') + '</button>' +
|
||||
'<button class="btn btn-ghost" data-amsstart="' + esc(a.pkg) + '">启动</button>' +
|
||||
(a.pid > 0 ? '<button class="btn btn-ghost" data-amskill="' + esc(a.pkg) + '" data-fg="' + (a.state === 'fg' ? '1' : '0') + '">结束</button>' : '') +
|
||||
'</div></td></tr>';
|
||||
if (openHere) {
|
||||
const lines = (expandedProc.lines || []);
|
||||
h += '<tr class="proc-tail"><td colspan="8"><pre class="log-view">' + esc(lines.join('\n') || '(空)') +
|
||||
@@ -1009,7 +1140,7 @@
|
||||
}
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
h += '<p class="muted">点击「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。仅只读,无 restart/kill。</p>';
|
||||
h += '<p class="muted">「stdout 尾部」查看该 App 最新 stdout_*.log 末尾(崩溃尾巴)。「启动/结束」经 amsd(require_auth);结束前台 App 需二次确认。</p>';
|
||||
} else h += homeUnavailable();
|
||||
h += '</div></div>';
|
||||
return h;
|
||||
@@ -1072,9 +1203,27 @@
|
||||
procTimer = setInterval(fetchProcOnce, 5000);
|
||||
}
|
||||
function bindProc() {
|
||||
$('#content').addEventListener('click', function (e) {
|
||||
const b = e.target.closest('[data-applog]'); if (!b) return;
|
||||
viewAppStdout(b.dataset.applog, b);
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
const b = e.target.closest('[data-applog]');
|
||||
if (b) { viewAppStdout(b.dataset.applog, b); return; }
|
||||
const st = e.target.closest('[data-amsstart]');
|
||||
if (st) {
|
||||
const pkg = st.dataset.amsstart;
|
||||
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/start'); toast('已请求启动 ' + pkg); }
|
||||
catch (err) { toast('启动失败:' + errText(err)); }
|
||||
fetchProcOnce();
|
||||
return;
|
||||
}
|
||||
const kl = e.target.closest('[data-amskill]');
|
||||
if (kl) {
|
||||
const pkg = kl.dataset.amskill, fg = kl.dataset.fg === '1';
|
||||
const ok = await confirmDlg('结束 App', (fg ? '『' + pkg + '』正位于前台,' : '') + '确认结束 ' + pkg + '?(SIGTERM 语义)', true);
|
||||
if (!ok) return;
|
||||
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/kill'); toast('已请求结束 ' + pkg); }
|
||||
catch (err) { toast('结束失败:' + errText(err)); }
|
||||
fetchProcOnce();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1110,12 +1259,18 @@
|
||||
return 'net-state warn';
|
||||
}
|
||||
|
||||
function netStatusCards(d) {
|
||||
let h = '';
|
||||
// 接口卡(lo/wlan0/usb-eth:状态+IP+速率)
|
||||
const ifs = (d && d.ifaces) || [];
|
||||
h += '<div class="card"><h3 class="card-title">网络接口</h3><table class="table"><thead>' +
|
||||
'<tr><th>接口</th><th>类型</th><th>状态</th><th>IP 地址</th><th>网关</th><th>RX / TX</th></tr></thead><tbody>';
|
||||
/* 网络接口卡(/api/net/ifaces:kind/ip/gw/rssi/rfkill/dns + RX/TX)。
|
||||
* enforce 执行语义如实:仅 DNS 层拒绝(SOCK_DESTROY 在本内核为 no-op),绝不谎称「已断连」。 */
|
||||
function netIfaceCards(ifd) {
|
||||
if (!ifd || ifd.available === false) return '<div class="card"><h3 class="card-title">网络接口</h3>' + homeUnavailable() + '</div>';
|
||||
const ifs = (ifd && ifd.ifaces) || [];
|
||||
let h = '<div class="card"><h3 class="card-title">网络接口</h3>';
|
||||
let meta = '<div class="home-sub" style="margin-bottom:8px">';
|
||||
meta += (ifd.rssi != null) ? '信号 ' + esc(ifd.rssi) + ' dBm' : '信号 —';
|
||||
if (ifd.dns && ifd.dns.length) meta += ' · DNS ' + esc(ifd.dns.join(' / '));
|
||||
meta += '</div>';
|
||||
h += meta;
|
||||
h += '<table class="table"><thead><tr><th>接口</th><th>类型</th><th>状态</th><th>IP 地址</th><th>网关</th><th>RX / TX</th></tr></thead><tbody>';
|
||||
if (!ifs.length) h += '<tr><td colspan="6" class="empty">无接口数据</td></tr>';
|
||||
ifs.forEach(function (f) {
|
||||
const up = !!f.up;
|
||||
@@ -1125,9 +1280,20 @@
|
||||
'<td class="muted">' + fmtBytes(f.rx_bytes) + ' / ' + fmtBytes(f.tx_bytes) + '</td></tr>';
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
if (d && d.rate && (d.rate.rx_bps != null || d.rate.tx_bps != null))
|
||||
h += '<p class="muted">实时速率:↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s</p>';
|
||||
const rf = (ifd && ifd.rfkill) || [];
|
||||
if (rf.length) {
|
||||
h += '<div class="rfkill-row">';
|
||||
rf.forEach(r => h += '<span class="home-chip ' + (r.soft || r.hard ? 'warn' : 'ok') + '">' + esc(r.type) + (r.soft ? ' 软关' : '') + (r.hard ? ' 硬关' : '') + '</span>');
|
||||
h += '</div>';
|
||||
}
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
/* WiFi / 蓝牙摘要(/api/net/status:E0 概览,含实时速率)。接口细分已迁 netIfaceCards。 */
|
||||
function netStatusCards(d) {
|
||||
let h = '';
|
||||
if (d && d.rate && (d.rate.rx_bps != null || d.rate.tx_bps != null))
|
||||
h += '<div class="card"><p class="muted">实时速率:↓ ' + fmtBytes(d.rate.rx_bps) + '/s · ↑ ' + fmtBytes(d.rate.tx_bps) + '/s</p></div>';
|
||||
// WiFi 摘要
|
||||
const w = (d && d.wifi) || {};
|
||||
h += '<div class="card"><h3 class="card-title">WiFi</h3><table class="table"><tbody>';
|
||||
@@ -1155,9 +1321,50 @@
|
||||
return h;
|
||||
}
|
||||
|
||||
/* 网络策略卡(/api/net/policy;写走 POST /api/net/policy,require_auth + 二次确认)。
|
||||
* enforce 如实显示(dns_only):仅 DNS 层拒绝,不会真断连。 */
|
||||
function netPolicyCard(pol) {
|
||||
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';
|
||||
let h = '<div class="card"><h3 class="card-title">网络策略</h3>';
|
||||
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 || [];
|
||||
if (!rules.length) h += '<div class="empty">暂无策略规则(默认放行)</div>';
|
||||
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 => {
|
||||
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>';
|
||||
});
|
||||
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>';
|
||||
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>';
|
||||
h += '<p class="muted">防自锁实体(dropbear/wpa_supplicant/udhcpc/netmgrd/svcd/sysui/init)拒 block。</p>';
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
|
||||
function netConnsBody(cn) {
|
||||
const list = (cn && cn.conns) || [];
|
||||
let h = '';
|
||||
/* 连接 top 摘要(聚合计数,轻量——overview 与网速页的观察摘要,非完整归因)。 */
|
||||
const entCnt = {};
|
||||
(list || []).forEach(c => { entCnt[c.entity] = (entCnt[c.entity] || 0) + 1; });
|
||||
const top = Object.keys(entCnt).sort((a, b) => entCnt[b] - entCnt[a]).slice(0, 3);
|
||||
if (top.length) {
|
||||
h += '<div class="home-sub" style="margin-bottom:8px">连接 top:' + top.map(e => '<span class="net-entity">' + netBadge(e) + ' ×' + entCnt[e] + '</span>').join(' · ') + '(共 ' + list.length + ' 条)</div>';
|
||||
}
|
||||
if (!list.length) h += '<div class="empty">暂无连接' + (netEntityFilter ? '(该实体)' : '') + '</div>';
|
||||
else {
|
||||
h += '<table class="table"><thead><tr><th>实体</th><th>远端(域名)</th><th>本端</th><th>状态</th><th>寿命</th></tr></thead><tbody>';
|
||||
@@ -1202,8 +1409,9 @@
|
||||
let netLoaded = false; // 是否已成功加载过一次网络状态(用于「首次失败只显示横幅 + 占位」语义)
|
||||
async function fetchNetOnce() {
|
||||
if (state.route !== 'net') return;
|
||||
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body');
|
||||
let st, cn, dn;
|
||||
const stEl = $('#net-status'), cbEl = $('#net-conns-body'), dbEl = $('#net-dns-body'),
|
||||
ifEl = $('#net-ifaces'), poEl = $('#net-policy');
|
||||
let st, cn, dn, ifd, pol;
|
||||
try { st = await api('GET', '/api/net/status'); }
|
||||
catch (e) { netBannerErr(errText(e)); return; }
|
||||
netLoaded = true;
|
||||
@@ -1212,8 +1420,12 @@
|
||||
catch (e) { cn = null; }
|
||||
try { dn = await api('GET', '/api/net/dns'); }
|
||||
catch (e) { dn = null; }
|
||||
try { ifd = await api('GET', '/api/net/ifaces'); } catch (e) { ifd = null; }
|
||||
try { pol = await api('GET', '/api/net/policy'); } catch (e) { pol = null; }
|
||||
if (state.route !== 'net') return;
|
||||
if (ifEl) ifEl.innerHTML = netIfaceCards(ifd);
|
||||
if (stEl) stEl.innerHTML = netStatusCards(st);
|
||||
if (poEl) poEl.innerHTML = netPolicyCard(pol);
|
||||
if (cbEl) cbEl.innerHTML = netConnsBody(cn);
|
||||
if (dbEl) dbEl.innerHTML = netDnsBody(dn);
|
||||
}
|
||||
@@ -1250,7 +1462,9 @@
|
||||
$('#content').innerHTML =
|
||||
'<div class="page" id="net-page">' +
|
||||
'<div id="net-banner" class="page-banner" hidden></div>' +
|
||||
'<div id="net-ifaces"><p class="placeholder">加载网络接口…</p></div>' +
|
||||
'<div id="net-status"><p class="placeholder">加载网络状态…</p></div>' +
|
||||
'<div id="net-policy"><p class="placeholder">加载网络策略…</p></div>' +
|
||||
'<div class="card"><h3 class="card-title">每实体连接表</h3>' +
|
||||
'<div class="field-row" style="margin-bottom:12px">' +
|
||||
'<input type="text" id="net-entity-filter" placeholder="筛选实体(app: / svc: / proc:)" style="max-width:280px" value="' + esc(netEntityFilter) + '">' +
|
||||
@@ -1290,6 +1504,60 @@
|
||||
flush.disabled = false;
|
||||
fetchNetOnce();
|
||||
}
|
||||
/* ---- 网络策略写(require_auth + 二次确认;enforce=dns_only 如实显示,不谎称断连) ---- */
|
||||
const tgl = e.target.closest('[data-ptoggle]');
|
||||
if (tgl) {
|
||||
const ent = tgl.dataset.ptoggle, net = tgl.dataset.net;
|
||||
const ok = await confirmDlg('网络策略', '确认将 ' + ent + ' 网络策略设为「' + (net === 'block' ? '阻断(DNS 层拒绝,不会真断连)' : '放行') + '」?', net === 'block');
|
||||
if (!ok) { fetchNetOnce(); return; }
|
||||
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: net }); toast('已更新策略:' + ent); }
|
||||
catch (err) { toast('策略更新失败:' + errText(err)); }
|
||||
fetchNetOnce();
|
||||
return;
|
||||
}
|
||||
const del = e.target.closest('[data-pdel]');
|
||||
if (del) {
|
||||
const ent = del.dataset.pdel;
|
||||
const ok = await confirmDlg('删除策略', '确认删除 ' + ent + ' 的全部策略规则(恢复默认放行)?', true);
|
||||
if (!ok) { fetchNetOnce(); return; }
|
||||
try { await api('POST', '/api/net/policy', { op: 'del', entity: ent }); toast('已删除策略:' + ent); }
|
||||
catch (err) { toast('删除失败:' + errText(err)); }
|
||||
fetchNetOnce();
|
||||
return;
|
||||
}
|
||||
const blk = e.target.closest('#pol-block');
|
||||
if (blk) {
|
||||
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
|
||||
if (!ent) { toast('请输入实体(app:xxx / svc:xxx / proc:xxx)'); 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); }
|
||||
catch (err) { toast('阻断失败:' + errText(err)); }
|
||||
fetchNetOnce();
|
||||
return;
|
||||
}
|
||||
const alw = e.target.closest('#pol-allow');
|
||||
if (alw) {
|
||||
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
|
||||
if (!ent) { toast('请输入实体(app:xxx / svc:xxx / proc:xxx)'); return; }
|
||||
try { await api('POST', '/api/net/policy', { op: 'set_net', entity: ent, arg: 'allow' }); toast('已放行:' + ent); }
|
||||
catch (err) { toast('放行失败:' + errText(err)); }
|
||||
fetchNetOnce();
|
||||
return;
|
||||
}
|
||||
const dom = e.target.closest('#pol-dom');
|
||||
if (dom) {
|
||||
const ent = ($('#pol-entity') ? $('#pol-entity').value.trim() : '');
|
||||
const hst = ($('#pol-domain') ? $('#pol-domain').value.trim() : '');
|
||||
if (!ent) { toast('请输入实体'); 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; }
|
||||
try { await api('POST', '/api/net/policy', { op: 'set_domain', entity: ent, arg: hst }); toast('域名规则已更新'); }
|
||||
catch (err) { toast('域名规则失败:' + errText(err)); }
|
||||
fetchNetOnce();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1852,6 +2120,66 @@
|
||||
catch (e) { toast('保存失败:' + errText(e)); }
|
||||
}, 350);
|
||||
|
||||
/* ---------------- 6b. 远程控制页滑动手势(设备端靠触摸滑动操作,控制页据此补滑动手势) ----------------
|
||||
* 说明:后端 /api/input/tap 仅支持「单点坐标注入」(屏幕页用),无滑动/手势注入端点;
|
||||
* 故控制页滑动统一映射到 /api/key/:code 语义键(与本地按键行为一致),多步音量 = 逐键 tap 多次。 */
|
||||
const SWIPE = {
|
||||
minDist: 40, // 判定为「滑动」的最小位移(px),小于此视为点按
|
||||
axisRatio: 1.5, // 主轴位移须至少为次轴的倍数,防斜滑误判
|
||||
edgePx: 56, // 距控制区左缘多少 px 内起手即视为「左缘滑」
|
||||
maxVolSteps: 5, // 单次滑动最多递增音量步数(防连刷冲上限)
|
||||
volStepPx: 70, // 每多滑这么多位移,多算一步音量
|
||||
clickSuppressMs: 450 // 滑动后短暂屏蔽按钮点击,防误触
|
||||
};
|
||||
let swipeState = null; // 当前手势 {id,x0,y0,edge}
|
||||
let lastSwipeAt = 0; // 上次触发手势的时刻,用于屏蔽随后可能的 click
|
||||
async function sendKeyLabel(code, label) {
|
||||
try { await api('POST', '/api/key/' + code, { press: 'tap' }); toast(label + ' 已发送'); }
|
||||
catch (e) { toast('发送失败:' + errText(e)); }
|
||||
}
|
||||
async function sendKeyRepeat(code, n, label) {
|
||||
let count = 0, err = null;
|
||||
for (let i = 0; i < n; i++) {
|
||||
try { await api('POST', '/api/key/' + code, { press: 'tap' }); count++; }
|
||||
catch (e) { err = errText(e); break; } // 失败即停,避免连刷
|
||||
}
|
||||
if (count > 0) toast(label + ' ×' + count);
|
||||
if (err) toast('发送失败:' + err);
|
||||
}
|
||||
// 依据滑动主轴位移换算音量步数(>=1,封顶 maxVolSteps)
|
||||
function volSteps(ay) {
|
||||
return Math.max(1, Math.min(SWIPE.maxVolSteps, Math.round(ay / SWIPE.volStepPx)));
|
||||
}
|
||||
function handleRemoteGesture(dir, ax, ay) {
|
||||
if (dir === 'volup') sendKeyRepeat(115, volSteps(ay), '音量+');
|
||||
else if (dir === 'voldown') sendKeyRepeat(114, volSteps(ay), '音量−');
|
||||
else if (dir === 'next') sendKeyLabel(163, '下一曲');
|
||||
else if (dir === 'prev') sendKeyLabel(165, '上一曲');
|
||||
else if (dir === 'back') sendKeyLabel(158, '返回');
|
||||
}
|
||||
function remotePointerDown(e) {
|
||||
if (state.route !== 'remote' || swipeState) return; // 已有一指在手 → 忽略多点触控
|
||||
if (!e.target.closest('#remote-page')) return;
|
||||
const page = $('#remote-page'); if (!page) return;
|
||||
const r = page.getBoundingClientRect();
|
||||
swipeState = { id: e.pointerId, x0: e.clientX, y0: e.clientY, edge: (e.clientX - r.left) <= SWIPE.edgePx };
|
||||
}
|
||||
function remotePointerUp(e) {
|
||||
const s = swipeState;
|
||||
if (!s || e.pointerId !== s.id) return;
|
||||
swipeState = null;
|
||||
const dx = e.clientX - s.x0, dy = e.clientY - s.y0;
|
||||
const ax = Math.abs(dx), ay = Math.abs(dy);
|
||||
if (Math.max(ax, ay) < SWIPE.minDist) return; // 轻点/未动 → 交给按钮 click 处理
|
||||
let dir = null;
|
||||
if (s.edge && dx > 0 && dx >= ay * SWIPE.axisRatio && dx >= SWIPE.minDist) dir = 'back'; // 左缘滑 → 返回
|
||||
else if (ax >= ay * SWIPE.axisRatio) dir = (dx < 0 ? 'next' : 'prev'); // 水平 → 上下曲
|
||||
else if (ay >= ax * SWIPE.axisRatio) dir = (dy < 0 ? 'volup' : 'voldown'); // 垂直 → 音量
|
||||
if (!dir) return; // 斜滑/未达占优 → 忽略
|
||||
lastSwipeAt = Date.now();
|
||||
handleRemoteGesture(dir, ax, ay);
|
||||
}
|
||||
|
||||
/* ---------------- 6. 远程控制页 ---------------- */
|
||||
function renderRemote() {
|
||||
const keys = [
|
||||
@@ -1864,12 +2192,16 @@
|
||||
{ code: 158, label: '返回', ico: '↩' },
|
||||
{ code: 172, label: '主页', ico: '⌂' }
|
||||
];
|
||||
let h = '<div class="page" id="remote-page">';
|
||||
let h = '<div class="page" id="remote-page" style="touch-action:none">';
|
||||
h += '<div class="card"><h3 class="card-title">模拟按键</h3>' +
|
||||
'<p class="muted">行为与本地按键一致(设备端系统统一接管)。长按电源将触发电源菜单。</p>' +
|
||||
'<div class="key-grid">' + keys.map(k =>
|
||||
'<button class="key-btn" data-code="' + k.code + '"><span class="k-ico">' + k.ico + '</span>' + esc(k.label) + '</button>'
|
||||
).join('') + '</div></div>';
|
||||
).join('') + '</div>' +
|
||||
'<p class="muted" style="margin-top:12px;line-height:1.9"><b>滑动手势</b>(与按钮并存,轻点仍是点击):' +
|
||||
'上滑=音量+ · 下滑=音量-(滑得越远步数越多,单次至多 ' + SWIPE.maxVolSteps + ' 步) · ' +
|
||||
'左滑=下一曲 · 右滑=上一曲 · 控制区左缘右滑=返回</p>' +
|
||||
'</div>';
|
||||
h += '<div class="card"><h3 class="card-title">电源长按(电源菜单)</h3>' +
|
||||
'<div class="field-row" style="gap:10px"><button class="btn btn-primary" data-powerlong="1">长按电源 3s(电源菜单)</button>' +
|
||||
'<span class="muted">发送 code=116 · press=long · ms=3000</span></div></div>';
|
||||
@@ -1882,9 +2214,14 @@
|
||||
}
|
||||
function bindRemote() {
|
||||
$('#content').addEventListener('click', function (e) {
|
||||
if (Date.now() - lastSwipeAt < SWIPE.clickSuppressMs) return; // 刚滑完 → 屏蔽本次点击,防误触按钮
|
||||
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; }
|
||||
});
|
||||
// 滑动手势:指针事件统一触摸/鼠标;#remote-page 已设 touch-action:none 防浏览器滚动劫持
|
||||
$('#content').addEventListener('pointerdown', remotePointerDown);
|
||||
window.addEventListener('pointerup', remotePointerUp);
|
||||
window.addEventListener('pointercancel', remotePointerUp);
|
||||
}
|
||||
|
||||
/* ---------------- 7. 系统信息 / OTA 页 ---------------- */
|
||||
@@ -2328,7 +2665,7 @@
|
||||
system: { title: '系统信息 / 升级', render: renderSystem, bind: bindSystem },
|
||||
logs: { title: '系统日志', render: renderLogs, bind: bindLogs },
|
||||
notif: { title: '通知中心', render: renderNotif, bind: bindNotif },
|
||||
power: { title: '电源 / 功耗', render: renderPower, bind: null },
|
||||
power: { title: '电源 / 功耗', render: renderPower, bind: bindPower },
|
||||
proc: { title: '进程 / 服务', render: renderProc, bind: bindProc }
|
||||
};
|
||||
// 一次性绑定(各菜单 click / 侧栏);页面内交互用 #content 事件委托(已各自绑定)
|
||||
|
||||
Reference in new issue
Block a user