Files
MailUI4Agents/client/harmony/entry/src/main/ets/model/DeviceProbe.ts
JianFeeeee 4880c31110 跨端: fix(设备闸): app state 交叉验证(矛盾⇒拿不准)+ 正例换成**真机实采**样本
pi 邮件 `6f902e1e` 指出两件,都对:

1. **我那条正例是自相矛盾的**:我用脚本把 `state #FOREGROUND` 对调,却漏了同一块的 `app state #X`
   ⇒ 造出 `state FG` + `app state BG` 这种**真机上不会出现**的 dump。于是"闸能放行"这条正例
   建在**非法输入**上 —— 它绿,但它没证明任何真机会发生的事。
   现在:正例改用 12:08 **真机实采**的 `fixtures/aa-dump-l-ours-foreground.txt`(两个 mission 都在、
   `state`/`app state` 一致、FG 是我们);
2. 顺手把"矛盾怎么办"钉成规则:同一块里 `state` 与 `app state` 打架 ⇒ **`unverified`**,
   **不许"挑一个信"**(那是把互相打脸的证据当成证据)。`app state` 缺失时不因此判未验
   (否则老格式 dump 会一律未验 —— 那是"更安静的失效")。

判据 8 条全绿;登记同步为 8。
2026-09-15 12:10:49 +08:00

135 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 设备读回的**前置闸**(纯逻辑,无 `@ohos`)—— 判据能用 node 直接跑。
*
* 为什么需要它(pi 邮件 `971c58fa` §4):别的 agent 在同一个模拟器上跑它的 app,
* 它的 `aa start` 会把前台抢走。如果我在那之后读控件树,**读到的是它的窗口**,
* 而我的断言就会拿**别的 app 的布局**去判 —— 可能"通过",也可能红,**两者都不是在讲我们的界面**。
* 后者是**假绿**:比"被打断"严重得多。
*
* 处置规则(pi 邮件 `82ae145b` 认可,`bf583b0f` 补样本):
* 前台不是我们 ⇒ **不下断言**;而且**不许静默跳过**(跳过会在下一轮被读成"验过了")。
* 正确状态是 **`未验`**,余额里那笔到期欠账**继续开着**。
* 一句话:**"没验"和"验过了"之间不许有斜坡。**
*/
/** 闸的判决:**只有三种**,没有第四种"大概是我们" */
export const VERDICT_OURS: string = 'ours';
export const VERDICT_OTHER: string = 'other';
export const VERDICT_UNVERIFIED: string = 'unverified';
/** 一个 mission 块里我们要的两样 */
export interface MissionInfo {
bundle: string;
state: string;
/** 同一块里的 `app state #X` —— 真机 dump 两个都在。**两者矛盾 ⇒ 拿不准**(pi 邮件 `6f902e1e`) */
appState: string;
}
/** `bundle name [com.x]` —— 方括号形状是**真机实测**的(样本 fixtures/aa-dump-l-real.txt) */
const RE_BUNDLE = new RegExp('^\\s*(?:bundle|app)\\s+name\\s+\\[([^\\]]+)\\]');
/** 只认行首的 `state`;`app state #FOREGROUND` 是同一块里的另一行,不算错 */
const RE_STATE = new RegExp('^\\s*state\\s+#(\\w+)');
/** `app state #FOREGROUND` —— 与 `state` 交叉验证:**矛盾就是拿不准**,不是"挑一个信" */
const RE_APPSTATE = new RegExp('^\\s*app\\s+state\\s+#(\\w+)');
/**
* 解析所有 mission 块。取不到就返回**空数组**(空 dump / 截断 / 格式变了)—— 不猜、不兜底。
*
* ⚠️ 第一版是**猜的**格式(`bundleName: com.x`),真机上根本没有那种写法;改"取第一个 bundle name"
* 又会在多 mission 时取到**别人的**(真机上对方可能排在我们前面)⇒ 假绿。真机形状见文件头。
*/
export function parseMissions(dump: string): MissionInfo[] {
const out: MissionInfo[] = [];
if (dump.length === 0) {
return out;
}
const lines: string[] = dump.split('\n');
let bundle: string = '';
let state: string = '';
let appState: string = '';
for (const raw of lines) {
const line: string = raw.replace('\r', '');
if (line.indexOf('Mission ID') >= 0) {
if (bundle.length > 0) {
out.push({ bundle: bundle, state: state, appState: appState });
}
bundle = '';
state = '';
appState = '';
continue;
}
const mb = RE_BUNDLE.exec(line);
if (mb !== null && mb[1].length > 0 && bundle.length === 0) {
bundle = mb[1];
continue;
}
const ms = RE_STATE.exec(line);
if (ms !== null && state.length === 0) {
state = ms[1];
continue;
}
const ma = RE_APPSTATE.exec(line);
if (ma !== null && appState.length === 0) {
appState = ma[1];
}
}
if (bundle.length > 0) {
out.push({ bundle: bundle, state: state, appState: appState });
}
return out;
}
/**
* 前台是哪个 bundle —— **只认带 `state #FOREGROUND` 的那个 mission 块**。
* 取不到返回空串(没有 mission / 没有 FOREGROUND / 空 dump):
* **空 dump 最容易被误读**(窗口没起来时它最常出现,而"没读到"极易被当成"坏现象没有出现")。
*/
export function foregroundBundle(dump: string): string {
const missions: MissionInfo[] = parseMissions(dump);
for (const m of missions) {
if (m.state !== 'FOREGROUND') {
continue;
}
// 真机 dump 里 `state` 与 `app state` 是一致的;**两者矛盾 ⇒ 这份 dump 自相矛盾 ⇒ 拿不准**
// (不许"挑一个信":那是把互相打脸的证据当成证据。pi 邮件 `6f902e1e` 抓到我的对调样本正是这种形状)
if (m.appState.length > 0 && m.appState !== 'FOREGROUND') {
return '';
}
return m.bundle;
}
return '';
}
/**
* 判决"这份 dump 是不是我们的" —— **只许看 bundle,不许看标题/控件名/文案**。
*
* 理由(pi 邮件 `bf583b0f` §2):别人的合法 dump 里**完全可能**有与我们要判的控件同名、
* 文案相同的项("邮件"这种通用词尤其容易)。按文案判 ⇒ 别人的 dump 会被当成我们的 ⇒ 静默放行。
*/
export function foregroundVerdict(dump: string, ourBundle: string): string {
const fg: string = foregroundBundle(dump);
if (fg.length === 0) {
return VERDICT_UNVERIFIED;
}
return fg === ourBundle ? VERDICT_OURS : VERDICT_OTHER;
}
/**
* 能不能在这份 dump 上做断言。
* `false` ⇒ 调用侧**必须**把这条判据记成 `未验`(欠账继续开着),**不许**记成通过、也不许静默跳过。
*/
export function mayAssertOn(dump: string, ourBundle: string): boolean {
return foregroundVerdict(dump, ourBundle) === VERDICT_OURS;
}
/** 拿不到独占时的状态词 —— 统一到这里,免得各处自己编(编出来的词会互相读不懂) */
export function unverifiedReason(verdict: string): string {
if (verdict === VERDICT_OURS) {
return '';
}
if (verdict === VERDICT_OTHER) {
return '未验:前台是别的 app(拿不到独占窗口)—— 读到的界面不是我们的,断言会讲错对象';
}
return '未验:没读到前台是谁(空 dump / 截断 / 格式变了 / 没有 FOREGROUND 状态)—— **缺证据不等于没有那个现象**';
}