feat: M6.43 — reader分段流式读取(Phase1,只txt) + 面板前台管理/返回回桌面按钮
- reader分段(只txt): core/seg.{h,c} 512KB码位对齐切段(尾字完整)+manifest+按需读单段; book.c rd_book_open只布局每页所在段(治CPU100%/返回无效) load_seg只读当前段(非txt原路径); reader.c 进度实时保存(翻页/刮擦/字号/编码dirty+on_tick节流落盘+退出即时写,锚点存段号,崩溃不丢进度); rdtxt.c fread替代fgetc(去开书扫描卡顿); Phase2(双段拼接+4%释放)留待
- 面板: /api/ams/bg加fg/fg_count(后台保持向后兼容)+POST /api/ams/:pkg/killfg(前台救命:优先am kill,launcher拒则pid直信号SIGTERM->SIGKILL绕过门控)+前端/#/bg前台组'终止'绿徽标+确认弹窗(一键清仍只后台不误杀前台); POST /api/input/gesture(返回=左缘右滑/回桌面=底部上滑,注入滑动轨迹复用SDK手势)+前端屏幕控制页加返回/回桌面MD3按钮
0warning;宿主test_reader_core 72-0/seg_smoke 19-0/loaders_smoke/book_smoke/formats_smoke PASS;面板selftest/node check PASS
This commit is contained in:
1 parent
a64ae06eb6
commit
f40abbd494
17 files changed
+1069
-70
No files matched your search
@@ -304,6 +304,7 @@ void ams_history_parse(char *out, sbuf_t *s, const char *pkg);
|
||||
void ams_history_fill(sbuf_t *s, const char *pkg);
|
||||
int api_ams_history(int sock, const char *pkg);
|
||||
int api_ams_ctl(int sock, const char *pkg, int kill);
|
||||
int api_ams_kill_fg(int sock, const char *pkg); /* 前台强制终止(救命):am kill 优先,launcher 拒→直接 pid 信号 */
|
||||
void svcd_fill(sbuf_t *s);
|
||||
int api_svcd(int sock);
|
||||
int api_overview(int sock);
|
||||
@@ -446,7 +447,9 @@ int api_screen_jpeg(int sock, const req_t *r);
|
||||
int64_t now_mono_ms(void);
|
||||
int64_t touch_last_local_ms(const char *dev);
|
||||
int inject_tap(const char *dev, int x, int y);
|
||||
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1);
|
||||
int api_input_tap(int sock, const req_t *r);
|
||||
int api_input_gesture(int sock, const req_t *r);
|
||||
const char *web_root(void);
|
||||
const char *mime_for(const char *path);
|
||||
int serve_static(int sock, const char *path);
|
||||
|
||||
+147
-19
@@ -253,7 +253,32 @@ static void bg_proc_sample(int pid, double *pcpu, long *rss_kb)
|
||||
}
|
||||
}
|
||||
|
||||
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行)。 */
|
||||
/* 发射单条 APP 行 JSON(复用于 bg/fg 列表;带 state + is_fg + cpu_pct/rss_kb)。
|
||||
* toks 为已切分的 `APP ...` 行(n>=9);*first 控制数组元素逗号(调用方初始化为 1)。
|
||||
* cpu_pct/rss_kb 诚实原则:pid 读不到/不作为可用进程 → null(前端显「—」),绝不编造。 */
|
||||
static void ams_entry_emit(sbuf_t *out, char **toks, int n, int *first)
|
||||
{
|
||||
const char *st = toks[n - 6]; /* 自右向左:state=tok[n-6], pid=n-5(与 ams_fill 同布局) */
|
||||
char nm[128];
|
||||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||||
int pid = atoi(toks[n - 5]);
|
||||
double pcpu = -1.0; long rss = -1;
|
||||
if (pid > 0) bg_proc_sample(pid, &pcpu, &rss);
|
||||
if (!*first) sA(out, ",");
|
||||
*first = 0;
|
||||
sA(out, "{\"pkg\":"); sA_str(out, toks[1]);
|
||||
sA(out, ",\"name\":"); sA_str(out, nm);
|
||||
sA(out, ",\"pid\":%d", pid);
|
||||
sA(out, ",\"state\":"); sA_str(out, st);
|
||||
sA(out, ",\"is_fg\":%s", strcmp(st, "fg") == 0 ? "true" : "false");
|
||||
sA(out, ",\"cpu_pct\":");
|
||||
if (pcpu < 0) sA(out, "null"); else sA(out, "%.1f", pcpu);
|
||||
sA(out, ",\"rss_kb\":");
|
||||
if (rss < 0) sA(out, "null"); else sA(out, "%ld", rss);
|
||||
sA(out, "}");
|
||||
}
|
||||
|
||||
/* 解析已 strip 的 `am list` 全文 → bg 列表 JSON(state=bg 的 APP 行;向后兼容专用,selftest 覆盖)。 */
|
||||
void ams_bg_parse(char *list_text, sbuf_t *s)
|
||||
{
|
||||
char *toks[256];
|
||||
@@ -266,25 +291,10 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
||||
if (strncmp(ln, "APP ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 9) {
|
||||
/* 自右向左固定(与 ams_fill 同布局):state=tok[n-6], pid=n-5;pkg=tok[1],name=join(tok[2..n-6)) */
|
||||
const char *st = toks[n - 6];
|
||||
if (strcmp(st, "bg") == 0) {
|
||||
char nm[128];
|
||||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||||
int pid = atoi(toks[n - 5]);
|
||||
double pcpu; long rss;
|
||||
bg_proc_sample(pid, &pcpu, &rss);
|
||||
if (!first) sA(&apps, ",");
|
||||
first = 0; count++;
|
||||
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
|
||||
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
|
||||
sA(&apps, ",\"pid\":%d", pid);
|
||||
sA(&apps, ",\"state\":"); sA_str(&apps, st);
|
||||
sA(&apps, ",\"cpu_pct\":");
|
||||
if (pcpu < 0) sA(&apps, "null"); else sA(&apps, "%.1f", pcpu);
|
||||
sA(&apps, ",\"rss_kb\":");
|
||||
if (rss < 0) sA(&apps, "null"); else sA(&apps, "%ld", rss);
|
||||
sA(&apps, "}");
|
||||
count++;
|
||||
ams_entry_emit(&apps, toks, n, &first);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,6 +305,8 @@ void ams_bg_parse(char *list_text, sbuf_t *s)
|
||||
sbuf_free(&apps);
|
||||
}
|
||||
|
||||
/* am list 全文 → bg+fg 双列表 JSON。`apps`(bg)保持向后兼容(count=bg 数量);
|
||||
* 追加 `fg` + `fg_count`(前台 App,前端救命分组)。state=stop 无进程,不在本页(proc 页可见)。 */
|
||||
void ams_bg_fill(sbuf_t *s)
|
||||
{
|
||||
char out[16384];
|
||||
@@ -303,7 +315,28 @@ void ams_bg_fill(sbuf_t *s)
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
||||
panel_strip_ansi(out);
|
||||
ams_bg_parse(out, s);
|
||||
char *toks[256];
|
||||
sbuf_t bg = {0}, fg = {0};
|
||||
int bg_first = 1, fg_first = 1, bg_count = 0, fg_count = 0;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "APP ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 9) {
|
||||
const char *st = toks[n - 6];
|
||||
if (strcmp(st, "bg") == 0) { bg_count++; ams_entry_emit(&bg, toks, n, &bg_first); }
|
||||
else if (strcmp(st, "fg") == 0) { fg_count++; ams_entry_emit(&fg, toks, n, &fg_first); }
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"count\":%d,\"ts\":%ld,\"apps\":[%s]",
|
||||
bg_count, (long)time(NULL), bg.b ? bg.b : "");
|
||||
sA(s, ",\"fg_count\":%d,\"fg\":[%s]}", fg_count, fg.b ? fg.b : "");
|
||||
sbuf_free(&bg);
|
||||
sbuf_free(&fg);
|
||||
}
|
||||
|
||||
int api_ams_bg(int sock)
|
||||
@@ -314,3 +347,98 @@ int api_ams_bg(int sock)
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 从 am list 解析某 pkg 的当前 pid(未注册/无进程 → 0)。 */
|
||||
static int ams_pkg_pid(const char *pkg)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
|
||||
if (run_cmd(cmd, out, sizeof out) != 0 || !strstr(out, "APP")) return 0;
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "APP ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 9 && strcmp(toks[1], pkg) == 0) {
|
||||
int pid = atoi(toks[n - 5]);
|
||||
if (nl) *nl = '\n';
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* /proc/<pid>/stat 进程是否已消失/僵尸:-1=已消失,1=僵尸,0=存活。 */
|
||||
static int proc_zombie(int pid)
|
||||
{
|
||||
char path[64], line[256];
|
||||
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||||
if (read_small_file(path, line, sizeof line) < 0) return -1;
|
||||
char *rp = strrchr(line, ')');
|
||||
if (!rp) return -1;
|
||||
const char *p = rp + 2;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
return (*p == 'Z') ? 1 : 0;
|
||||
}
|
||||
|
||||
/* 前台强制终止(救命):优先走 `am kill`(launcher 干净 SIGTERM 序列,accepted);若 launcher 拒
|
||||
* (framework / 无前台会话 → "launcher refused"),回退:解析 am list 拿当前 pid → 直接 SIGTERM,
|
||||
* 宽限后仍存活走 SIGKILL(与 launcher 杀序列同语义)。这是用户明示「前台卡死救命」的强通道——
|
||||
* 前端已二次确认 + 标注「前台」。require_auth 在路由层已护。 */
|
||||
int api_ams_kill_fg(int sock, const char *pkg)
|
||||
{
|
||||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
|
||||
/* ① 干净通道:am kill(launcher 执行 SIGTERM 序列)。 */
|
||||
{
|
||||
char out[256];
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd, "%s kill %s 2>/dev/null", ams_bin(), pkg);
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code == 0 && strstr(out, "OK ")) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||
sA_str(&s, pkg);
|
||||
sA(&s, ",\"force\":false,\"message\":"); sA_str(&s, out);
|
||||
sA(&s, "}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
panel_log("ams kill-fg %s via am refused: %s -> direct signal fallback", pkg,
|
||||
out[0] ? out : "(empty)");
|
||||
}
|
||||
|
||||
/* ② 回退:直接按 pid 信号(绕过 launcher 门控)。 */
|
||||
int pid = ams_pkg_pid(pkg);
|
||||
if (pid <= 0) return send_err(sock, 409, "CONFLICT", "该 App 当前无进程可终止");
|
||||
|
||||
kill(pid, SIGTERM);
|
||||
int killed_hard = 0;
|
||||
for (int i = 0; i < 5; i++) { /* SIGTERM 宽限 ~0.6s(与 launcher 杀序列同语义) */
|
||||
struct timespec ts = {0, 120 * 1000 * 1000};
|
||||
nanosleep(&ts, NULL);
|
||||
int z = proc_zombie(pid);
|
||||
if (z < 0 || z == 1) break; /* 消失/僵尸 → 已终止 */
|
||||
}
|
||||
if (proc_zombie(pid) == 0) { kill(pid, SIGKILL); killed_hard = 1; }
|
||||
|
||||
char msg[192];
|
||||
snprintf(msg, sizeof msg, "force terminated pid %d via direct signal%s", pid,
|
||||
killed_hard ? " (SIGKILL 强杀)" : " (SIGTERM)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"op\":\"kill-fg\",\"pkg\":");
|
||||
sA_str(&s, pkg);
|
||||
sA(&s, ",\"force\":%s,\"pid\":%d,\"message\":", killed_hard ? "true" : "false", pid);
|
||||
sA_str(&s, msg);
|
||||
sA(&s, "}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap。 */
|
||||
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap
|
||||
* 与系统手势注入 /api/input/gesture(back=左缘右滑 / home=底部上滑回桌面)。 */
|
||||
#include "mp.h"
|
||||
|
||||
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
||||
@@ -6,6 +7,17 @@ static int g_screen_pw = 0, g_screen_ph = 0;
|
||||
static int64_t g_last_self_inject_ms = 0;
|
||||
#define TOUCH_ACTIVE_MS 5000
|
||||
|
||||
/* 手势注入阈值(镜像 meow_ui_input.c / np_gesture.h 系统已识别手势,勿新造语义):
|
||||
* 返回=左缘(起)右移 ≥SWIPE_THRESH 且横向主导;回桌面=底部热区(起)上移 ≥HOME_SWIPE
|
||||
* 且竖向主导 + down_x≥EDGE_W(避左缘返回列)。此处位移留容差(≥65)。 */
|
||||
#define GESTURE_EDGE_W 24 /* 左缘宽度:返回手势起点 x 须 < 此值(meow_ui_input.c EDGE_W) */
|
||||
#define GESTURE_SWIPE_DX 78 /* 返回手势右移位移(≥ SDK SWIPE_THRESH 65,留容差) */
|
||||
#define GESTURE_HOME_EDGE_H 28 /* 底部热区高:回桌面起点 y 须 >= fb_h-此值(HOME_EDGE_H) */
|
||||
#define GESTURE_HOME_DY 80 /* 回桌面手势上移位移(≥ SDK HOME_SWIPE 65,留容差) */
|
||||
#define GESTURE_STEPS 10 /* 线性轨迹插值点数(营造真实滑动轨迹) */
|
||||
#define GESTURE_STEP_MS 12 /* 每步间隔毫秒 */
|
||||
#define GESTURE_HOLD_MS 60 /* 末端按住毫秒再抬起(防 fling 误判) */
|
||||
|
||||
|
||||
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||||
{
|
||||
@@ -230,6 +242,47 @@ int inject_tap(const char *dev, int x, int y)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 线性滑动注入:按下(原点) → 插值移动 → 末端按住 → 抬起。事件序同 inject_tap
|
||||
* (BTN_TOUCH 边沿先于 ABS,goodix 时序免疫;首帧锁原点),设备手势机据此判
|
||||
* 「左缘右滑=返回」或「底部上滑=回桌面」(复用系统已识别信号,不新造)。 */
|
||||
int inject_swipe(const char *dev, int x0, int y0, int x1, int y1)
|
||||
{
|
||||
int fd = open(dev, O_WRONLY);
|
||||
if (fd < 0) return -1;
|
||||
struct input_event ev;
|
||||
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||||
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||||
/* 按下边沿 + 首帧 ABS 锁原点(与 tap 同序) */
|
||||
__WE(EV_KEY, BTN_TOUCH, 1);
|
||||
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_X, x0);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_Y, y0);
|
||||
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||||
__WE(EV_ABS, ABS_X, x0);
|
||||
__WE(EV_ABS, ABS_Y, y0);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
/* 插值逐点移动:每步坐标更新 + 同步,营造连续轨迹 */
|
||||
for (int i = 1; i <= GESTURE_STEPS; i++) {
|
||||
int xi = x0 + (x1 - x0) * i / GESTURE_STEPS;
|
||||
int yi = y0 + (y1 - y0) * i / GESTURE_STEPS;
|
||||
__WE(EV_ABS, ABS_MT_POSITION_X, xi);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_Y, yi);
|
||||
__WE(EV_ABS, ABS_X, xi);
|
||||
__WE(EV_ABS, ABS_Y, yi);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
usleep((useconds_t)GESTURE_STEP_MS * 1000);
|
||||
}
|
||||
/* 末端短暂按住再抬起(与 tap 同:抬起到按下边沿被手势机视为一次抬手) */
|
||||
usleep((useconds_t)GESTURE_HOLD_MS * 1000);
|
||||
__WE(EV_KEY, BTN_TOUCH, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
#undef __WE
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_input_tap(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
@@ -275,3 +328,71 @@ int api_input_tap(int sock, const req_t *r)
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* /api/input/gesture:注入设备已识别的系统手势(back=左缘右滑 / home=底部上滑回桌面)。
|
||||
* 复用现有触摸注入通道(/dev/input/event1 滑动轨迹),不触碰被物理键红线禁注入的
|
||||
* /api/key 158/172;由设备自身手势机判成 MEOW_SWIPE_LEFT / MEOW_EV_HOME。 */
|
||||
int api_input_gesture(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jg = jv_obj_get(root, "gesture");
|
||||
if (!jg || jg->type != J_STR) {
|
||||
jv_free(root);
|
||||
return send_err(sock, 400, "BAD_REQUEST", "缺 gesture(back|home)");
|
||||
}
|
||||
const char *gesture = jg->str;
|
||||
int is_back = (strcmp(gesture, "back") == 0);
|
||||
int is_home = (strcmp(gesture, "home") == 0);
|
||||
jv_free(root);
|
||||
if (!is_back && !is_home)
|
||||
return send_err(sock, 400, "BAD_REQUEST", "gesture 非法(仅 back|home)");
|
||||
|
||||
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||||
const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||||
int sw = 480, sh = 720;
|
||||
{ int fx = 0, fy = 0; if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||||
if (sw < 1) sw = 480;
|
||||
if (sh < 1) sh = 720;
|
||||
|
||||
/* 几何(镜像 SDK 判据 + 留容差,见上 GESTURE_* 宏) */
|
||||
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
|
||||
if (is_back) {
|
||||
x0 = 4; /* 左缘内(< EDGE_W=24) */
|
||||
if (x0 >= sw) x0 = sw - 1;
|
||||
if (x0 >= GESTURE_EDGE_W) x0 = GESTURE_EDGE_W - 1;
|
||||
y0 = y1 = sh / 2; /* 屏中部,避开顶部下拉/底部上滑热区 */
|
||||
x1 = x0 + GESTURE_SWIPE_DX;
|
||||
if (x1 >= sw) x1 = sw - 1; /* 横向主导 + 右移≥阈值 */
|
||||
} else { /* home */
|
||||
x0 = x1 = sw / 2; /* 屏底中部,避开左缘返回列 */
|
||||
if (x0 < GESTURE_EDGE_W) x0 = x1 = GESTURE_EDGE_W;
|
||||
y0 = sh - 2; /* 底部热区(>= fb_h-HOME_EDGE_H) */
|
||||
if (y0 >= sh) y0 = sh - 1;
|
||||
y1 = y0 - GESTURE_HOME_DY;
|
||||
if (y1 < 0) y1 = 0; /* 竖向主导 + 上移≥阈值 */
|
||||
}
|
||||
|
||||
/* 仲裁:本机最近触摸后 5s 内忽略注入(与 tap 同) */
|
||||
int64_t now = now_mono_ms();
|
||||
int64_t last = touch_last_local_ms(dev);
|
||||
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||||
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
g_last_self_inject_ms = now_mono_ms();
|
||||
if (inject_swipe(dev, x0, y0, x1, y1) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "手势注入失败(无法打开触摸设备)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":true,\"gesture\":\"%s\",\"from\":[%d,%d],\"to\":[%d,%d]}",
|
||||
gesture, x0, y0, x1, y1);
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
@@ -339,16 +339,22 @@ int handle(int sock, req_t *r)
|
||||
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);
|
||||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||||
if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(sock, 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 前台前端二次确认) */
|
||||
/* POST /api/ams/<pkg>/start|kill|killfg(管启/停;require_auth 已护,kill 前台前端二次确认;
|
||||
* killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */
|
||||
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 > 7 && !strcmp(p + pl - 7, "/killfg")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
return api_ams_kill_fg(sock, pkg);
|
||||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2.2" fill="var(--bg)"/><circle cx="15" cy="12" r="2.2" fill="var(--bg)"/><circle cx="7" cy="18" r="2.2" fill="var(--bg)"/></svg></span>
|
||||
<span class="nav-label">进程·服务</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="bg" title="后台活动(App 后台常驻:CPU/内存占用 + 一键清后台)">
|
||||
<button class="nav-item" data-route="bg" title="App 活动(前台可终止救命 · 后台常驻:CPU/内存占用 + 一键清后台)">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M12 3 3 8l9 5 9-5-9-5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 12l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M3 16l9 5 9-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
|
||||
<span class="nav-label">后台</span>
|
||||
</button>
|
||||
|
||||
@@ -283,6 +283,9 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
||||
.bg-row .bg-name{ font-weight:600; }
|
||||
.bg-row .bg-sub{ font-size:12px; color:var(--on-surface-variant); margin-top:2px; display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
|
||||
.bg-row .btn{ flex:0 0 auto; }
|
||||
.bg-group{ margin:14px 0 8px; font-weight:600; font-size:13px; color:var(--on-surface-variant); }
|
||||
.bg-group:first-of-type{ margin-top:0; }
|
||||
.bg-group + .bg-list{ margin-bottom:4px; }
|
||||
|
||||
/* 实时屏幕(屏幕页) */
|
||||
.screen-frame{ display:flex; justify-content:center; max-width:480px; margin:0 auto; }
|
||||
|
||||
@@ -21,7 +21,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
notif: { title: '通知中心', render: renderNotif, bind: bindNotif },
|
||||
power: { title: '电源 / 功耗', render: renderPower, bind: bindPower },
|
||||
proc: { title: '进程 / 服务', render: renderProc, bind: bindProc },
|
||||
bg: { title: '后台管理', render: renderBg, bind: bindBg }
|
||||
bg: { title: 'App 活动(前台可终止 · 后台管理)', render: renderBg, bind: bindBg }
|
||||
};
|
||||
let started = false;
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
power: '<svg viewBox="0 0 24 24"><path d="M12 3v8" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M6.6 6.6a7.5 7.5 0 1 0 10.8 0" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/></svg>',
|
||||
play: '<svg viewBox="0 0 24 24"><path d="M7 5.5v13l11-6.5z" fill="currentColor"/></svg>',
|
||||
next: '<svg viewBox="0 0 24 24"><path d="M5 5.5l9 6.5-9 6.5z" fill="currentColor"/><rect x="16.2" y="5" width="2.4" height="14" fill="currentColor"/></svg>',
|
||||
prev: '<svg viewBox="0 0 24 24"><path d="M19 5.5l-9 6.5 9 6.5z" fill="currentColor"/><rect x="5.4" y="5" width="2.4" height="14" fill="currentColor"/></svg>'
|
||||
prev: '<svg viewBox="0 0 24 24"><path d="M19 5.5l-9 6.5 9 6.5z" fill="currentColor"/><rect x="5.4" y="5" width="2.4" height="14" fill="currentColor"/></svg>',
|
||||
back: '<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" fill="currentColor"/></svg>',
|
||||
home: '<svg viewBox="0 0 24 24"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z" fill="currentColor"/></svg>'
|
||||
};
|
||||
let ctlBright = { min: 1, max: 100, value: 100 }; // 控制板亮度(读取自 /api/settings/display.brightness)
|
||||
let logsGroup = 'all', logsPrefix = '';
|
||||
@@ -889,9 +891,10 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- 批④ 后台活动页(#/bg):列表 + 行内「杀死」 + 顶栏「一键清后台」(复用 /api/ams/bg 只读 + /api/ams/:pkg/kill) ---- */
|
||||
/* ---- 批④ App 活动页(#/bg):前台「终止」救命 + 后台列表/行内「杀死」 + 顶栏「一键清后台」
|
||||
*(复用 /api/ams/bg 只读:apps=bg 向后兼容 + fg=前台;前台终止走 /api/ams/:pkg/killfg,后台杀死走 /api/ams/:pkg/kill) ---- */
|
||||
let bgTimer = null;
|
||||
let bgData = null; // {available,count,apps:[{pkg,name,pid,state,cpu_pct,rss_kb}]}
|
||||
let bgData = null; // {available,count,apps:[{pkg,name,pid,state,is_fg,cpu_pct,rss_kb}],fg_count,fg:[...]}
|
||||
|
||||
function stopBg() { if (bgTimer) { clearInterval(bgTimer); bgTimer = null; } }
|
||||
|
||||
@@ -900,30 +903,43 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
return '<span class="bg-ico">' + ch + '</span>';
|
||||
}
|
||||
|
||||
/* 状态徽标:前台=ok/绿,后台=warn/黄(复用已定义的 home-chip 组件样式)。 */
|
||||
function bgStateBadge(a) {
|
||||
const isFg = a.is_fg || a.state === 'fg';
|
||||
return '<span class="home-chip ' + (isFg ? 'ok' : 'warn') + '">' + (isFg ? '前台' : '后台') + '</span>';
|
||||
}
|
||||
|
||||
/* 单行 App 卡(kind='fg'=前台可终止/救命;'bg'=后台可杀死)。 */
|
||||
function bgRowHtml(a, kind) {
|
||||
const cpu = (a.cpu_pct == null) ? '—' : a.cpu_pct.toFixed(1) + '%';
|
||||
const mem = (a.rss_kb == null) ? '—' : fmtBytes(Number(a.rss_kb) * 1024);
|
||||
const sub = 'CPU ' + cpu + ' · ' + mem +
|
||||
(a.pid > 0 ? ' · pid ' + esc(a.pid) : '') + ' · ' + bgStateBadge(a);
|
||||
const btn = (kind === 'fg')
|
||||
? '<button class="btn btn-danger" data-bgkillfg="' + esc(a.pkg) + '">终止</button>'
|
||||
: '<button class="btn btn-danger" data-bgkill="' + esc(a.pkg) + '">杀死</button>';
|
||||
return '<div class="bg-row">' + bgIconHtml(a) +
|
||||
'<div class="bg-info"><div class="bg-name">' + esc(a.name || a.pkg || '未命名') + '</div>' +
|
||||
'<div class="bg-sub">' + sub + '<span class="mono">' + esc(a.pkg) + '</span></div></div>' +
|
||||
btn + '</div>';
|
||||
}
|
||||
|
||||
function renderBgBody(d) {
|
||||
let h = '<div class="page" id="bg-page">';
|
||||
h += '<div class="card"><h3 class="card-title">后台活动 App(meow_amsd bg · 只读)</h3>';
|
||||
h += '<div class="card"><h3 class="card-title">App 活动(前台可终止 · 后台管理)</h3>';
|
||||
if (!d || d.available === false) {
|
||||
h += homeUnavailable();
|
||||
} else {
|
||||
const fg = d.fg || [];
|
||||
const apps = d.apps || [];
|
||||
if (!apps.length) {
|
||||
h += '<div class="empty">无后台 App</div>';
|
||||
} else {
|
||||
h += '<div class="bg-list">';
|
||||
apps.forEach(a => {
|
||||
const cpu = (a.cpu_pct == null) ? '—' : a.cpu_pct.toFixed(1) + '%';
|
||||
const mem = (a.rss_kb == null) ? '—' : fmtBytes(Number(a.rss_kb) * 1024);
|
||||
const sub = 'CPU ' + cpu + ' · ' + mem + ' · 后台' +
|
||||
(a.pid > 0 ? ' · pid ' + esc(a.pid) : '');
|
||||
h += '<div class="bg-row">' + bgIconHtml(a) +
|
||||
'<div class="bg-info"><div class="bg-name">' + esc(a.name || a.pkg || '未命名') + '</div>' +
|
||||
'<div class="bg-sub">' + sub + '<span class="mono">' + esc(a.pkg) + '</span></div></div>' +
|
||||
'<button class="btn btn-danger" data-bgkill="' + esc(a.pkg) + '">杀死</button></div>';
|
||||
});
|
||||
h += '</div>';
|
||||
}
|
||||
h += '<p class="muted">占用数据读自 /proc/<pid>(与启动器后台管理页同源、同口径);「杀死」经 amsd(require_auth)。CPU 首采样为「—」,轮询后显示真实占用;数据不可得时如实显示「—」,绝不编造。</p>';
|
||||
h += '<p class="muted">前台 App 可「终止」(救命:前台卡死/无响应时强制结束,需二次确认);后台 App 可「杀死」或一键清。</p>';
|
||||
h += '<div class="bg-group">' + (fg.length ? '前台 App(' + fg.length + ')' : '前台 App') + '</div>';
|
||||
if (!fg.length) h += '<div class="empty">当前无前台 App</div>';
|
||||
else { h += '<div class="bg-list">'; fg.forEach(a => { h += bgRowHtml(a, 'fg'); }); h += '</div>'; }
|
||||
h += '<div class="bg-group">' + (apps.length ? '后台 App(' + apps.length + ')' : '后台 App') + '</div>';
|
||||
if (!apps.length) h += '<div class="empty">无后台 App</div>';
|
||||
else { h += '<div class="bg-list">'; apps.forEach(a => { h += bgRowHtml(a, 'bg'); }); h += '</div>'; }
|
||||
h += '<p class="muted">占用数据读自 /proc/<pid>(与启动器后台管理页同源、同口径);「终止/杀死」经 amsd(require_auth),前台终止为救命操作需二次确认。CPU 首采样为「—」,轮询后显示真实占用;数据不可得时如实显示「—」,绝不编造。</p>';
|
||||
}
|
||||
h += '</div></div>';
|
||||
return h;
|
||||
@@ -965,6 +981,15 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
fetchBgOnce();
|
||||
}
|
||||
|
||||
/* 前台强制终止(救命):明确标注「前台」,二次确认;后端 am kill 优先、launcher 拒则直接按进程信号。 */
|
||||
async function bgKillFg(pkg) {
|
||||
const ok = await confirmDlg('终止前台 App', '『' + pkg + '』当前位于前台。若它已卡死/无响应,可强制终止(救命):先请求干净结束,启动器拒绝时直接按进程信号(SIGTERM/SIGKILL)。确认终止?', true);
|
||||
if (!ok) return;
|
||||
try { await api('POST', '/api/ams/' + encodeURIComponent(pkg) + '/killfg'); toast('已请求终止前台 ' + pkg); }
|
||||
catch (e) { toast('终止失败:' + errText(e)); }
|
||||
fetchBgOnce();
|
||||
}
|
||||
|
||||
async function bgClearAll() {
|
||||
const list = (bgData && bgData.apps) ? bgData.apps : [];
|
||||
if (!list.length) { toast('无后台 App 可清'); return; }
|
||||
@@ -981,6 +1006,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
|
||||
function bindBg() {
|
||||
$('#content').addEventListener('click', async function (e) {
|
||||
const kfg = e.target.closest('[data-bgkillfg]');
|
||||
if (kfg) { await bgKillFg(kfg.dataset.bgkillfg); return; }
|
||||
const kl = e.target.closest('[data-bgkill]');
|
||||
if (kl) { await bgKill(kl.dataset.bgkill); return; }
|
||||
});
|
||||
@@ -1967,7 +1994,17 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
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, '返回');
|
||||
else if (dir === 'back') sendGesture('back'); // 左缘右滑 → 注入设备已识别的返回手势
|
||||
}
|
||||
|
||||
async function sendGesture(name) {
|
||||
const label = name === 'back' ? '返回' : '回桌面';
|
||||
try {
|
||||
const d = await api('POST', '/api/input/gesture', { gesture: name });
|
||||
if (d && d.dispatched) toast(label + ' 已触发');
|
||||
else if (d && d.reason === 'local_touch_active') toast('本机触摸中,注入已暂停');
|
||||
else toast(label + ' 触发未派发');
|
||||
} catch (e) { toast(label + ' 触发失败:' + errText(e)); }
|
||||
}
|
||||
|
||||
function remotePointerDown(e) {
|
||||
@@ -2010,6 +2047,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
h += remoteBtn('next', 'next', '下一曲', '下一曲(code=163)');
|
||||
h += remoteBtn('prev', 'prev', '上一曲', '上一曲(code=165)');
|
||||
h += remoteBtn('power', 'power', '电源', '电源键(单击;code=116)');
|
||||
h += remoteBtn('back', 'back', '返回', '左缘右滑返回(注入设备已识别的返回手势)');
|
||||
h += remoteBtn('home', 'home', '回桌面', '底部上滑回桌面(注入设备已识别的回桌面手势)');
|
||||
h += '</div>';
|
||||
h += '<div class="remote-bright">' +
|
||||
'<span class="r-lbl">指定亮度</span>' +
|
||||
@@ -2018,7 +2057,7 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
'</div>';
|
||||
h += '<button class="btn" id="ctl-powerlong">' + (MP_ICONS.power || '') + ' <span>长按电源(电源菜单)</span></button>';
|
||||
h += '<p class="muted" style="margin-top:8px;line-height:1.8">滑动手势(在遥控板上):上滑=音量+ · 下滑=音量- · 左滑=下一曲 · 右滑=上一曲 · 左缘右滑=返回。<br>' +
|
||||
'<b>返回/主页</b>属物理键红线(<code>/api/key</code> 后端禁注入 158/172),故未提供注入按钮。</p>';
|
||||
'<b>返回/回桌面</b>走手势注入(<code>/api/input/gesture</code>,模拟设备已识别的左缘右滑/底部上滑),<b>不走</b>被物理键红线禁注入的 <code>/api/key</code> 158/172。</p>';
|
||||
h += '</div>';
|
||||
return h;
|
||||
}
|
||||
@@ -2484,6 +2523,8 @@ const { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode
|
||||
else if (a === 'next') sendKey(163, 'tap');
|
||||
else if (a === 'prev') sendKey(165, 'tap');
|
||||
else if (a === 'power') sendKey(116, 'tap');
|
||||
else if (a === 'back') sendGesture('back');
|
||||
else if (a === 'home') sendGesture('home');
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('#ctl-powerlong')) { sendKey(116, 'long', 3000); return; }
|
||||
|
||||
Reference in new issue
Block a user