Merge branch 'feat/m39-click-input-gate-removal'

This commit is contained in:
AskaEth committed 2026-11-10 22:27:23 +08:00
commit b1acc9096b
13 files changed
+261 -56

No files matched your search

+3 -2
View File
@@ -302,12 +302,13 @@ meow_launcherd --sim-exit N # 菜单打开 N 毫秒后模拟退出
**修复(`run_menu` + `menu_after_app_exit()`)**:
1. **drain**:应用退出后立即读完并丢弃 `touch_fd` 的全部残留事件。
2. **冷却**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + 2500ms`,冷却窗内忽略一切触摸按下边沿(与 watchdog 的同值 COOLDOWN_MS 对齐,覆盖实测幽灵窗 1.2-2.25s);启动失败(返回 -1)不置冷却,避免无谓锁菜单。
2. **短守卫**:应用**真正跑过**(`launch_app` 返回 0)时置 `menu_touch_cooldown_until = now + MENU_TOUCH_GUARD_MS`(**150ms**,M39 定值;M2.6 曾 2500ms、M2.22 缩到 1200ms),守卫窗内忽略触摸按下边沿;启动失败(返回 -1)不置守卫,避免无谓锁菜单。
**真正的防重放机制是第 1 条的 drain**:evdev 每个 fd 各收一份事件副本,App 运行期(launcher 不轮询 `touch_fd`)的全部事件都堆在本进程的 fd 里,drain 一次读空后已无历史事件可重放;**150ms 只兜面板重配后驱动补发的幽灵尾巴**。M39 起 drain 同时复位 `t_down/原点/坐标`——只清队列不清 pressed 边沿,会把「按下已被 drain、抬手稍后到」误判成一次 tap。旧注释写的「覆盖实测幽灵窗 1.2-2.25s」与实现自相矛盾(1200/2500ms 与 1.2–2.25s 的关系在 M2.22 注释里自己都写「实测略短」),故 M39 按「drain 是机制、时长只是尾巴守卫」定稿。
3. **菜单重绘**:v6.8 曾把 `last_redraw` 从绝对时间戳 `t` 改为相对量 `el`(使 500ms 安全重绘生效)以覆盖应用退出后的残帧(如 BYE)。**v6.9 起摒弃周期整屏重绘**(它在 500ms 定时把整个菜单重写进正在显示的 buffer0,造成列表 ~3fps 频闪):改为**仅 dirty 时整屏重绘**,应用退出/进入路径显式置 `dirty=1` 主动覆盖应用帧;时钟每秒用轻量 `draw_clock()` 只刷数字。
**Bug 3 根因与修复**:旧代码在所有 `launch_app` 调用点**无条件**置「CANNOT START: <name>」toast,即使应用**正常跑完**。修复:仅当 `launch_app` 返回 **-1(真启动失败)** 才置 toast;应用干净退出(返回 0)不显示。`launch_app` 用 **CLOEXEC 管道**检测 execve 失败(子进程写 errno → 父进程读 >0 判失败并返回 -1),不再把子进程 `_exit(127)` 误当「应用跑完」。
**副作用(已知取舍)**:应用退出后 2.5s 内菜单触摸不响应(返回键误触/退出后紧急再操作需等待;与 watchdog 相同值,实测幽灵窗 1.2-2.25s)。物理触摸的幽灵无法注入复现(§18.2 注入不投递),以上防抖逻辑在注入/sim 链路已验证,物理最终确认需用户。
**副作用(已知取舍)**:应用退出后 **150ms** 内菜单触摸不响应(M39 定值;M2.6 时是 2.5s,用户可见症状是「点一下没反应、要点两次」,M2.22 缩到 1.2s 仍会吞)。真正的不可控敞口是「面板在 drain 之后、队列之外新产生的硬件幽灵触摸」——这类幽灵 1.2s 与 150ms 都盖不住,只能靠真机日志收口:App 退出后观察 3–5s 内是否**自己重进 App / 闪回播放器**(Bug 2/4 原症状),并 `grep -c "menu touch dropped by cooldown"` 应为 0。物理触摸的幽灵无法注入复现(§18.2 注入不投递),宿主侧已由 `test_meow_ui_home_gesture` T4 覆盖「启动后立即点击生效 / 系统手势守卫内不触发、守卫后正好一次」。
---
Binary file not shown.
Binary file not shown.
+40 -27
View File
@@ -56,17 +56,28 @@ static int menu_live_open_pending = 0;
* fresh menu taps. A replayed tap on an app row relaunches the app (symptom:
* BYE then the app's main screen — Bug 2); a replayed tap on the "BACK TO
* PLAYER" row closes the menu and SIGCONTs the player (symptom: whole screen
* flashes back to the player — Bug 4). We drain the queue immediately after
* the app exits and ignore touches for a short cooldown that covers the
* measured 1.2-2.25s ghost window (matches the watchdog's COOLDOWN_MS). */
* flashes back to the player — Bug 4).
*
* M39:真正的机制是「排空本进程 touch_fd」——evdev 每个 fd 各收一份事件副本,App 运行期间
* (launcher 不读)的全部事件都堆在这个 fd 里,app 一退出就会作为「新菜单 tap」重放。
* menu_drain_touch() 把它们全读掉(含按下沿)+ 复位手势状态机,历史上用于兜底的
* 1.2–2.25s 冷却窗口因此可以收窄到 MENU_TOUCH_GUARD_MS:那个窗口是从「App 退出」开始计时
* 的,用户看到菜单后伸手的第一下必然落在里面被吞(用户实测「首页点图标要点两次」根因之一)。
* 现在只留极短的驱动尾巴守卫,不再按时间吞真实输入。 */
static unsigned long long menu_touch_cooldown_until = 0;
/* 排空本进程触摸队列(非阻塞读到 EAGAIN)+ 复位手势状态机。
* 队列/down 边沿都要清:只清队列而保留 t_down,会让「按下已被 drain 掉、抬手稍后才到」的
* 这次手势被当成一次有效 tap(ghost touch 重放的另一半)。 */
static void menu_drain_touch(void)
{
if (touch_fd >= 0) {
unsigned char ev[16];
while (read(touch_fd, ev, sizeof ev) == (ssize_t)sizeof ev) { /* discard */ }
}
t_down = 0; t_down_valid = 0;
t_down_x = t_down_y = -1;
t_last_x = t_last_y = -1;
}
static void menu_arm_touch(long ms)
@@ -76,18 +87,19 @@ static void menu_arm_touch(long ms)
}
/* Drain the touch queue + arm the post-app cooldown (call right after the
* app exits, before the menu resumes polling touch). The drain always runs so
* touches queued by the launch-attempt tail are cleared; the cooldown only
* arms when an app actually ran (app_ran) so a failed launch doesn't lock the
* menu out for 2.5s. */
/* Drain the touch queue + arm the post-app guard (call right after the app
* exits, before the menu resumes polling touch). The drain always runs so
* touches queued by the launch-attempt tail are cleared; the guard only arms
* when an app actually ran (app_ran) so a failed launch doesn't lock the menu
* out. */
static void menu_after_app_exit(int app_ran)
{
menu_drain_touch();
/* M2.22(D):2500ms 冷却把正常的「app 退出后立即点下一个应用」也吞掉(点几下才进)。
* 用户要求点一下就能进 → 调窄到 1200ms,仍覆盖 goodix ghost 窗口(1.2–2.25s 实测略短,
* 与 watchdog COOLDOWN_MS 同阶),防启动重放。只调窄,保留 cooldown 本身。 */
if (app_ran) menu_touch_cooldown_until = now_ms() + 1200;
/* M39:M2.6 的 2500ms → M2.22 的 1200ms → 现在的 MENU_TOUCH_GUARD_MS(150ms)。
* 依据:重放的来源是本进程 fd 里 App 运行期的事件,menu_drain_touch() 已一次性清空
* (含 t_down/原点,见上),窗口只兜「面板重配后驱动补发的幽灵尾巴」。1200ms 会稳定吞掉
* 用户「返回桌面后立即点下一个图标」——正是要修的症状。 */
if (app_ran) menu_touch_cooldown_until = now_ms() + MENU_TOUCH_GUARD_MS;
}
@@ -1207,8 +1219,9 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
/* touch: event reads (event1 is not grabbed) */
touch_fd = open(DEV_TOUCH, O_RDONLY | O_NONBLOCK);
if (touch_fd < 0) logmsg("touch open failed: %s", strerror(errno));
/* M2.6 幽灵触摸防护:菜单激活(启动)即 drain + 冷却 */
menu_arm_touch(TAP_ARM_DELAY_MS);
/* M39:菜单激活(启动)即 drain + 极短守卫(M2.6 原为 300ms;界面此时已整帧画完,
* 窗口只会吞用户看到首页后的第一下,故收窄到 MENU_TOUCH_GUARD_MS) */
menu_arm_touch(MENU_TOUCH_GUARD_MS);
/* key polling */
struct keypoll kp_power, kp_keys;
@@ -1335,7 +1348,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
sim_exit_tap_done = 1;
if (meow_mode) {
int rb = confirm_reboot_player();
menu_arm_touch(TAP_ARM_DELAY_MS);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
dirty = 1;
logmsg("meow mode: BACK TO PLAYER -> confirm reboot (rb=%d)", rb);
} else {
@@ -1482,7 +1495,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
pwr_hold_at = 0; pwr_menu_state = 0; pwr_woke = 0;
pwr_suppress_click = 1; /* M2.34:菜单返回后抑制本次电源松开被误判单击熄屏 */
dirty = 1; /* 取消后重绘应用列表 */
menu_arm_touch(800); /* M2.6 幽灵触摸防护:菜单关闭后忽略触摸 */
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原 M2.6 800ms) */
/* M2.25 黑屏修复:阻塞式 show_power_menu() 期间 kd_* 防抖状态冻结。
* 用户常「松开电源 → 用触摸点取消」,返回后 kd_pwr.stable 仍是旧的「按住=1」,
* 而 m_prev_pwr 已按 raw 重播为 0 → 二者错配误判「新按下」刷新 pwr_hold_at,
@@ -1666,7 +1679,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
logmsg("menu: top-edge live drag-down -> notification center (dy=%d)",
t_last_y - loy);
t_down = 0; t_down_valid = 0; /* 消费本次手势:返回后不再判 tap/滑动/重开 */
menu_arm_touch(TAP_ARM_DELAY_MS);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
@@ -1682,7 +1695,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
dirty = 1;
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
if (click.status == NOTIF_CLICK_STATUS_OK &&
@@ -1764,7 +1777,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* 本函数只在顶部热区(down_y<SB_BAR_H)+向下主导时返回 1,与左缘互斥。 */
if (mnp_top_swipe_down(tx, ty, dx, dy, SB_BAR_H, MENU_SWIPE_THRESH)) {
logmsg("menu: top-edge swipe-down -> notification center (release edge, fallback)");
menu_arm_touch(TAP_ARM_DELAY_MS);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
notif_click_t click;
memset(&click, 0, sizeof click);
click.app_idx = -1;
@@ -1780,7 +1793,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
dirty = 1;
/* M5:通知点击按 route_status 产出 click(0 跳转 / 1 NO ROUTE / 2 SOURCE NOT FOUND) */
if (click.status == NOTIF_CLICK_STATUS_OK &&
@@ -1818,7 +1831,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
if (dx <= -MENU_SWIPE_THRESH && adx > ady &&
tx >= MEOW_EDGE_W && ty >= SB_BAR_H) {
logmsg("menu: left-swipe -> Now Playing (dx=%d,dy=%d)", dx, dy);
menu_arm_touch(TAP_ARM_DELAY_MS);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
run_now_playing(apps, napps);
/* 重播按键防抖基线,防播放页内电源/音量边沿被 run_menu 误判 */
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
@@ -1830,7 +1843,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
dirty = 1;
continue;
}
@@ -1840,7 +1853,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
/* 批③:顶栏居右「后台管理」按钮(最高优先级;阻挡非阻塞,返回后重画) */
if (menu_hit_bgm_btn(tx, ty)) {
logmsg("menu: touch topbar bg-manager button");
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
run_bg_page(apps, napps);
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
@@ -1851,7 +1864,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms();
menu_arm_touch(800);
menu_arm_touch(MENU_TOUCH_GUARD_MS);
dirty = 1;
continue;
}
@@ -1863,7 +1876,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* 绝不恢复到播放器死帧、绝不对死进程 SIGCONT。 */
logmsg("meow mode: BACK TO PLAYER -> confirm reboot");
int rb = confirm_reboot_player();
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护 */
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
dirty = 1;
if (rb) { logmsg("reboot-to-player: yes, rebooting"); }
/* rb==1 时 reboot 已发起,通常不回;rb==0 留在菜单 */
@@ -1878,7 +1891,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
* 返回」的抬起边沿在 run_menu 被误判为面板单击而熄屏。 */
else if (ty >= EXIT_ROW - ROW_H - 4 && ty < EXIT_ROW - ROW_H + ROW_H - 8) {
logmsg("menu: touch settings row -> settings page");
menu_arm_touch(TAP_ARM_DELAY_MS); /* 幽灵触摸防护:吃掉打开此行的那次触摸 */
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫 */
run_settings_page();
/* 重播音量基线(key_router)+ 本页播放/电源基线 */
key_router_rebase();
@@ -1889,7 +1902,7 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state,
m_prev_pwr = kp_is_down(&kp_power, KEY_POWER);
m_prev_play = kp_is_down(&kp_keys, KEY_PLAYPAUSE);
last_input_ms = now_ms(); /* 回主界面即视为输入 */
menu_arm_touch(800); /* 设置页返回后再忽略近期触摸 */
menu_arm_touch(MENU_TOUCH_GUARD_MS); /* M39:drain + 短守卫(原设置页返回 800ms) */
dirty = 1; /* 全帧重画应用列表 */
}
/* M2.18 排查乱跳:改为 else if —— 原为两个独立 if,退出行与
@@ -178,7 +178,12 @@ static inline const char *meow_str_zh(const char *id, const char *zh)
#define MENU_SWIPE_THRESH 65 /* px:抬手位移 >= 阈值判为滑动,不启动行(同 SDK SWIPE_THRESH) */
#define NCP_DRAG_ENTER_SLOP 16
#define NCP_DRAG_THR_PCT 40
#define TAP_ARM_DELAY_MS 300
/* M39:launcher 触摸守卫窗口(取代 M2.6「菜单关闭后忽略触摸」800ms / M2.22 的 App 退出后
* 1200ms / M2.6 打开子页 300ms)。语义只剩「drain 队列后仍防面板重配尾巴的幽灵按下」,
* 不再吞用户看到界面后的第一下:窗口起点固定在 drain 之后,且只覆盖极短的驱动尾巴。
* 依据:每次 arm 前都先 menu_drain_touch() 清本进程 touch_fd 队列(evdev 每 fd 各收一份,
* App/子页运行期间的全量事件都在本 fd 里,drain 后已无历史事件可重放)。 */
#define MENU_TOUCH_GUARD_MS 150
#define PM_NUM 6
#define PM_R 50 /* 圆钮半径(直径 100) */
#define PM_GAPX 130 /* 两列圆心 x 间距(175->305,居中 240) */
Binary file not shown.
Binary file not shown.
+39 -20
View File
@@ -117,10 +117,23 @@ static volatile int g_exit = 0;
static meow_ui_events g_events;
static int g_events_set = 0;
static uint64_t last_tick_ms = 0;
/* M2.22(C):启动 cooldown。应用刚进入时,用户点列表那几下(BTN_TOUCH 残留/抬手尾巴)
* 可能被当作「左缘滑动」→ 立即返回。窗口期丢弃 SWIPE(尤其 LEFT),仍接受 TAP。 */
static uint64_t startup_cooldown_until = 0;
#define STARTUP_COOLDOWN_MS 500
/* M39:启动输入门 = 一次性排空 + 极短「系统手势」防穿透(取代 M2.22(C) 的 500ms 全量吞窗)。
*
* 旧实现从 meow_ui_run 入口起 500ms 内丢弃 TAP/SWIPE/HOME/NOTIF_PULL/TOUCH_DOWN/MOVE/LONG_PRESS。
* 窗口起点在 App 画完首帧之后(reader/files 都是先 page_push 再 run),用户看到界面后伸手的
* 第一下必然落在窗口里被吞 → launcher/reader「要点两次」、files 概率、ncm/picbrowser 因首屏
* 更晚(LOADING/缩略图)点得晚才稳定成功。
*
* 残留输入的真实来源只有一处:evdev fd 在 input 线程启动时才 open,App 之前根本收不到;
* 能进队列的只有「fd 打开 → 进入分发」之间极短的尾巴 —— meow_input_drain() 一次性清掉即可
* (清环形队列 + 复位触摸状态机,见 meow_input_drain)。故:
* - TAP / TOUCH_DOWN / TOUCH_MOVE / LONG_PRESS 不再按时间吞(用户第一下立即生效);
* - 系统手势(左缘返回 SWIPE、底部上滑回桌面 HOME、顶部下拉通知中心、滑动进度条)保留
* STARTUP_GESTURE_GUARD_MS 防穿透:启动瞬间的幽灵手势不得把刚进入的 App 直接弹回桌面。
* 跨启动的在途手势仍不会产生 TAP:fd 打开前发生的按下 App 看不到(t_down=0),其抬手被
* touch_release 的首行 !t_down 早退丢弃。 */
static uint64_t startup_gesture_guard_until = 0;
#define STARTUP_GESTURE_GUARD_MS 150
/* settings.conf 路径(可被 env MEOW_SETTINGS_FILE 覆盖,宿主测试用;设备默认同 meow_ui_sys.c)。 */
static const char *meow_settings_path(void)
@@ -708,19 +721,23 @@ void meow_ui_run(const meow_ui_events *ev)
if (ev) { g_events = *ev; g_events_set = 1; }
g_exit = 0;
meow_input_start();
/* M39:启动排空一次(清掉 evdev fd 打开到进入分发之间堆积的事件尾巴 + 复位触摸状态机),
* 取代 M2.22(C) 的 500ms 按时间吞输入窗口。窗口只保留给系统手势(防穿透),见下。 */
meow_input_drain();
last_tick_ms = meow_ui_time_ms();
startup_cooldown_until = meow_ui_time_ms() + STARTUP_COOLDOWN_MS; /* M2.22(C):启动窗口丢弃 SWIPE */
startup_gesture_guard_until = meow_ui_time_ms() + STARTUP_GESTURE_GUARD_MS;
while (!g_exit) {
/* agent-119:实时触摸按压/按住/释放 → 即时重绘命中按钮区(不等 app 重绘)。
* IME 打开时软键盘为覆盖层且拦截触摸,不驱动背后按钮视觉,跳过。
* §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动;启动窗口不触发)。 */
* §6.3 通用长按:每帧检测「按下 ≥ 阈值且位移 ≤ 容忍」跨越(IME 打开不驱动)。
* M39:长按不再受启动窗口门控(长按需要按住 ≥ 阈值,启动残留手势已由 drain 清掉)。 */
if (!meow_ime_active()) {
meow_widget_touch_refresh();
if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll();
meow_input_long_press_poll();
} else if (meow_ime_mode_pinyin()) {
/* 拼音 IME 打开:喂长按轮询(长按退格清空字段)。英文模式不喂(保持既有行为)。 */
if (meow_ui_time_ms() >= startup_cooldown_until) meow_input_long_press_poll();
meow_input_long_press_poll();
}
meow_input_ev e;
@@ -729,10 +746,10 @@ void meow_ui_run(const meow_ui_events *ev)
if (g_exit) break; /* 已请求退出:不再分发剩余事件,杜绝「退出后又触发」的偶发再进/退 */
got = 1;
meow_ui_activity_ping(); /* M2.71-bug5:任意交互事件都喂 launcher 重置息屏计时(节流) */
int in_startup = (meow_ui_time_ms() < startup_cooldown_until); /* M2.22(C) */
int in_gesture_guard = (meow_ui_time_ms() < startup_gesture_guard_until); /* M39 */
if (e.type == MEOW_EV_TAP) {
if (in_startup) continue; /* 启动窗口也吞 TAP:旧手势残留点按(如上一次左滑抬起)
* 在进入瞬间命中 UI 按钮(如全屏按钮)会误触发——丢弃。 */
/* M39:TAP 不再按时间吞——用户看到界面后的第一下立即生效(旧 500ms 窗口在
* 首帧之后 arm,正是「要点两次」的根因;残留手势已在 run 入口 drain)。 */
/* 抬手未达标:清视觉条 */
meow_ui_swipe_hint(-1);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
@@ -754,7 +771,8 @@ void meow_ui_run(const meow_ui_events *ev)
if (meow_view_dispatch_event(MEOW_EV_KEY, e.a, e.b)) continue;
if (g_events_set && g_events.on_key) g_events.on_key(e.a, e.b);
} else if (e.type == MEOW_EV_SWIPE) {
if (in_startup) continue; /* 启动残留被当左缘滑动:丢弃,防进入即返回 */
if (in_gesture_guard) continue; /* M39:仅系统手势防穿透(启动瞬间幽灵手势不得把
* 刚进入的 App 直接弹回桌面);TAP 已不在此门控。 */
/* 抬手达标:清视觉条(左滑返回),触发行消失 */
meow_ui_swipe_hint(-1);
meow_ui_flush_region(0, meow_ui_statusbar_height(), meow_ui_width(), 6);
@@ -784,7 +802,7 @@ void meow_ui_run(const meow_ui_events *ev)
* 批① meow_bg_add 保活,进程保留转后台、SDK present 自抑);**不**调
* meow_ui_request_exit()——那会断送保活(进程 exit(0))。launcher 据 app 声明
* (background_keepalive/background_audio+媒体活跃)决定无限期/宽限。 */
if (in_startup) continue; /* 启动窗口误触残留:丢弃 */
if (in_gesture_guard) continue; /* M39:系统手势防穿透(与 SWIPE 同窗口) */
meow_ui_swipe_hint(-1);
meow_ui_gesture_end(); /* §6.4:抬手边沿,释放手势捕获 */
/* IME 打开时软键盘覆盖底部:键盘拥有底部区,把事件交给 IME 且**绝不**回桌面
@@ -792,7 +810,7 @@ void meow_ui_run(const meow_ui_events *ev)
if (meow_ime_active()) { (void)meow_ime_handle_event(&e); continue; }
meow_ui_nav_request_exit();
} else if (e.type == MEOW_EV_NOTIF_PULL) {
if (in_startup) continue; /* 启动窗口顶区误触残留:丢弃,防进入即开通知中心 */
if (in_gesture_guard) continue; /* M39:系统手势防穿透(防进入即开通知中心) */
/* 系统级「下拉展开通知中心」:起始于顶部热区的下滑为系统保留手势,不下发
* APP on_swipe;路由到系统钩子(App 进程只检测+经 IPC 上报 MSG_NOTIF_OPEN,
* 通知中心由 launcher 渲染,见 FRAMEWORK_DESIGN §10.5)。
@@ -803,10 +821,11 @@ void meow_ui_run(const meow_ui_events *ev)
meow_ui_sys_notif_pull(e.a); /* e.a = 手势按下原点 Y */
} else if (e.type == MEOW_EV_TOUCH_DOWN || e.type == MEOW_EV_TOUCH_MOVE) {
/* M2.71-bug2/6:拖动过程(跟手)。IME 弹出时触摸优先喂键盘(键盘覆盖层拦截,
* on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽);启动窗口也吞(防残留点按
* 误锁拖拽)。模态(dialog/sheet)期间本主循环被阻塞,天然不在此分发。
* on_touch_* 丢弃,避免按到键盘却在背后控件上锁拖拽)。模态(dialog/sheet)期间
* 本主循环被阻塞,天然不在此分发。
* M39:不再受启动窗口门控——跟手拖动是用户可感知的第一下交互;启动残留点按
* 已由 run 入口 meow_input_drain()(含触摸状态机复位)清掉,不会锁错拖拽。
* IME 打开时先把压点喂给拼音(候选栏滑动翻页判定 g_ime_swipe_y),再丢弃。 */
if (in_startup) continue;
if (meow_ime_active()) { meow_ime_handle_event(&e); continue; }
/* §6.4:down 捕获——命中目标捕获整个手势直到 up;随后 move/up 只发给该目标
* (防滑出后误触发下层)。真实触发放置捕获(宿主直调 on_touch_* 不受影响)。 */
@@ -817,7 +836,7 @@ void meow_ui_run(const meow_ui_events *ev)
g_events.on_touch_move(e.a, e.b);
got = 1; /* 交互事件:不睡眠,保持跟手帧同步 */
} else if (e.type == MEOW_EV_SWIPE_PROGRESS) {
if (in_startup) continue; /* 启动窗口进度条静音 */
if (in_gesture_guard) continue; /* M39:系统手势防穿透(启动进度条静音) */
int x0 = e.a, xc = e.b, dx = xc - x0; /* dx=当前X-按下X */
/* SDK 自动画顶部条:长度=手指当前 X(=距左缘距离,向内划长度随位移增长)。
* 全屏/收合下「左缘返回手势保留」,顶部动态提示条(meow_ui_swipe_hint)照常绘制
@@ -833,8 +852,8 @@ void meow_ui_run(const meow_ui_events *ev)
} else if (e.type == MEOW_EV_LONG_PRESS) {
/* §6.3 通用长按:按下 ≥ 阈值未抬手触发。坐标=触发时刻按下点(App 用
* meow_widget_hit/meow_listview_hit 判定命中控件/行)。已注册回调则分发;
* 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。 */
if (in_startup) continue;
* 其抬手已被 touch_release 抑制(不再产生 TAP/SWIPE),此处无需吞后续 tap。
* M39:不再受启动窗口门控(长按需按住达阈值,残留手势已由 drain 清掉)。 */
/* 拼音 IME 打开:长按退格清空字段(英文模式 handle_event 返回 0 → 走既有长按回调)。 */
if (meow_ime_active() && meow_ime_handle_event(&e)) continue;
meow_ui_gesture_end(); /* §6.4:长按已触发,其抬手不再 TAP/SWIPE——顺手释放捕获 */
+1 -1
View File
@@ -166,7 +166,7 @@ static void host_tap(int x, int y)
static void *driver(void *arg)
{
(void)arg;
usleep(600000); /* 等 meow_ui_run 起动(startup cooldown=500ms) */
usleep(600000); /* 等 meow_ui_run 起动(M39: 启动守卫 150ms) */
meow_input_inject(MEOW_EV_TAP, 150, 322); /* 点 TextField → 阻塞 IME */
usleep(350000); /* 等过 IME 打开 TAP 冷却窗(200ms,bug3)+ drain 40ms */
meow_input_inject(MEOW_EV_TAP, K_Q_X, K_Q_Y); /* shift 关 → 'q' */
+37 -1
View File
@@ -8,7 +8,9 @@
* M3 捕获手势滑出不误触:gesture_begin 命中按钮 A 后捕获;任凭概念滑动到远处(不重新命中),
* meow_ui_gesture_owner 恒为 A(绝不滑出误触下层/邻控件);end 释放;
* M4 list 焦点点外释放:meow_list_tap_at/meow_listview_tap_at 使列表成为唯一焦点;
* focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调。
* focus_tap 落在列表内 → 保位;落在列表外(页面内容/空白)→ 释放焦点并触发 lost 回调;
* M5 启动排空 drain 语义(M39):drain 清空已入队事件 + 复位在途手势(迟到抬手不产 TAP),
* 且 drain 之后的新按下仍正常产出 TAP(排空只清残留,不吞新输入)。
*
* 编译(宿主 gcc,链真实 widget/draw/input/activity/gfx/sys/theme/vol/log/dialog/ime/notify/ipc +
* 打桩 font/global;完整超集便于链接):
@@ -23,10 +25,15 @@
#include "meow_ui.h"
#include "meow_ui_internal.h"
#include "meow_ui_view.h"
#include "meow_ui_nav.h" /* meow_ui_nav_request_exit:meow_ui_input.c 的 MEOW_EV_HOME 分支引用 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 不链 meow_ui_nav.c:以桩满足 HOME 分支的链接依赖(同 test_meow_ui_home_gesture 范式)。 */
static int g_nav_exit_calls = 0;
void meow_ui_nav_request_exit(void) { g_nav_exit_calls++; }
static int g_fail = 0;
#define CHECK(cond, name) do { \
if (cond) { printf(" PASS %s\n", name); } \
@@ -132,6 +139,35 @@ int main(void)
CHECK(g_lost_calls == 2, "lost callback fired again");
(void)btnB;
/* ================= M5:启动排空 drain 语义(M39) =================
* M39 用「meow_ui_run 入口 drain 一次」取代 M2.22(C) 的 500ms 按时间吞输入窗口,
* 所以 drain 必须真的清干净:①队列里已入队的事件被丢弃;②在途手势(按下已发生、
* 抬手尚未到)被复位 —— 抬手不得再产出 TAP(否则 drain 之后会冒出迟到点击);
* ③drain 之后的新按下仍能正常产出 TAP(排空只清残留,不吞新输入)。 */
printf("M5 startup drain semantics (queue + in-flight gesture)\n");
{
/* ① 队列排空 */
meow_input_inject(MEOW_EV_TAP, 10, 20);
meow_input_inject(MEOW_EV_TAP, 30, 40);
meow_input_drain();
meow_input_ev e;
CHECK(!meow_input_pop(&e), "drain empties already-queued events");
/* ② 在途手势:按下 → drain → 抬手,不得产生迟到 TAP */
meow_input_touch_set(1, 120, 130, 0);
meow_input_drain();
meow_input_touch_release();
CHECK(!meow_input_pop(&e), "drain resets in-flight gesture (late release -> no TAP)");
/* ③ drain 之后的新一次点击正常产出 */
meow_input_touch_set(1, 200, 300, 0);
meow_input_touch_release();
int ok = meow_input_pop(&e);
int coords_ok = ok && e.type == MEOW_EV_TAP && e.a == 200 && e.b == 300;
if (!coords_ok && ok) printf(" tap after drain: type=%d a=%d b=%d\n", e.type, e.a, e.b);
CHECK(coords_ok, "tap after drain still dispatches");
}
/* 清理:控件释放自动注销 z 序绘制物 + 焦点/捕获复位 */
meow_widget_free(list);
meow_widget_free(btnB);
+132 -1
View File
@@ -12,6 +12,14 @@
* 3b 未注册 on_swipe:注入 MEOW_EV_SWIPE(UP)(中部上滑)→ nav_request_exit 不触发(App 滚动);
* 3c 注册 on_swipe:注入 MEOW_EV_HOME → nav_request_exit 触发(系统级手势优先于 App on_swipe),
* on_swipe 不被调用——屏底热区上滑是系统保留手势,任意 App 内生效(用户 2026-09-04 批②)。
* T4 启动输入门(M39,取代 M2.22(C) 的 500ms 全量吞窗:启动 drain + ≤150ms 系统手势防穿透):
* 4a 启动后立即 TAP → on_tap 立即生效(旧窗口会吞,就是「要点两次」的根因);
* 4b 启动后立即 HOME → 守卫期不触发(防穿透);守卫过后再上滑 → 触发一次;
* 4c 启动后立即 SWIPE(LEFT) → 守卫期不下发;守卫过后 → 下发一次;
* 4d 启动后立即 TOUCH_DOWN / TOUCH_MOVE → 跟手回调立即收到;
* 4e 启动后立即 LONG_PRESS → 长按回调立即收到;
* 4f meow_ui_run 入口 drain → run 之前已入队的 TAP/HOME/SWIPE 全部丢弃(旧时间窗要解决的
* 「残留输入」改由一次性排空解决,且不影响 4a 的新输入)。
*
* 编译(宿主 gcc,链真实 SDK 源全集 + 打桩 font/global;**不链 meow_ui_nav.c**——本测以测试内
* meow_ui_nav_request_exit 桩计数验证触发):
@@ -140,7 +148,7 @@ static int g_inj_type, g_inj_a, g_inj_b;
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再注入 */
usleep(700000); /* 等启动守卫(M39: 150ms,原 500ms cooldown)过后再注入 */
meow_input_inject(g_inj_type, g_inj_a, g_inj_b);
usleep(80000);
meow_ui_request_exit();
@@ -181,6 +189,128 @@ static void test_dispatch(void)
CHECK(g_swipe_calls == 0, "T3c system-priority home -> on_swipe NOT called");
}
/* ============ T4 启动输入门(M39) ============
* 语义:meow_ui_run 入口 meow_input_drain() 排空一次(清队列 + 复位触摸状态机),随后只对
* 「系统手势」(SWIPE / HOME / NOTIF_PULL / SWIPE_PROGRESS)保留 ≤150ms 防穿透;
* TAP / TOUCH_DOWN / TOUCH_MOVE / LONG_PRESS 立即生效。 */
#define T4_GUARD_WAIT_MS 260 /* > STARTUP_GESTURE_GUARD_MS(150),留调度余量 */
static volatile int g_ticks = 0; /* run 循环 tick 计数(注入时序锚点) */
static void tick_marker(void) { g_ticks++; }
static int g_taps = 0, g_tap_x = -1, g_tap_y = -1;
static void tap_marker(int x, int y) { g_taps++; g_tap_x = x; g_tap_y = y; }
static int g_downs = 0, g_moves = 0;
static void down_marker(int x, int y) { (void)x; (void)y; g_downs++; }
static void move_marker(int x, int y) { (void)x; (void)y; g_moves++; }
static int g_longs = 0;
static void long_marker(int x, int y) { (void)x; (void)y; g_longs++; }
static int g_inj2_type, g_inj2_a, g_inj2_b, g_inj_later;
/* 首个 tick(run 循环已进入、run 入口 drain 已过)注入一次;g_inj_later=1 时越过守卫再注入一次。 */
static void *drv_startup(void *arg)
{
(void)arg;
while (g_ticks < 1) usleep(1000);
meow_input_inject(g_inj_type, g_inj_a, g_inj_b);
if (g_inj_later) {
usleep(T4_GUARD_WAIT_MS * 1000);
meow_input_inject(g_inj2_type, g_inj2_a, g_inj2_b);
}
usleep(80000);
meow_ui_request_exit();
return NULL;
}
static void run_startup_scenario(int type, int a, int b, int later,
void (*tap_cb)(int, int), void (*swipe_cb)(int, int, int),
void (*td_cb)(int, int), void (*tm_cb)(int, int))
{
g_inj_type = type; g_inj_a = a; g_inj_b = b;
g_inj2_type = type; g_inj2_a = a; g_inj2_b = b;
g_inj_later = later;
g_ticks = 0; g_taps = 0; g_tap_x = g_tap_y = -1;
g_downs = 0; g_moves = 0; g_longs = 0;
g_nav_exit_calls = 0;
g_swipe_calls = 0; g_swipe_dir = -1; g_swipe_dx = 0;
meow_input_drain();
meow_ui_events ev; memset(&ev, 0, sizeof ev);
ev.on_tick = tick_marker;
ev.on_tap = tap_cb;
ev.on_swipe = swipe_cb;
ev.on_touch_down = td_cb;
ev.on_touch_move = tm_cb;
pthread_t thd; pthread_create(&thd, NULL, drv_startup, NULL);
meow_ui_run(&ev);
pthread_join(thd, NULL);
}
/* run 之前已入队(模拟「进入 App 前就堆积的残留输入」),run 入口 drain 必须全部丢掉。 */
static void *drv_predrain(void *arg)
{
(void)arg;
usleep(T4_GUARD_WAIT_MS * 1000); /* 越过守卫:证明不是被窗口吞,而是被 drain 清掉 */
meow_ui_request_exit();
return NULL;
}
static void run_predrain_scenario(void)
{
g_taps = 0; g_nav_exit_calls = 0; g_swipe_calls = 0;
meow_input_drain();
meow_input_inject(MEOW_EV_TAP, 120, 200);
meow_input_inject(MEOW_EV_HOME, MEOW_SWIPE_UP, 120);
meow_input_inject(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80);
meow_ui_events ev; memset(&ev, 0, sizeof ev);
ev.on_tap = tap_marker;
ev.on_swipe = app_on_swipe;
pthread_t thd; pthread_create(&thd, NULL, drv_predrain, NULL);
meow_ui_run(&ev);
pthread_join(thd, NULL);
}
static void test_startup_gate(void)
{
printf("T4 startup input gate (drain + <=150ms system-gesture guard)\n");
/* 4a 启动后立即点击:必须生效(旧 500ms 窗口在首帧后 arm → 会吞掉这一下) */
run_startup_scenario(MEOW_EV_TAP, 120, 200, 0, tap_marker, NULL, NULL, NULL);
CHECK(g_taps == 1, "T4a immediate TAP after start -> on_tap fired (got %d)", g_taps);
CHECK(g_taps == 1 && g_tap_x == 120 && g_tap_y == 200, "T4a tap coords preserved (%d,%d)", g_tap_x, g_tap_y);
/* 4b 启动后立即上滑回桌面:守卫期不触发;守卫过后同一手势触发一次 */
run_startup_scenario(MEOW_EV_HOME, MEOW_SWIPE_UP, 120, 1, NULL, NULL, NULL, NULL);
CHECK(g_nav_exit_calls == 1, "T4b immediate HOME blocked by guard, post-guard HOME fires exactly once (got %d)", g_nav_exit_calls);
/* 4c 启动后立即左缘返回 SWIPE:守卫期不下发;守卫过后下发一次(行为与修改前一致) */
run_startup_scenario(MEOW_EV_SWIPE, MEOW_SWIPE_LEFT, 80, 1, NULL, app_on_swipe, NULL, NULL);
CHECK(g_swipe_calls == 1, "T4c immediate SWIPE(LEFT) blocked by guard, post-guard delivers exactly once (got %d)", g_swipe_calls);
CHECK(g_swipe_dir == MEOW_SWIPE_LEFT, "T4c swipe dir == LEFT (got %d)", g_swipe_dir);
/* 4d 启动后立即跟手拖动:DOWN/MOVE 立即下发(不再被启动窗口吞) */
run_startup_scenario(MEOW_EV_TOUCH_DOWN, 240, 300, 0, NULL, NULL, down_marker, move_marker);
CHECK(g_downs == 1, "T4d immediate TOUCH_DOWN delivered (got %d)", g_downs);
run_startup_scenario(MEOW_EV_TOUCH_MOVE, 240, 350, 0, NULL, NULL, down_marker, move_marker);
CHECK(g_moves == 1, "T4d immediate TOUCH_MOVE delivered (got %d)", g_moves);
/* 4e 启动后立即长按:长按回调立即收到(长按本身需按住达阈值,启动窗口门控已移除) */
meow_ui_set_long_press_cb(long_marker);
run_startup_scenario(MEOW_EV_LONG_PRESS, 200, 420, 0, NULL, NULL, NULL, NULL);
CHECK(g_longs == 1, "T4e immediate LONG_PRESS delivered (got %d)", g_longs);
meow_ui_set_long_press_cb(NULL);
/* 4f run 入口 drain:进入前堆积的 TAP/HOME/SWIPE 全部丢弃(替代旧时间窗的防残留语义) */
run_predrain_scenario();
CHECK(g_taps == 0, "T4f pre-queued TAP drained at run entry (got %d)", g_taps);
CHECK(g_nav_exit_calls == 0, "T4f pre-queued HOME drained at run entry (got %d)", g_nav_exit_calls);
CHECK(g_swipe_calls == 0, "T4f pre-queued SWIPE drained at run entry (got %d)", g_swipe_calls);
}
int main(void)
{
meow_ui_opts o = { FBW, FBH };
@@ -189,6 +319,7 @@ int main(void)
test_predicate();
test_touch_classify();
test_dispatch();
test_startup_gate();
meow_ui_shutdown();
printf("home_gesture: %d pass / %d fail\n", g_pass, g_fail);
+1 -1
View File
@@ -61,7 +61,7 @@ static int vev_child(meow_view *v, int ev, int a, int b, void *ctx)
static void *driver(void *arg)
{
(void)arg;
usleep(700000); /* 等 startup cooldown(500ms) 过后再发 */
usleep(700000); /* 等启动守卫(M39: 150ms)过后再发 */
meow_input_inject(MEOW_EV_KEY, MEOW_KEY_VOLUMEUP, 1); /* child 消费 → app_on_key 不触发 */
usleep(80000);
meow_input_inject(MEOW_EV_TAP, 10, 10); /* parent 消费 → app_on_tap 不触发 */
+2 -2
View File
@@ -28,12 +28,12 @@
# ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)----
patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|4441822d87e76bfce2ce584604ac3830|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A)
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|bb2d5f1251a99a6b1a7c7dddb5f338ac|M6.46 编入杀后台修复(M7:bgmgr_can_kill 门控 framework→protected_app+后台页 sub 指针持久化(悬垂修正)+am kill 走 meow_bg_terminate 与页面按钮同通道)+SDK 时钟 ABI 修复(M11:meow_ui_time 按 o32 time32 取时并链接期接管, 链 meow_ui.a 4c4a1554);继承M6.41 收编启动失败原因透传(subpage_t/meow_launcher_internal启动失败原因)+后台管理按钮点不动修复(bg_page)+主菜单字体/样式微调;继承M6.40(系统级音量翻页映射+P1 MD3 select面板28/对话框按钮胶囊)+M6.39(SDK LazyGrid/LazyColumn/LazyRow MD3容器化+SDK全局App内底部上滑回桌面)+M6.38(批②上滑回桌面+SDK对话框MD3细化+UI微调+空artwork)+M6.37(SDK默认列表MD3容器化)+M6.36(批①叠加/穿透根治+MD3全面对齐+后台图标/圆角卡+批①后台常驻框架+批③后台管理页+i18n);链meow_ui.a(sdk meow_lyric/lyric_view/meow_img_color,meow_img_pixels访问器);0warning
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|e342d24fa9fd878c840e5b3e918cfbdf|M39 点击输入门(launcher 触摸守卫 1200ms→drain+MENU_TOUCH_GUARD_MS 150ms(App退出)/800ms×6(子页返回)/300ms×8(菜单/子页打开)统一收窄,drain 同时复位 t_down/原点防幽灵抬手重放;SDK meow_ui_run 500ms 全量吞窗→启动 meow_input_drain 一次+TAP/TOUCH_DOWN/MOVE/LONG_PRESS 不再按时间吞,仅系统手势(SWIPE/HOME/NOTIF_PULL/SWIPE_PROGRESS)保留 150ms 防穿透), 链 meow_ui.a 8525aa6b;继承 M6.46 编入杀后台修复(M7:bgmgr_can_kill 门控 framework→protected_app+后台页 sub 指针持久化(悬垂修正)+am kill 走 meow_bg_terminate 与页面按钮同通道)+SDK 时钟 ABI 修复(M11:meow_ui_time 按 o32 time32 取时并链接期接管, 旧链 meow_ui.a 4c4a1554);继承M6.41 收编启动失败原因透传(subpage_t/meow_launcher_internal启动失败原因)+后台管理按钮点不动修复(bg_page)+主菜单字体/样式微调;继承M6.40(系统级音量翻页映射+P1 MD3 select面板28/对话框按钮胶囊)+M6.39(SDK LazyGrid/LazyColumn/LazyRow MD3容器化+SDK全局App内底部上滑回桌面)+M6.38(批②上滑回桌面+SDK对话框MD3细化+UI微调+空artwork)+M6.37(SDK默认列表MD3容器化)+M6.36(批①叠加/穿透根治+MD3全面对齐+后台图标/圆角卡+批①后台常驻框架+批③后台管理页+i18n);链meow_ui.a(sdk meow_lyric/lyric_view/meow_img_color,meow_img_pixels访问器);0warning
patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|ad8c1f2bfe5481d2069466e22d5e1cf6|应用看门狗(亮屏亮度按 settings 恢复,四支决策不再回退固定 60;M6.23 源码修复首次真正编入固件)
patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187)
mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)
patch_v2/meow_sysui/meow_sysui|/usr/bin/meow_sysui|173013cc8f0899b3001d0b894ac0d9e6|系统 UI(sysui)
patch_v2/meowpkg/nyai|/usr/bin/nyai|a7d24553ebf8e47cd251236840b5259c|nyai M2.97(M2.89 导出 exec 相对化 + 271 app.json depends 安装提示 agent-240/271)
patch_v2/meowpkg/nyai|/usr/bin/nyai|ef7940ecf5ece993d718ac1d9019cc82|nyai M35 r1(cf5703b 重编:包身份以已签名 app.json 清单为准 + 暂存迁 SD 并必清理 + 修 exFAT 大小写绕过/fmt_utc 1/2 月/成员名/炸弹纵深;M39 同 commit 把钉值补上——此前仓库内 nyai 已是 ef7940ec 而清单仍钉 M2.97 的 a7d24553,从 main 打包固件会在此行 exit 1);继承 M2.97(M2.89 导出 exec 相对化 + 271 app.json depends 安装提示 agent-240/271)
patch_v2/meow_svc_power/meow_svc_power|/usr/bin/meow_svc_power|39ae3aa255695a97f4281124c0621b52|电源专项服务(充电态权威源 mp2731-charger/status,修 battery/status 撒谎恒 Discharging)
patch_v2/meow_verify/meow_verify|/usr/bin/meow_verify|6c2e8ed24aebe5caf41e7aed391312d5|II期验签 2B(多公钥+v2签名块 agent-221)
patch_v2/meow_crash/meow_crash|/usr/bin/meow_crash|f02c4f45099157dc388b3d10b443aa57|系统级崩溃捕获 (agent-187) + log升级: 清理前缀扩 perf_/hang_ 承接1GB纪律 (337)